Logging
Jeder Actor hat einen Logger unter this.log, automatisch an den
Pfad des Actors gebunden:
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 }}Vier Methoden auf jedem 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;}Aufrufe unter dem konfigurierten level sind No-Ops —
this.log.debug(...) auf einem System, das für info konfiguriert
ist, evaluiert nicht einmal die Argumente (der Level-Check passiert
zuerst).
Level konfigurieren
Abschnitt betitelt „Level konfigurieren“Systemweit über die Settings:
const system = ActorSystem.create('my-app', { logLevel: 'debug' });Oder über die Env- / Config-Datei (actor-ts.log-level = "debug" in
application.conf). Fünf Level: debug / info / warn /
error / silent. Default ist info.
Strukturierte JSON-Logs — eingebauter JsonLogger
Abschnitt betitelt „Strukturierte JSON-Logs — eingebauter JsonLogger“Für Log-Aggregator-Pipelines (Loki, ELK, Datadog, CloudWatch,
Splunk etc.) willst du ein JSON-Objekt pro Record, nicht den
menschenlesbaren Text, den der Default-ConsoleLogger ausgibt.
JsonLogger ist die mitgelieferte Implementierung:
import { ActorSystem, JsonLogger } from 'actor-ts';
const system = ActorSystem.create('my-app', { logger: new JsonLogger(),});Output (ein \n-terminiertes JSON-Objekt pro 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}]}Jeder Record trägt ts (ISO-8601), level, msg und die
gemergten statischen + dynamischen MDC-Felder (statisch aus
withFields, dynamisch aus LogContext.run, dynamisch gewinnt
bei Key-Kollision). Zusätzliche positionale ...args aus
log.info(msg, extra1, extra2) landen unter einem args-Array
— Log-Aggregators indexieren verschachtelte Keys automatisch.
Error-Instanzen werden als { name, message, stack }
serialisiert statt als Default-"{}" (Errors enumerierbare
Surface ist leer). Zyklische Referenzen, BigInt und Funktionen
werden sanitiert, damit ein Log-Call nie wirft.
Der Konstruktor von JsonLogger nimmt optional einen Sink —
default schreibt er nach process.stdout, der richtigen Pipe für
den Docker-Logging-Driver, den Kubernetes-Log-Scraper, vector,
fluent-bit und jq:
new JsonLogger(LogLevel.Info, '', {}, { write: (line) => process.stderr.write(line), // umleitung nach stderr});OTLP-Logs-Pipeline — otelLogger
Abschnitt betitelt „OTLP-Logs-Pipeline — otelLogger“Für einen OpenTelemetry-Collector, der über
@opentelemetry/sdk-logs + OTLPLogExporter angebunden ist,
brückst du über die OTel-Logs-API statt stdout-JSON:
import * as logsApi from '@opentelemetry/api-logs';import { ActorSystem, otelLogger } from 'actor-ts';
// (SDK-Setup: LoggerProvider + OTLP-Exporter registrieren — außerhalb des Scope.)
const system = ActorSystem.create('my-app', { logger: otelLogger({ api: logsApi }),});@opentelemetry/api-logs ist eine optionale Peer-Dep — das
Framework importiert sie nie, du bringst deinen vorhandenen
Namespace-Import mit. Jedes this.log.info(...) in einem Actor
landet als OTel-LogRecord mit Severity gemappt auf OTels
Standard-Severity-Number-Bereich, dem Actor-Pfad auf source,
den gemergten MDC-Feldern auf attributes und der traceId/
spanId des aktiven Spans automatisch verlinkt, wenn Tracing
im selben Prozess aktiv ist.
Eigener Logger
Abschnitt betitelt „Eigener Logger“Das Logger-Interface ist klein genug, um es direkt zu
implementieren, wenn keiner der eingebauten passt — z. B. willst
du ein spezifisches Binär-Wireformat, willst per Fanout auf
mehrere Sinks routen, oder eine bestehende Log-Library wrappen.
Gleiche Shape wie die eingebauten:
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 { /* gib eine gebundene Kopie zurück */ return this; } withFields(fields: LogContextData): Logger { /* gib eine gebundene Kopie zurück */ return this; }}Das Framework ruft withSource einmal pro Actor auf, um den Pfad
des Actors zu binden; das musst du nicht selbst tun.
Strukturierte Felder via withFields
Abschnitt betitelt „Strukturierte Felder via withFields“Statische Felder — derselbe Wert auf jedem Record, der von diesem Logger emittiert wird:
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 gibt einen neuen Logger zurück, der die Felder auf
jedem Emit eingestempelt hat. Nützlich für Component-Level-Tagging,
das sich über Nachrichten nicht ändert.
Dynamischer Kontext — LogContext (MDC)
Abschnitt betitelt „Dynamischer Kontext — LogContext (MDC)“Für Felder, die pro Request statt pro Actor variieren — eine
Correlation-ID, eine Request-ID, eine User-ID — verwende die
LogContext-MDC. Setze sie am Einstiegspunkt; jeder Log-Aufruf
darin liest sie automatisch:
import { LogContext } from 'actor-ts';
// HTTP-Request-Handler — wickelt die Actor-Arbeit in einen Kontext-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); });});
// Innerhalb jedes Actors, der via `tell` / `ask` von dort erreicht wird: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 ist mit AsyncLocalStorage unterlegt — der Kontext
propagiert über awaits, tells und Cluster-Hops hinweg. Drei
Operationen:
| Methode | Was sie tut |
|---|---|
LogContext.run(ctx, fn) | Führt fn mit ctx als aktuellem Kontext aus. |
LogContext.with(extra, fn) | Führt fn mit { ...current, ...extra } als Kontext aus. |
LogContext.get() | Liest den aktuellen Kontext (leeres Objekt, wenn keiner aktiv). |
Statische Felder (withFields) und dynamische MDC mergen zur
Emit-Zeit; dynamisch gewinnt bei Schlüssel-Kollision
(innermost-scope-wins-Intuition).
Wie MDC über tell propagiert
Abschnitt betitelt „Wie MDC über tell propagiert“Wenn du ref.tell(msg) innerhalb eines LogContext.run-Scopes
aufrufst, snapshotet die Runtime den aktuellen Kontext auf das
Envelope. Das onReceive des empfangenden Actors läuft unter
einem frischen LogContext.run dieses Snapshots. Das bedeutet:
- Eine einzige
correlationIdfließt durch jeden Actor, der vom Einstiegspunkt aus erreicht wird. - Über Cluster-Nodes hinweg reitet der Snapshot auf dem
Wire-Envelope — der empfangende Node stellt ihn wieder her, bevor
er
onReceiveaufruft. - Die
correlationIdtaucht in jeder Log-Zeile der Spur auf, damit dein Aggregator einen Multi-Actor-, Multi-Node-Request zu einem durchsuchbaren Thread zusammenfügen kann.
Strukturiertes Logging — withFields + MDC zusammen
Abschnitt betitelt „Strukturiertes Logging — withFields + MDC zusammen“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 ist statisch (die Identität des Actors), requestId
ist per-Message-dynamisch. Beide landen im strukturierten Suffix;
der Log-Aggregator bekommt einen abfragefreundlichen Record.
Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Actor —
this.logist Teil des Contexts, den jeder Actor hat. - Actor-System — die
logger/logLevel-Settings. - Tracing —
Span-ID-Propagation, gebaut auf demselben
AsyncLocalStorage-Primitive. - Observability — Metriken — die Metriken-Schnittstelle, separat von Logs, aber konzeptionell benachbart.
Die Logger- und
LogContext-API-Referenzen decken die
volle Schnittstelle ab.