Logging
Esta página aún no está disponible en tu idioma.
Every actor has a logger at this.log, automatically bound to the
actor’s path:
import { Actor } from 'actor-ts';
class Worker extends Actor<...> { override onReceive(msg): void { this.log.info('handling message'); // → [2025-05-13T11:42:01.123Z] INFO actor-ts://my-app/user/worker - handling message }}Four methods on every logger — debug, info, warn, error:
interface Logger { readonly level: LogLevel; debug(message: string, ...args: unknown[]): void; info(message: string, ...args: unknown[]): void; warn(message: string, ...args: unknown[]): void; error(message: string, ...args: unknown[]): void; withSource(source: string): Logger; withFields(fields: LogContextData): Logger;}Calls below the configured level are no-ops — this.log.debug(...)
on a system configured for info doesn’t even evaluate the
arguments (the level check happens first).
Configuring the level
Section titled “Configuring the level”System-wide via the settings:
const system = ActorSystem.create('my-app', { logLevel: 'debug' });Or via the env / config file (actor-ts.log-level = "debug" in
application.conf). Five levels: debug / info / warn /
error / silent. Default is info.
Structured JSON logs — built-in JsonLogger
Section titled “Structured JSON logs — built-in JsonLogger”For log-aggregation pipelines (Loki, ELK, Datadog, CloudWatch,
Splunk, etc.) you want one JSON object per record, not the
human-readable text the default ConsoleLogger emits.
JsonLogger is the shipped implementation:
import { ActorSystem, JsonLogger } from 'actor-ts';
const system = ActorSystem.create('my-app', { logger: new JsonLogger(),});Output (one \n-terminated JSON object per record):
{"ts":"2026-05-14T12:34:56.789Z","level":"info", "source":"actor-ts://my-app/user/order", "msg":"placing order", "correlationId":"abc-123","userId":"user-42", "args":[{"items":42}]}Every record carries ts (ISO-8601), level, msg, and the
merged static + dynamic MDC (static from withFields, dynamic
from LogContext.run, with dynamic winning on key collision).
Extra positional ...args from log.info(msg, extra1, extra2)
land under an args array — log aggregators index nested keys
automatically.
Error instances serialise as { name, message, stack }
instead of the default "{}" (Error’s enumerable surface is
empty). Circular references, BigInt, and functions are
sanitised so a log call never throws.
JsonLogger’s constructor takes an optional sink — by default
it writes to process.stdout, the right pipe for the Docker
logging driver, the Kubernetes log scraper, vector,
fluent-bit, and jq:
new JsonLogger(LogLevel.Info, '', {}, { write: (line) => process.stderr.write(line), // redirect to stderr});OTLP-Logs pipeline — otelLogger
Section titled “OTLP-Logs pipeline — otelLogger”For an OpenTelemetry Collector wired up via
@opentelemetry/sdk-logs + OTLPLogExporter, bridge through
the OTel Logs API instead of stdout-JSON:
import * as logsApi from '@opentelemetry/api-logs';import { ActorSystem, otelLogger } from 'actor-ts';
// (SDK setup: register a LoggerProvider + OTLP exporter — out of scope here.)
const system = ActorSystem.create('my-app', { logger: otelLogger({ api: logsApi }),});@opentelemetry/api-logs is an optional peer dep — the
framework never imports it, you bring your existing namespace
import. Every this.log.info(...) inside an actor lands as
an OTel LogRecord with severity mapped to OTel’s standard
severity-number range, the actor’s path on source, the
merged MDC on attributes, and the active span’s
traceId/spanId automatically linked when tracing is enabled
in the same process.
Custom logger
Section titled “Custom logger”The Logger interface is small enough to implement directly if
neither built-in fits — e.g. you want a specific binary wire
format, you want to route to multiple sinks via fan-out, or you
need to wrap an existing log library. Same shape as the built-
ins:
import { type Logger, LogLevel, type LogContextData } from 'actor-ts';
class MyLogger implements Logger { level = LogLevel.Info; debug(msg: string, ...args: unknown[]): void { /* ... */ } info(msg: string, ...args: unknown[]): void { /* ... */ } warn(msg: string, ...args: unknown[]): void { /* ... */ } error(msg: string, ...args: unknown[]): void { /* ... */ } withSource(source: string): Logger { /* return a bound copy */ return this; } withFields(fields: LogContextData): Logger { /* return a bound copy */ return this; }}The framework calls withSource once per actor to bind the
actor’s path; you don’t have to do that yourself.
Structured fields via withFields
Section titled “Structured fields via withFields”Static fields — same value on every record emitted by this logger:
class ShardCoordinator extends Actor<...> { private readonly log = this.context.log.withFields({ component: 'shard-coordinator', shardId: this.shardId, });
override onReceive(msg): void { this.log.info('rebalance start'); // → ... shard-coordinator - rebalance start {component=shard-coordinator, shardId=12} }}withFields returns a new logger with the fields stamped on every
emit. Useful for component-level tagging that doesn’t change
across messages.
Dynamic context — LogContext (MDC)
Section titled “Dynamic context — LogContext (MDC)”For fields that vary per request rather than per actor — a
correlation id, a request id, a user id — use the LogContext
MDC. Set it at the entry point; every log call inside reads it
automatically:
import { LogContext } from 'actor-ts';
// HTTP request handler — wraps the actor work in a context scope.app.post('/orders', async (req, res) => { const correlationId = req.headers['x-correlation-id'] ?? randomUUID();
await LogContext.run({ correlationId, userId: req.user.id }, async () => { const result = await orderActor.ask({ kind: 'place', ... }); res.json(result); });});
// Inside any actor reached via `tell` / `ask` from there:class OrderActor extends Actor<...> { override onReceive(msg): void { this.log.info('placing order'); // → ... order-actor - placing order {correlationId=abc-123, userId=user-42} paymentActor.tell({ kind: 'charge', ... }); }}LogContext is backed by AsyncLocalStorage — the context
propagates across awaits, tells, and cluster hops. Three
operations:
| Method | What it does |
|---|---|
LogContext.run(ctx, fn) | Runs fn with ctx as the current context. |
LogContext.with(extra, fn) | Runs fn with { ...current, ...extra } as the context. |
LogContext.get() | Reads the current context (empty object if none active). |
Static fields (withFields) and dynamic MDC merge at emit
time; dynamic wins on key collision (innermost-scope-wins
intuition).
How MDC propagates across tell
Section titled “How MDC propagates across tell”When you call ref.tell(msg) inside a LogContext.run scope, the
runtime snapshots the current context onto the envelope. The
receiving actor’s onReceive runs under a fresh LogContext.run
of that snapshot. This means:
- A single
correlationIdflows through every actor reached from the entry point. - Across cluster nodes, the snapshot rides on the wire envelope —
the receiving node restores it before invoking
onReceive. - The
correlationIdshows up in every log line in the trail, letting your aggregator stitch a multi-actor, multi-node request into one searchable thread.
Structured logging — withFields + MDC together
Section titled “Structured logging — withFields + MDC together”class PerSessionWorker extends Actor<...> { private readonly log = this.context.log.withFields({ sessionId: this.id });
override async onReceive(msg): Promise<void> { await LogContext.run({ requestId: msg.requestId }, async () => { this.log.info('handling'); // → ... per-session-worker - handling {sessionId=abc, requestId=xyz} }); }}sessionId is static (the actor’s identity), requestId is
per-message dynamic. Both end up in the structured suffix; the
log aggregator gets a query-friendly record.
Where to next
Section titled “Where to next”- Actor —
this.logis part of the context every actor has. - Actor system — the
logger/logLevelsettings. - Tracing —
span-id propagation built on the same
AsyncLocalStorageprimitive. - Observability — Metrics — the metrics surface, separate from logs but conceptually adjacent.
The Logger and
LogContext API references
cover the full surface.