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