Skip to main content

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

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 }
})
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

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
  }
})

Build docs developers (and LLMs) love