Files
cell/docs/logging.md
2026-02-18 12:05:05 -06:00

5.7 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

Three channels are conventional:

Channel Usage
log.console(msg) General output
log.error(msg) Errors and warnings
log.system(msg) Internal system messages

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, error, and system to the terminal in pretty format:

[a3f12] [console] main.ce:5 server started on port 8080
[a3f12] [error] main.ce:12 connection refused

The format is [actor_id] [channel] file:line message.

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 "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 "*")
path file path Output file (file sinks only)

Formats

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

Set stack = true at the top level of .cell/log.toml to capture a full call stack with every log record.

stack = true

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

With pretty format, the stack is printed indented below each message:

[a3f12] [error] server.ce:42 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:

{"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.

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 add terminal console --format=bare --channels=console
pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console
pit log remove terminal
pit log read dump --lines=20 --channel=error
pit log tail dump

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