Problem
An actor that throws in onReceive and is restarted by its supervisor produces zero log output at any level. Not a warning, not a debug line — nothing. A crash-looping actor in production is invisible: the only residual signals are actor_restarted_total (unlabelled, in a registry that is a no-op unless someone called .enable() in code) and an ActorRestarted event on a stream that nothing in src/ subscribes to.
ActorCell.ts contains six log calls across 1272 lines, and an ordinary actor's failure path passes through none of them.
Evidence
The catch hands the cause to the parent and logs nothing:
src/internal/ActorCell.ts:1030-1037
} catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
failure = err;
if (span) {
span.recordException(err);
span.setStatus('error', err.message);
}
this.failToParent(err, message);
failToParent logs only when there is no parent — i.e. only for the root guardian:
src/internal/ActorCell.ts:1107-1119
private failToParent(cause: Error, message?: unknown): void {
this.mailbox.suspend();
if (this.state === 'running') this.state = 'suspended';
for (const child of this._children.values()) child.enqueueSystem({ kind: 'suspend' });
if (this._parent) {
this._parent.enqueueSystem({ kind: 'failure', cause, child: this.self, message });
} else {
// Root guardian failed — log and terminate the system.
this.log.error(`Guardian ${this.path} failed; terminating system`, cause);
onFailure logs only once the restart threshold is exceeded (:1154), and onRecreate (:914) logs nothing. The complete set of log calls in the file is lines 622, 810, 874, 932, 1116, 1154.
Reproduced with a logger injected via ActorSystemOptions at LogLevel.Trace, for three crashes and three restarts:
log lines mentioning the crash: []
total log lines captured: 0
Proposal
Log the failure where it happens. One this.log.error in failToParent carrying the cause, its stack, the actor path and the failing message's type, plus one line in onRecreate naming the directive taken. That is roughly five lines and it converts an invisible crash loop into an alertable one.
Rate-limiting is worth considering for a hot loop, but silence is not the right default — the current behaviour means the first thing an operator learns about a failing actor is a downstream symptom.
Acceptance sketch
Verification status
Found in the ten-lens production-readiness review of 2026-08-05 (v0.13.0) and re-verified before filing: reproduced by execution with a spy logger at trace level; the log-call inventory above is a complete grep of this.log.* in ActorCell.ts.
Part of the production-readiness review batch — tracked in #913.
Problem
An actor that throws in
onReceiveand is restarted by its supervisor produces zero log output at any level. Not a warning, not a debug line — nothing. A crash-looping actor in production is invisible: the only residual signals areactor_restarted_total(unlabelled, in a registry that is a no-op unless someone called.enable()in code) and anActorRestartedevent on a stream that nothing insrc/subscribes to.ActorCell.tscontains six log calls across 1272 lines, and an ordinary actor's failure path passes through none of them.Evidence
The catch hands the cause to the parent and logs nothing:
failToParentlogs only when there is no parent — i.e. only for the root guardian:onFailurelogs only once the restart threshold is exceeded (:1154), andonRecreate(:914) logs nothing. The complete set of log calls in the file is lines 622, 810, 874, 932, 1116, 1154.Reproduced with a logger injected via
ActorSystemOptionsatLogLevel.Trace, for three crashes and three restarts:Proposal
Log the failure where it happens. One
this.log.errorinfailToParentcarrying the cause, its stack, the actor path and the failing message's type, plus one line inonRecreatenaming the directive taken. That is roughly five lines and it converts an invisible crash loop into an alertable one.Rate-limiting is worth considering for a hot loop, but silence is not the right default — the current behaviour means the first thing an operator learns about a failing actor is a downstream symptom.
Acceptance sketch
onReceiveproduces an ERROR line with the cause, the stack, the actor path and the failing message type.Restart/Stop/Resume/Escalate) appears in the log.actor_restarted_totalcarries an actor-class label so the metric can answer "which actor is restarting" (see [Feature] Stock metric actor_mailbox_dropped_total labels by actor path — unbounded cardinality #658/[Security]actor_mailbox_dropped_totalcarries a dynamicpathlabel that under sharding derives from a remote-supplied entity id, so a remote party mints permanent, never-evicted metric series #745 for the cardinality constraint onpath).Verification status
Found in the ten-lens production-readiness review of 2026-08-05 (
v0.13.0) and re-verified before filing: reproduced by execution with a spy logger at trace level; the log-call inventory above is a complete grep ofthis.log.*inActorCell.ts.Part of the production-readiness review batch — tracked in #913.