Documentation Index
Fetch the complete documentation index at: https://mintlify.com/iii-hq/sdk/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The III SDK provides a context-aware Logger that automatically attaches trace IDs, span IDs, and function names to every log message. Access it via getContext().logger inside function handlers.
Quick Start
import { getContext } from 'iii-sdk'
iii.registerFunction({ id: 'users::create' }, async (input) => {
const { logger } = getContext()
logger.info('Creating user', { email: input.email })
logger.warn('Duplicate email detected', { email: input.email })
logger.error('Database connection failed', { error: 'timeout' })
logger.debug('Validation passed', { fields: Object.keys(input) })
return { success: true }
})
Logger API
Methods
class Logger {
info(message: string, data?: unknown): void
warn(message: string, data?: unknown): void
error(message: string, data?: unknown): void
debug(message: string, data?: unknown): void
}
Parameters
- message (string): Human-readable log message
- data (optional): Structured data to attach (objects, arrays, primitives)
Automatic Context Attributes
The Logger automatically attaches these attributes to every log:
// Emitted log structure:
{
severityNumber: 9, // SeverityNumber.INFO
body: 'Creating user',
attributes: {
'trace_id': '4bf92f3577b34da6a3ce929d0e0e4736', // Auto-attached
'span_id': '00f067aa0ba902b7', // Auto-attached
'service.name': 'users::create', // Auto-attached (function ID)
'log.data': '{"email":"user@example.com"}' // Your data (stringified)
}
}
Context Propagation
Logger is created per function invocation
Each function call gets a unique Logger instance with its own trace context:iii.registerFunction({ id: 'orders::create' }, async (input) => {
const { logger } = getContext() // Unique logger for this invocation
logger.info('Starting order creation') // trace_id: abc123
// Call another function
await iii.call('inventory::reserve', { sku: input.sku })
logger.info('Order created') // Same trace_id: abc123
return { success: true }
})
iii.registerFunction({ id: 'inventory::reserve' }, async (input) => {
const { logger } = getContext() // Different logger, SAME trace_id!
logger.info('Reserving inventory') // trace_id: abc123 (inherited)
return { reserved: true }
})
Trace IDs propagate across services
When function A calls function B, the trace context is automatically propagated:orders::create (trace_id: abc123)
└── inventory::reserve (trace_id: abc123) ← Same trace ID!
└── db::query (trace_id: abc123) ← Same trace ID!
Severity Levels
import { SeverityNumber } from 'iii-sdk'
// Severity mapping:
logger.debug(...) // SeverityNumber.DEBUG = 5
logger.info(...) // SeverityNumber.INFO = 9
logger.warn(...) // SeverityNumber.WARN = 13
logger.error(...) // SeverityNumber.ERROR = 17
Structured Logging
Always pass structured data (objects) instead of interpolating strings:
import { getContext } from 'iii-sdk'
iii.registerFunction({ id: 'payments::process' }, async (input) => {
const { logger } = getContext()
// ❌ Bad: string interpolation
logger.info(`Processing payment for user ${input.userId} amount ${input.amount}`)
// ✅ Good: structured data
logger.info('Processing payment', {
userId: input.userId,
amount: input.amount,
currency: input.currency,
paymentMethod: input.method
})
// ✅ Great: include error objects
try {
await stripe.charge(input)
} catch (error) {
logger.error('Payment failed', {
userId: input.userId,
amount: input.amount,
errorCode: error.code,
errorMessage: error.message,
stripeRequestId: error.requestId
})
}
return { success: true }
})
Fallback Behavior
When OpenTelemetry is disabled or not initialized, the Logger falls back to standard console/logging:
// OTel disabled
const iii = init('ws://localhost:49134', {
otel: { enabled: false }
})
iii.registerFunction({ id: 'test' }, async (input) => {
const { logger } = getContext()
logger.info('Hello') // Falls back to console.info('[test] Hello')
logger.error('Error', { code: 500 }) // console.error('[test] Error', { code: 500 })
})
Accessing the Active Span
import { getContext, SpanStatusCode } from 'iii-sdk'
iii.registerFunction({ id: 'analytics::process' }, async (input) => {
const { logger, trace } = getContext()
// Add custom attributes to the span
trace?.setAttribute('analytics.type', input.type)
trace?.setAttribute('analytics.dataset_size', input.data.length)
// Add events to the span
trace?.addEvent('Starting data processing')
try {
const result = await processData(input.data)
trace?.addEvent('Processing complete', { rows: result.length })
logger.info('Analytics processed', { rows: result.length })
return result
} catch (error) {
// Record exception in span
trace?.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
trace?.recordException(error)
logger.error('Analytics processing failed', { error: error.message })
throw error
}
})
Consuming Logs
import type { OtelLogEvent } from 'iii-sdk'
// Subscribe to all logs from the engine
const unsubscribe = iii.onLog((log: OtelLogEvent) => {
console.log(`[${log.service_name}] ${log.body}`, log.attributes)
})
// Filter by severity level
iii.onLog((log) => {
console.error('ERROR:', log.body, log.attributes)
}, { level: 'error' })
// Later: stop consuming logs
unsubscribe()
Best Practices
Logging Best Practices:
- ✅ Use
getContext().logger instead of console.log
- ✅ Always pass structured data (objects) to the
data parameter
- ✅ Use appropriate severity levels (
info for normal flow, error for failures)
- ✅ Log at decision points (validation, errors, external calls)
- ✅ Include relevant IDs (userId, orderId, requestId) in log data
- ✅ Avoid logging sensitive data (passwords, tokens, PII)
- ✅ Use
trace?.recordException(error) for automatic error tracking
- ✅ Keep log messages concise and searchable (don’t include dynamic data in message)
Example: Full Context Usage
import { getContext, SpanStatusCode } from 'iii-sdk'
iii.registerFunction({ id: 'orders::create' }, async (input) => {
const { logger, trace } = getContext()
// Add business context to span
trace?.setAttribute('order.user_id', input.userId)
trace?.setAttribute('order.total', input.total)
trace?.setAttribute('order.items_count', input.items.length)
logger.info('Creating order', {
userId: input.userId,
itemCount: input.items.length,
total: input.total
})
try {
// Validate
if (input.total < 0) {
logger.warn('Invalid order total', { total: input.total, userId: input.userId })
return { error: 'Invalid total' }
}
// Reserve inventory
trace?.addEvent('Reserving inventory')
const reserved = await iii.call('inventory::reserve', { items: input.items })
if (!reserved.success) {
logger.warn('Inventory reservation failed', { userId: input.userId })
return { error: 'Out of stock' }
}
// Process payment
trace?.addEvent('Processing payment')
const payment = await iii.call('payments::charge', {
userId: input.userId,
amount: input.total
})
logger.info('Order created successfully', {
orderId: payment.orderId,
userId: input.userId,
total: input.total
})
return { success: true, orderId: payment.orderId }
} catch (error) {
trace?.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
trace?.recordException(error)
logger.error('Order creation failed', {
userId: input.userId,
error: error.message,
stack: error.stack
})
throw error
}
})