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 —
traceIdandspanIdare read from the ambientTelemetryContextat the moment the log method is called. If a span is active, every log line inside that span is correlated. - Error normalization —
Errorobjects 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
ConsoleTelemetryLoggercallsgetTelemetryContext()at the time.log()is invoked. If a span created withstartSpan()is active on the call stack,traceIdandspanIdwill 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
TelemetryLoggerand callsetTelemetryLogger(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
messagefield. Useattributesinstead. Log aggregators index messages for grouping; a unique message per event defeats that. - The
errorfield inTelemetryLogEventis separate fromattributes. Pass errors viaattributes: { error: myError }when calling the convenience.error()method — the implementation extracts it automatically. flush()is optional on theTelemetryLoggerinterface. If your custom logger buffers output, implementflush()and callshutdownTelemetry()at process exit to ensure all log lines are drained.