Docs/telemetryv1.0.0-rc.1

Telemetry — Logging

Overview

The telemetry logger provides structured, context-aware logging that integrates seamlessly with the metrics and tracing pillars. Every log event is emitted as a JSON object that includes the active traceId and spanId from the current TelemetryContext, enabling log-to-trace correlation in any log aggregator.

The default implementation, ConsoleTelemetryLogger, delegates to the framework's underlying logger and serializes events as compact JSON. You can replace it with any implementation of TelemetryLogger.

Key Concepts

  • Category — an optional string label (e.g., 'UserService', 'OrdersWorker') that groups related log lines.
  • Automatic context — traceId and spanId are read from the ambient TelemetryContext at the moment the log method is called. If a span is active, every log line inside that span is correlated.
  • Error normalization — Error objects passed to .error() are decomposed into { name, message, stack, code?, details? } so they serialize safely to JSON.
  • Child loggers — logger.child(context) creates a new logger instance with merged default context. Useful for per-service or per-request loggers.

API Reference

getTelemetryLogger(): TelemetryLogger

Returns the global logger singleton. This is the primary entry point.

import { getTelemetryLogger } from '@tagna/udiot/telemetry';
const logger = getTelemetryLogger();

setTelemetryLogger(logger: TelemetryLogger): TelemetryLogger

Replaces the global logger and returns the previous one.

TelemetryLogger interface

interface TelemetryLogger {
  log(event: TelemetryLogEvent): void | Promise<void>;
  trace(message: string, attributes?: TelemetryAttributes): void;
  debug(message: string, attributes?: TelemetryAttributes): void;
  info(message: string, attributes?: TelemetryAttributes): void;
  warn(message: string, attributes?: TelemetryAttributes): void;
  error(message: string, attributes?: TelemetryAttributes & { error?: Error | TelemetryErrorPayload }): void;
  child(context: Partial<TelemetryContext>): TelemetryLogger;
  flush?(): Promise<void>;
}

All level methods accept a message string and an optional attributes bag. Structured attributes are stored separately from the message — do not interpolate variables into the message string.

Log levels

Method Level string Typical use
.trace() 'trace' Very fine-grained diagnostic events
.debug() 'debug' Debug information for development
.info() 'info' Normal operational events
.warn() 'warn' Unexpected but handled conditions
.error() 'error' Errors that affect the current operation

TelemetryLogEvent

The full event structure used by the low-level .log() method:

Field Type Description
level TelemetryLogLevel One of 'trace', 'debug', 'info', 'warn', 'error', 'fatal'
message string The log message
timestamp string | undefined ISO 8601; defaults to new Date().toISOString()
category string | undefined Logical category or service name
component string | undefined Sub-component within the service
operation string | undefined The operation being performed
durationMs number | undefined Duration of the operation
attributes TelemetryAttributes | undefined Arbitrary key/value data
context Partial<TelemetryContext> | undefined Explicit context override
error TelemetryErrorPayload | Error | undefined Error details

TelemetryLogLevel

type TelemetryLogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';

TelemetryErrorPayload

The normalized error shape produced when an Error instance is passed to .error():

interface TelemetryErrorPayload {
  name?: string;      // error.name (e.g., 'TypeError')
  message: string;    // error.message
  stack?: string;     // error.stack
  code?: string | number; // error.code (if present)
  details?: Record<string, unknown>; // error.details (if present)
}

ConsoleTelemetryLogger

The default implementation. It serializes each event to JSON and writes it through the framework's underlying logger at the appropriate level. 'trace' and 'debug' map to the underlying debug() method; 'error' and 'fatal' map to error().

import { ConsoleTelemetryLogger } from '@tagna/udiot/telemetry';

// Instantiate with a default context (merged into every log event)
const logger = new ConsoleTelemetryLogger({ service: 'payments', environment: 'production' });

bindLoggerContext(partial: Partial<TelemetryContext>): TelemetryLogger

Merges partial into the active TelemetryContext and returns a child logger with the same context applied. Useful for request-scoped loggers.


Basic Usage

Service class with per-category logger

import { getTelemetryLogger, startSpan } from '@tagna/udiot/telemetry';

class UserService {
  private readonly logger = getTelemetryLogger().child({ category: 'UserService' });

  async createUser(email: string): Promise<{ id: string }> {
    return startSpan('UserService.createUser', { kind: 'internal' }, async () => {
      this.logger.info('user.create.start', { email });

      try {
        const user = await db.users.insert({ email });
        this.logger.info('user.create.success', { userId: user.id });
        return user;
      } catch (error) {
        this.logger.error('user.create.failed', {
          email,
          error: error instanceof Error ? error : new Error(String(error)),
        });
        throw error;
      }
    });
  }
}

The resulting JSON log line for the success path looks like:

{
  "level": "info",
  "message": "user.create.success",
  "timestamp": "2026-03-22T10:00:00.000Z",
  "attributes": { "userId": "usr_abc123" },
  "context": {
    "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
    "spanId": "00f067aa0ba902b7",
    "category": "UserService"
  }
}

Logging at multiple levels

const logger = getTelemetryLogger();

logger.trace('cache.lookup.start', { key: 'user:42' });
logger.debug('cache.hit', { key: 'user:42', ttl: 300 });
logger.info('request.received', { method: 'POST', path: '/orders' });
logger.warn('rate.limit.approaching', { current: 95, limit: 100, windowMs: 60000 });
logger.error('payment.failed', {
  orderId: 'ord_xyz',
  error: new Error('Card declined'),
});

Using the low-level .log() method for full control

logger.log({
  level: 'info',
  message: 'order.shipped',
  category: 'OrdersService',
  operation: 'shipOrder',
  durationMs: 142,
  attributes: { orderId: 'ord_xyz', carrier: 'FedEx' },
  context: { requestId: 'req_001' },
});

Integration Notes

  • Automatic trace correlation — the ConsoleTelemetryLogger calls getTelemetryContext() at the time .log() is invoked. If a span created with startSpan() is active on the call stack, traceId and spanId will be present in every log line emitted within the executor.
  • Child loggers — use .child(context) to create a logger that always includes a fixed set of context fields (e.g., { service: 'UserService', tenantId: 'tenant_x' }). Child loggers merge their default context with the ambient context at log time.
  • Custom logger — implement TelemetryLogger and call setTelemetryLogger(myLogger) during initialization. This is useful for routing logs to Winston, Pino, or a remote logging service.

Pitfalls

  • Do not put dynamic or high-cardinality values in the message field. Use attributes instead. Log aggregators index messages for grouping; a unique message per event defeats that.
  • The error field in TelemetryLogEvent is separate from attributes. Pass errors via attributes: { error: myError } when calling the convenience .error() method — the implementation extracts it automatically.
  • flush() is optional on the TelemetryLogger interface. If your custom logger buffers output, implement flush() and call shutdownTelemetry() at process exit to ensure all log lines are drained.
From the canonical UDIOT public documentation.Try it in the playground ↗