---
title: "Logging"
description: "Configurable channel-based logging with sinks"
weight: 25
type: "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.

```javascript
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.

```toml
[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 `"*"`) |
| `stack` | array of channel names | Channels that capture a stack trace |
| `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.

```json
{"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:

```javascript
{
  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

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
[sink.terminal]
type = "console"
format = "bare"
channels = ["console", "error"]
stack = ["error"]
```

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:

```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}]}
```

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](/docs/cli/#pit-log) for the full reference.

```bash
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.

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

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

```javascript
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.

```toml
[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.

```toml
[sink.json_out]
type = "console"
format = "json"
channels = ["console", "error"]
```

```bash
pit run myapp.ce | jq '.event'
```

### Reading logs

```bash
# 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
```
