?

What rousseau emits

Every daemon uses the Go standard library log/slog package. Choose between two handlers via log.format:

Value Handler Use case
text (default) slog.NewTextHandler Interactive rousseau chat. Colours off; grep-friendly.
json slog.NewJSONHandler Any daemon in production. Every field is a JSON key.

Levels: debug, info, warn, error.

Production config:

log:
  level: info
  format: json

Structured keys you can rely on

The following keys are load-bearing — parse them, don't rewrite them. They appear across internal/cli/ and internal/agent/:

Key Emitted from Fields Meaning
tool.execute agent.runTools name, id A tool call ran.
tool.denied agent.runTools name, reason Approver blocked the call.
tool.error agent.runTools name, err Tool ran but returned an error.
agent.compressed agent.Turn messages Session compression fired.
agent.compress_failed agent.Turn err Compression provider errored; loop continued.
whatsapp.starting cli/whatsapp.go store, allowlist WhatsApp bridge booted.
whatsapp.voice_enabled cli/whatsapp.go binary, model Voice transcription active.
cron.fire internal/cron/scheduler.go name, job Cron job fired.
cron.deliver internal/cron/scheduler.go name, target, bytes Cron reply delivered.

Every log line carries the standard time, level, msg slog fields plus any attributes above.

Log pipelines — pick your stack

Loki + Promtail + Grafana. See the systemd + Promtail config below the tabs. Query with LogQL:

sum by (level) (rate({job="rousseau-agent"} [5m]))

Alerts on approval denials:

count_over_time({job="rousseau-agent"} |= "tool.denied" [15m]) > 5

Log pipeline: Loki + Grafana

Systemd + Promtail

Point Promtail at the rousseau service's journal:

# /etc/promtail/promtail.yaml
scrape_configs:
  - job_name: rousseau-agent
    journal:
      matches: _SYSTEMD_USER_UNIT=rousseau-agent.service
      labels: { job: rousseau-agent }
    relabel_configs:
      - source_labels: [__journal__systemd_user_unit]
        target_label: unit
    pipeline_stages:
      - json:
          expressions: { level: level, msg: msg }
      - labels: { level: "" }

Grafana dashboards can then filter on level=WARN and msg="tool.denied" to build the "blocked tool calls" panel.

Kubernetes

Deploy the Grafana Agent (or Loki + Alloy) as a DaemonSet. Because rousseau writes to stdout in the container, no file scraping is required.

Log pipeline: Datadog

# /etc/datadog-agent/conf.d/rousseau.d/conf.yaml
logs:
  - type: journald
    include_units:
      - rousseau-agent.service
    service: rousseau-agent
    source: rousseau-agent

Because rousseau emits JSON, Datadog's built-in JSON parser lifts level, msg, and every attribute into first-class facets. Configure a monitor on msg:tool.denied for approval-policy alerts.

Log pipeline: Vector

# /etc/vector/vector.toml
[sources.rousseau_journal]
type = "journald"
include_units = ["rousseau-agent.service"]

[transforms.rousseau_parse]
type = "remap"
inputs = ["rousseau_journal"]
source = '''
. = merge(., parse_json(.message) ?? {})
'''

[sinks.loki]
type = "loki"
inputs = ["rousseau_parse"]
endpoint = "https://loki.internal:3100"
labels = { job = "rousseau-agent", level = "{{ level }}" }

Key metrics to graph

There is no Prometheus endpoint today. The metrics you want ride on the log stream:

Metric How to derive
Tool-call rate count msg:tool.execute
Denial rate count msg:tool.denied
Error rate count msg:tool.error
Compression events count msg:agent.compressed
Cron fires count msg:cron.fire
Cron delivery bytes sum bytes where msg:cron.deliver

Loki + LogQL: sum by (name) (count_over_time({job="rousseau-agent"} |= "tool.denied" [1h])).

OpenTelemetry roadmap

An OpenTelemetry integration is on the roadmap. When it ships, expect:

  • otel.trace context propagation through the agent loop (one span per Turn, child spans per tool call).
  • Metric exporter for the same counters that today ride on logs.
  • Configurable OTLP endpoint via env vars.

Until then, treat the structured slog output as the observability substrate. Every event you would want a metric or a trace for is already there — the metadata is complete, only the wire format is different.

Debugging without a log pipeline

Interactive:

rousseau --config /etc/rousseau/config.yaml whatsapp \
  --allow 447900123456@s.whatsapp.net 2>&1 | jq

The daemon writes slog to stderr; piping through jq gives an interactive filter. jq 'select(.msg == "tool.denied")' shows every blocked call.

rousseau doctor is the other observability lever — a snapshot of every dependency and every config choice at a moment in time.

Troubleshooting

journal has no entries

The daemon wrote nothing yet, or the journald matcher is wrong. Confirm with journalctl --user -u rousseau-agent.service --no-pager.

JSON parsing errors in the pipeline

Rousseau logs one line per event. If a log event's msg contains a newline (rare — some transports include multi-line error strings), the pipeline may split it into two events. Filter with a regex or use structured parsing that respects embedded newlines.

Missing attributes downstream

Loki drops attributes it cannot map to labels. Use line_format in LogQL to project attributes into the rendered output, or index them as labels with pipeline_stages.labels.

Datadog service tag is missing

Datadog uses the service field for filtering. The journald source sets it from the config; ensure service: rousseau-agent is present.

Grafana dashboards show no data

Verify the LogQL query matches your labels. Promtail's default job label is set by the scrape config — if you changed it, update every dashboard query.

Related pages

Further reading

  • internal/cli/root.gonewLogger sets the slog handler.
  • internal/agent/agent.gotool.execute, tool.denied, agent.compressed events.
  • internal/transport/whatsapp/dispatch.go — transport-side event emission.
  • Grafana LogQL docs and Datadog log processing docs (external).

Type to search 150+ pages. Ranking: BM25 with title/description boost.