Files
cell/docs/logging.md

7.5 KiB

title, description, weight, type
title description weight type
Logging Configurable channel-based logging with sinks 25 docs

Logging in ƿit is channel-based. Any log.X(value) call writes to channel "X". Channels are routed to sinks — named destinations that format and deliver log output to the console or to files.

Channels

These channels are conventional:

Channel Usage
log.console(msg) General output
log.error(msg) Errors
log.warn(msg) Compiler diagnostics and warnings
log.system(msg) Internal system messages
log.build(msg) Per-file compile/link output
log.shop(msg) Package management internals

Any name works. log.debug(msg) creates channel "debug", log.perf(msg) creates "perf", and so on.

log.console("server started on port 8080")
log.error("connection refused")
log.debug({query: "SELECT *", rows: 42})

Non-text values are JSON-encoded automatically.

Default Behavior

With no configuration, a default sink routes console and error to the terminal in clean format. The error channel includes a stack trace by default:

server started on port 8080
error: connection refused
    at handle_request (server.ce:42:3)
    at main (main.ce:5:1)

Clean format prints messages without actor ID or channel prefix. Error messages are prefixed with error:. Other formats (pretty, bare) include actor IDs and are available for debugging. Stack traces are always on for errors unless you explicitly configure a sink without them.

Channels like warn, build, and shop are not routed to the terminal by default. Enable them with pit log enable <channel>.

Configuration

Logging is configured in .cell/log.toml. Each [sink.NAME] section defines a sink.

[sink.terminal]
type = "console"
format = "bare"
channels = ["console"]

[sink.errors]
type = "file"
path = ".cell/logs/errors.jsonl"
channels = ["error"]

[sink.everything]
type = "file"
path = ".cell/logs/all.jsonl"
channels = ["*"]
exclude = ["console"]

Sink fields

Field Values Description
type "console", "file" Where output goes
format "clean", "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

clean — CLI-friendly. No actor ID or channel prefix. Error channel messages are prefixed with error:. This is the default format.

server started on port 8080
error: connection refused

pretty — human-readable, one line per message. Includes actor ID, channel, source location, and message.

[a3f12] [console] main.ce:5 server started

bare — minimal. Actor ID and message only.

[a3f12] server started

json — structured JSONL (one JSON object per line). Used for file sinks and machine consumption.

{"actor_id":"a3f12...","timestamp":1702656000.5,"channel":"console","event":"server started","source":{"file":"main.ce","line":5,"col":3,"fn":"init"}}

Log Records

Every log call produces a record:

{
  actor_id: "a3f12...",       // full actor GUID
  timestamp: 1702656000.5,    // seconds since epoch
  channel: "console",         // channel name
  event: "the message",       // value passed to log
  source: {
    file: "main.ce",
    line: 5,
    col: 3,
    fn: "init"
  }
}

File sinks write one JSON-encoded record per line. Console sinks format the record according to their format setting.

Stack Traces

The error channel captures stack traces by default. To enable stack traces for other channels, add a stack field to a sink — an array of channel names that should include a call stack.

Via the CLI:

pit log add terminal console --channels=console,error,debug --stack=error,debug

Or in log.toml:

[sink.terminal]
type = "console"
format = "bare"
channels = ["console", "error", "debug"]
stack = ["error", "debug"]

Only channels listed in stack get stack traces. Other channels on the same sink print without one:

[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 for channels that have stack capture enabled:

{"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}]}

Channels without stack configuration produce no stack field. Capturing stacks adds overhead — enable it for debugging, not production.

CLI

The pit log command manages sinks and reads log files. See CLI — pit log for the full reference.

pit log list                                           # show sinks
pit log channels                                       # list channels with enabled/disabled status
pit log enable warn                                    # enable a channel on the terminal sink
pit log disable warn                                   # disable a channel
pit log add terminal console --format=clean --channels=console
pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console
pit log add debug console --channels=error,debug --stack=error,debug
pit log remove terminal
pit log read dump --lines=20 --channel=error
pit log tail dump

Channel toggling

The enable and disable commands modify the terminal sink's channel list without touching other sink configuration. This is the easiest way to opt in to extra output:

pit log enable warn      # see compiler warnings
pit log enable build     # see per-file compile/link output
pit log disable warn     # hide warnings again

Examples

Development setup

Route console output to the terminal with minimal formatting. Send everything else to a structured log file for debugging.

[sink.terminal]
type = "console"
format = "bare"
channels = ["console"]

[sink.debug]
type = "file"
path = ".cell/logs/debug.jsonl"
channels = ["*"]
exclude = ["console"]
log.console("listening on :8080")   // -> terminal: [a3f12] listening on :8080
log.error("bad request")            // -> debug.jsonl only
log.debug({latency: 0.042})         // -> debug.jsonl only

Separate error log

Keep a dedicated error log alongside a full dump.

[sink.terminal]
type = "console"
format = "pretty"
channels = ["console", "error", "system"]

[sink.errors]
type = "file"
path = ".cell/logs/errors.jsonl"
channels = ["error"]

[sink.all]
type = "file"
path = ".cell/logs/all.jsonl"
channels = ["*"]

JSON console

Output structured JSON to the console for piping into other tools.

[sink.json_out]
type = "console"
format = "json"
channels = ["console", "error"]
pit run myapp.ce | jq '.event'

Reading logs

# Last 50 error entries
pit log read errors --lines=50

# Errors since a timestamp
pit log read errors --since=1702656000

# Filter a wildcard sink to one channel
pit log read all --channel=debug --lines=10

# Follow a log file in real time
pit log tail all