> ## 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.

# Context-Aware Logging

> Access the Logger from getContext() to emit logs with automatic trace correlation

## 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

<CodeGroup>
  ```typescript Node.js theme={null}
  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 }
  })
  ```

  ```python Python theme={null}
  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)
  ```
</CodeGroup>

## Logger API

### Methods

<CodeGroup>
  ```typescript Node.js theme={null}
  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
  }
  ```

  ```python Python theme={null}
  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: ...
  ```
</CodeGroup>

### 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:

<CodeGroup>
  ```typescript Node.js theme={null}
  // 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)
    }
  }
  ```

  ```python Python theme={null}
  # 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)
  }
  ```
</CodeGroup>

## Context Propagation

<Steps>
  <Step title="Logger is created per function invocation">
    Each function call gets a unique Logger instance with its own trace context:

    <CodeGroup>
      ```typescript Node.js theme={null}
      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 }
      })
      ```

      ```python Python theme={null}
      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)
      ```
    </CodeGroup>
  </Step>

  <Step title="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!
    ```
  </Step>
</Steps>

## Severity Levels

<CodeGroup>
  ```typescript Node.js theme={null}
  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
  ```

  ```python Python theme={null}
  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
  ```
</CodeGroup>

## Structured Logging

<Tip>
  Always pass structured data (objects) instead of interpolating strings:
</Tip>

<CodeGroup>
  ```typescript Node.js theme={null}
  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 }
  })
  ```

  ```python Python theme={null}
  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)
  ```
</CodeGroup>

## Fallback Behavior

<Note>
  When OpenTelemetry is **disabled** or not initialized, the Logger falls back to standard console/logging:
</Note>

<CodeGroup>
  ```typescript Node.js theme={null}
  // 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 })
  })
  ```

  ```python Python theme={null}
  # 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)
  ```
</CodeGroup>

## Accessing the Active Span

<CodeGroup>
  ```typescript Node.js theme={null}
  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
    }
  })
  ```

  ```python Python theme={null}
  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)
  ```
</CodeGroup>

## Consuming Logs

<CodeGroup>
  ```typescript Node.js theme={null}
  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()
  ```

  ```python Python theme={null}
  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()
  ```
</CodeGroup>

## Best Practices

<Tip>
  **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)
</Tip>

### Example: Full Context Usage

<CodeGroup>
  ```typescript Node.js theme={null}
  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
    }
  })
  ```

  ```python Python theme={null}
  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)
  ```
</CodeGroup>
