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 viagetContext().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 }
})
from iii import get_context
async def create_user(input_data: dict) -> dict:
ctx = get_context()
ctx.logger.info('Creating user', {'email': input_data['email']})
ctx.logger.warn('Duplicate email detected', {'email': input_data['email']})
ctx.logger.error('Database connection failed', {'error': 'timeout'})
ctx.logger.debug('Validation passed', {'fields': list(input_data.keys())})
return {'success': True}
iii.register_function('users::create', create_user)
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
}
class Logger:
def info(self, message: str, data: Any = None) -> None: ...
def warn(self, message: str, data: Any = None) -> None: ...
def error(self, message: str, data: Any = None) -> None: ...
def debug(self, message: str, data: Any = None) -> None: ...
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)
}
}
# Emitted log structure:
{
'timestamp': 1234567890123456789,
'severity_number': 9, # SeverityNumber.INFO
'severity_text': 'INFO',
'body': 'Creating user',
'attributes': {
'function_name': 'users::create', # Auto-attached
'data': '{"email": "user@example.com"}' # Your data (stringified)
},
'trace_id': 307445734561825860130216614920333936, # Auto-attached (if OTel enabled)
'span_id': 4049083503131475655 # Auto-attached (if OTel enabled)
}
Context Propagation
1
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 }
})
async def create_order(input_data: dict) -> dict:
ctx = get_context() # Unique context for this invocation
ctx.logger.info('Starting order creation') # trace_id: abc123
# Call another function
await iii.call('inventory::reserve', {'sku': input_data['sku']})
ctx.logger.info('Order created') # Same trace_id: abc123
return {'success': True}
async def reserve_inventory(input_data: dict) -> dict:
ctx = get_context() # Different context, SAME trace_id!
ctx.logger.info('Reserving inventory') # trace_id: abc123 (inherited)
return {'reserved': True}
iii.register_function('orders::create', create_order)
iii.register_function('inventory::reserve', reserve_inventory)
2
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
from opentelemetry._logs import SeverityNumber
# 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 }
})
from iii import get_context
async def process_payment(input_data: dict) -> dict:
ctx = get_context()
# ❌ Bad: string interpolation
ctx.logger.info(f"Processing payment for user {input_data['user_id']} amount {input_data['amount']}")
# ✅ Good: structured data
ctx.logger.info('Processing payment', {
'user_id': input_data['user_id'],
'amount': input_data['amount'],
'currency': input_data['currency'],
'payment_method': input_data['method']
})
# ✅ Great: include error objects
try:
await stripe.charge(input_data)
except Exception as error:
ctx.logger.error('Payment failed', {
'user_id': input_data['user_id'],
'amount': input_data['amount'],
'error_code': getattr(error, 'code', None),
'error_message': str(error),
'stripe_request_id': getattr(error, 'request_id', None)
})
return {'success': True}
iii.register_function('payments::process', process_payment)
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 })
})
# OTel disabled
iii = III('ws://localhost:49134', InitOptions(
otel={'enabled': False}
))
async def test_func(input_data: dict) -> dict:
ctx = get_context()
ctx.logger.info('Hello') # Falls back to logging.info('[test] Hello')
ctx.logger.error('Error', {'code': 500}) # logging.error('[test] Error', extra={'data': {...}})
return {}
iii.register_function('test', test_func)
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
}
})
from iii import get_context
from opentelemetry.trace import StatusCode
async def process_analytics(input_data: dict) -> dict:
ctx = get_context()
# Add custom attributes to the span
if ctx.trace:
ctx.trace.set_attribute('analytics.type', input_data['type'])
ctx.trace.set_attribute('analytics.dataset_size', len(input_data['data']))
ctx.trace.add_event('Starting data processing')
try:
result = await process_data(input_data['data'])
if ctx.trace:
ctx.trace.add_event('Processing complete', {'rows': len(result)})
ctx.logger.info('Analytics processed', {'rows': len(result)})
return result
except Exception as error:
# Record exception in span
if ctx.trace:
ctx.trace.set_status(StatusCode.ERROR, str(error))
ctx.trace.record_exception(error)
ctx.logger.error('Analytics processing failed', {'error': str(error)})
raise
iii.register_function('analytics::process', process_analytics)
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()
from iii.iii_types import OtelLogEvent
# Subscribe to all logs from the engine
def log_handler(log: dict) -> None:
print(f"[{log['service_name']}] {log['body']}", log['attributes'])
unsubscribe = iii.on_log(log_handler)
# Filter by severity level (engine-side filtering)
iii.on_log(lambda log: print('ERROR:', log['body']), {'level': 'error'})
# Later: stop consuming logs
unsubscribe()
Best Practices
Logging Best Practices:
- ✅ Use
getContext().loggerinstead ofconsole.log - ✅ Always pass structured data (objects) to the
dataparameter - ✅ Use appropriate severity levels (
infofor normal flow,errorfor 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
}
})
from iii import get_context
from opentelemetry.trace import StatusCode
async def create_order(input_data: dict) -> dict:
ctx = get_context()
# Add business context to span
if ctx.trace:
ctx.trace.set_attribute('order.user_id', input_data['user_id'])
ctx.trace.set_attribute('order.total', input_data['total'])
ctx.trace.set_attribute('order.items_count', len(input_data['items']))
ctx.logger.info('Creating order', {
'user_id': input_data['user_id'],
'item_count': len(input_data['items']),
'total': input_data['total']
})
try:
# Validate
if input_data['total'] < 0:
ctx.logger.warn('Invalid order total', {
'total': input_data['total'],
'user_id': input_data['user_id']
})
return {'error': 'Invalid total'}
# Reserve inventory
if ctx.trace:
ctx.trace.add_event('Reserving inventory')
reserved = await iii.call('inventory::reserve', {'items': input_data['items']})
if not reserved.get('success'):
ctx.logger.warn('Inventory reservation failed', {'user_id': input_data['user_id']})
return {'error': 'Out of stock'}
# Process payment
if ctx.trace:
ctx.trace.add_event('Processing payment')
payment = await iii.call('payments::charge', {
'user_id': input_data['user_id'],
'amount': input_data['total']
})
ctx.logger.info('Order created successfully', {
'order_id': payment['order_id'],
'user_id': input_data['user_id'],
'total': input_data['total']
})
return {'success': True, 'order_id': payment['order_id']}
except Exception as error:
if ctx.trace:
ctx.trace.set_status(StatusCode.ERROR, str(error))
ctx.trace.record_exception(error)
ctx.logger.error('Order creation failed', {
'user_id': input_data['user_id'],
'error': str(error),
'stack': getattr(error, '__traceback__', None)
})
raise
iii.register_function('orders::create', create_order)