From a1d1e721b6df0ee7f0c0dfd3a552351fb5f90630 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Wed, 18 Feb 2026 12:22:33 -0600 Subject: [PATCH] stack trace in logging toml --- docs/logging.md | 17 +++++++++-------- internal/engine.cm | 25 ++++++++++++++++++++----- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/docs/logging.md b/docs/logging.md index 3d4e8ec9..2aea9e42 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -68,6 +68,7 @@ exclude = ["console"] | `format` | `"pretty"`, `"bare"`, `"json"` | How output is formatted | | `channels` | array of names, or `["*"]` | Which channels this sink receives. Quote `'*'` on the CLI to prevent shell glob expansion. | | `exclude` | array of names | Channels to skip (useful with `"*"`) | +| `stack` | array of channel names | Channels that capture a stack trace | | `path` | file path | Output file (file sinks only) | ### Formats @@ -113,33 +114,33 @@ File sinks write one JSON-encoded record per line. Console sinks format the reco ## Stack Traces -Set `stack = true` at the top level of `.cell/log.toml` to capture a full call stack with every log record. +Add a `stack` field to a sink to capture a full call stack for specific channels. The value is an array of channel names. ```toml -stack = true - [sink.terminal] type = "console" -format = "pretty" +format = "bare" channels = ["console", "error"] +stack = ["error"] ``` -With pretty format, the stack is printed indented below each message: +Only channels listed in `stack` get stack traces. Other channels on the same sink print without one: ``` -[a3f12] [error] server.ce:42 connection failed +[a3f12] server started +[a3f12] connection failed at handle_request (server.ce:42:3) at process (router.ce:18:5) at main (main.ce:5:1) ``` -With JSON format, a `stack` array is added to the record: +With JSON format, a `stack` array is added to the record for channels that have stack capture enabled: ```json {"actor_id":"a3f12...","channel":"error","event":"connection failed","source":{"file":"server.ce","line":42,"col":3,"fn":"handle_request"},"stack":[{"fn":"handle_request","file":"server.ce","line":42,"col":3},{"fn":"process","file":"router.ce","line":18,"col":5},{"fn":"main","file":"main.ce","line":5,"col":1}]} ``` -When `stack` is not set or `false`, no stack is captured and the `stack` field is omitted from records. Capturing stacks adds overhead — enable it for debugging, not production. +Channels without `stack` configuration produce no stack field. Capturing stacks adds overhead — enable it for debugging, not production. ## CLI diff --git a/internal/engine.cm b/internal/engine.cm index 2ef5b826..8dcf5792 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -324,7 +324,7 @@ var log_config = null var channel_sinks = {} var wildcard_sinks = [] var warned_channels = {} -var log_stack_enabled = false +var stack_channels = {} function log(name, args) { var msg = args[0] @@ -438,6 +438,7 @@ function ensure_log_dir(path) { function build_sink_routing() { channel_sinks = {} wildcard_sinks = [] + stack_channels = {} var names = array(log_config.sink) arrfor(names, function(name) { var sink = log_config.sink[name] @@ -445,7 +446,12 @@ function build_sink_routing() { if (!is_array(sink.channels)) sink.channels = [] if (is_text(sink.exclude)) sink.exclude = [sink.exclude] if (!is_array(sink.exclude)) sink.exclude = [] + if (is_text(sink.stack)) sink.stack = [sink.stack] + if (!is_array(sink.stack)) sink.stack = [] if (sink.type == "file" && sink.path) ensure_log_dir(sink.path) + arrfor(sink.stack, function(ch) { + stack_channels[ch] = true + }) arrfor(sink.channels, function(ch) { if (ch == "*") { wildcard_sinks[] = sink @@ -471,13 +477,13 @@ function load_log_config() { terminal: { type: "console", format: "pretty", - channels: ["console", "error", "system"] + channels: ["console", "error", "system"], + stack: ["error"] } } } } build_sink_routing() - log_stack_enabled = log_config.stack == true } function pretty_format(rec) { @@ -503,7 +509,16 @@ function pretty_format(rec) { function bare_format(rec) { var aid = text(rec.actor_id, 0, 5) var ev = is_text(rec.event) ? rec.event : json.encode(rec.event, false) - return `[${aid}] ${ev}\n` + var out = `[${aid}] ${ev}\n` + var i = 0 + var fr = null + if (rec.stack && length(rec.stack) > 0) { + for (i = 0; i < length(rec.stack); i = i + 1) { + fr = rec.stack[i] + out = out + ` at ${fr.fn} (${fr.file}:${text(fr.line)}:${text(fr.col)})\n` + } + } + return out } function sink_excluded(sink, channel) { @@ -549,7 +564,7 @@ log = function(name, args) { } caller = caller_info(2) - if (log_stack_enabled) stack = os.stack(1) + if (stack_channels[name]) stack = os.stack(1) rec = { actor_id: _cell.id, timestamp: time.number(),