-
released this
2026-08-02 20:52:43 -05:00 | 220 commits to main since this releasestack_tracewas the last of the five keys Portainer's log viewer colours to go
unused — and the one that mattered on the worst night.What an error used to look like
err={"type":"Error","message":"op.gg returned HTTP 403","stack":"Error: op.gg returned HTTP 403\n at crawlCounters (/app/dist/src/counters.js:316:11)\n at async runOpggWindow (/app/dist/src/index.js:466:7)"}Three separate problems, and the dashboard could not help with any of them.
erris not one of the five keys it knows, so the whole error was demoted to a
trailingkey=valueat the end of the line. That renderer flattens a nested
object to JSON. And the stack inside it was a single string with its newlines
escaped, so the frames printed as literal\non one unbroken line — the least
readable form of the most useful field.What it looks like now
counters/classic > counters crawl or publish failed queue=classic error=OpggHttpError: op.gg returned HTTP 403 for https://op.gg/…/counters/top at crawlCounters (/app/dist/src/counters.js:316) at runOpggWindow (/app/dist/src/index.js:466)erroris rendered red —formatKeyValuePairspecial-cases exactly the key
nameserrorandERR— andstack_tracegets the dedicated renderer that
prints one indentedat func (source:line)per frame, which is the shape a
stack was always meant to have.The class name is kept when it adds something.
OpggHttpErrorversus
RiotApiErrorsays which half of the window failed before the message has to,
andCountersCrawlAbortedsays the crawl stopped itself rather than broke. A
bareErrorsays only that something threw, so it is left off.No call site changed
The transformation is a
formatters.loghook, which runs before pino's own
error serialiser and therefore still sees theErrorinstance rather than the
{type, message, stack}object it would otherwise become. That ordering is the
whole opportunity.Being a property of the logger rather than of fifteen call sites also means a
newlogger.error({ err }, …)gets this for free instead of silently
regressing. The six call sites that pass a pre-stringifiederr.messagerather
than the error get the rederrorkey too, without being touched — they simply
have no stack to give.Details
- Stacks are capped at 12 frames. An async throw in Node carries twenty-odd,
most of themnode:internal/…plumbing below the first line of this codebase,
and Portainer prints every frame it is given. A cap rather than a filter on
node:internal/: the interesting frame is occasionally in there, and a filter
that hides the cause is worse than a list that is slightly too long. - Frame parsing is anchored on the trailing numbers, because a source is
routinelyC:\…\index.js:527:5orfile:///C:/…/index.js:527:5— both carry
colons of their own, and splitting on the first takes the drive letter for the
path. - A wrapped error's
causeis reported beside it.OpggParseErrortakes
one, and without this the line named the wrapper and lost what it wrapped. - A thrown non-Error carries its contents rather than
[object Object].
throwaccepts anything, and a line that reports a failure and then spends
its one field saying nothing is barely better than no line at all.
Minor rather than patch for the changed log output. No artifact, schema, or
published file changes. This completes the Portainer work: all five keys the
dashboard renders —time,level,caller,message,stack_trace— are
now used.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Stacks are capped at 12 frames. An async throw in Node carries twenty-odd,