Observability
dotagent ships with structured JSON logs, daily rotation, and opt-in OpenTelemetry export from day zero. No need to bolt on a logging stack later — pick what you want to plug into and go.
TL;DR
There's nothing to configure. Install dotagent, start the daemon, and you immediately get:
Daemon stderr → captured by launchd / systemd (compact human-friendly text)
Daemon structured logs →
~/.config/dotagent/logs/daemon/dotagent.log(JSON, rotated daily)Each agent's stdout+stderr →
~/.config/dotagent/logs/agents/<name>/<name>.log(raw, rotated daily)Rotated files older than 1 day → gzipped automatically
Files older than 30 days (daemon) / 14 days (agents) → deleted automatically
Audit log →
~/.config/dotagent/audit.log(append-only, hash-chained; rotates at 32MB into segments that are never deleted)
The config.toml file is entirely optional. Create one only if you want to:
Change verbosity / retention horizons
Enable OpenTelemetry export to a remote backend
Override defaults for your environment
This guide covers:
What gets logged — daemon, agents, plugins, audit
Where things live — log directory layout
Log format — JSON schema with examples
Rotation and retention — defaults + customizing
Inspecting locally —
dotagent logs,jqqueriesOpenTelemetry export — opt-in, two-line setup
Audit log vs operational log — different purpose, different rules
What gets logged
dotagent has four distinct log streams:
logs/daemon/dotagent.log
daemon's tracing
JSON (one line per record)
daily, gzip → delete
Structured operational logs for debugging
logs/agents/<name>/<name>.log
agent process stdout+stderr
Raw text
daily, gzip → delete
What the agent script actually said
stderr
daemon's tracing
Compact text
only installed when stderr is a TTY
Human-friendly stream for an interactive run
audit.log
daemon
Newline-delimited JSON, hash-chained
32MB → sealed segment, never deleted
Tamper-evident security event ledger
OpenTelemetry (when enabled) re-emits the same structured records as OTLP spans, so any backend gets the full picture.
stderr is conditional, on purpose
Run dotagent daemon in a terminal and you get the compact stream. Run it under launchd or systemd and you do not: there stderr is an appended plain file (logs/daemon/run.avelino.dotagent-error.log) that no rotation policy covers, so mirroring into it would duplicate an already-rotated log into one that grows forever.
That leaves the stderr file as a crash channel — panics, and startup failures that happen before logging is up. An empty …-error.log under a service manager is the healthy state. To follow what the daemon is doing, read logs/daemon/dotagent.log.
DOTAGENT_LOG_STDERR=1 forces the mirror on (units rewired to journald, which does rotate); DOTAGENT_LOG_STDERR=0 forces it off. ANSI colour follows the same TTY rule and additionally honours NO_COLOR, in the daemon and in every subcommand.
Filesystem layout
Everything under $DOTAGENT_HOME (default ~/.config/dotagent):
Log format
Daemon records are JSON with tracing's standard envelope:
Key field conventions used across the codebase:
agent
run/dispatch/plugin records
agent name (matches agent.toml agent.name)
schedule
run/dispatch records
schedule id (matches [[schedules]].id)
attempt
retry records
1-based attempt number
exit_code
post-run records
agent's exit code (124 = timeout)
duration_seconds
post-run records
wall-clock duration
plugin
plugin / notifier invocations
plugin short name (sink-roam) or notifier:<driver> for built-in notifiers (notifier:desktop, notifier:imessage, ...)
event
plugin / notifier invocations
attempt_failed/given_up/recovered/...
Agent log files (logs/agents/<name>/<name>.log) are NOT JSON — they're the raw stdout+stderr the agent script produced, prefixed with a header per run:
Mixing structured (daemon) + raw (agent) is deliberate: the daemon's metadata is uniform across all runs, the agent's output is whatever shape its author chose.
Rotation and retention
You don't need to configure anything. Out of the box, dotagent ships these defaults:
level
info
tracing filter (off, error, warn, info, debug, trace)
format
compact
stderr format. Files are always JSON regardless.
retention_days
30
daemon logs older than this are deleted
per_agent_retention_days
14
agent logs (typically noisier; shorter horizon)
compress_after_days
1
rotated files older than this are gzipped
window_retention_days
30
state/windows/ files older than this are deleted ([state], not [logging])
If those match what you want, don't create a config.toml — the defaults kick in automatically.
Customizing
Override any subset by writing ~/.config/dotagent/config.toml. Missing fields keep their defaults — the file is purely partial overrides.
Apply with dotagent reload (sends SIGHUP; daemon re-reads the file on the next tick).
You can also override the level transiently via env var (overrides the config file):
How rotation works
tracing-appender rolls files daily — at midnight local time, the current file gets renamed to <name>.log.YYYY-MM-DD and a fresh <name>.log is opened.
How retention sweeps work
The daemon runs an internal cleanup pass once per day at 03:00 local time. It:
Walks
logs/daemon/and everylogs/agents/<name>/directory.Rotated files older than
compress_after_daysget gzipped in-place.Files older than
retention_days(orper_agent_retention_daysinside agent dirs) get deleted — rotated files only. The active (non-rotated) log is skipped no matter how old it is: launchd and systemd hold an open fd on it, and unlinking a file someone is still writing to strands every subsequent line in an unnamed inode. A long-lived daemon with a quiet log would otherwise have its live file deleted out from under it at day 31.Walks
state/windows/and deletes windows older thanwindow_retention_days, each together with its.lock. Windows are never gzipped — the daemon reads them as JSON, so a compressed window is a corrupted one. A window whose lock is currently held is skipped.
The audit log is never swept. It rotates by size, but its segments are not subject to [logs] retention and nothing deletes them — see Audit log vs operational log.
To trigger a sweep manually:
Inspecting locally
Tail the daemon log
Or via the launchd-captured stderr (compact, human format):
Tail an agent's output
Query the daemon log with jq
Health snapshot
OpenTelemetry export
Disabled by default. Out of the box, dotagent writes everything to local files (stdout/stderr captured by launchd/systemd + the JSON file described above). Nothing leaves your machine. That's intentional — zero-config, zero network egress.
Turn it on by writing just two lines of config.toml:
Authentication uses the standard OTel env var so dotagent doesn't need vendor-specific config:
That's it — every span the daemon emits (tick, agent_run, plugin_invoke) shows up in your backend.
Fuller config (all fields optional)
Vendor recipes
Honeycomb
EU region: use api.eu1.honeycomb.io:443.
Grafana Tempo (self-hosted or Grafana Cloud)
Grafana Cloud auth:
Self-hosted Tempo without auth:
Jaeger (local)
Open http://localhost:16686, pick dotagent from the service list.
Datadog
Datadog wants the OTLP receiver enabled on its agent:
What gets exported
Today the OTel pipeline exports spans:
daemon— root span for the daemon process lifetimetick— one per scheduler tickagent_run— one per agent invocationplugin_invoke— one per plugin call (preflight / on_success / on_failure)
Logs are NOT yet OTLP-exported (the tracing → OTLP logs bridge is on the roadmap). For now, ship logs via your filesystem agent (fluent-bit, vector, promtail) reading the JSON file.
Audit log vs operational log
These look similar but serve different purposes — do not conflate them.
Path
~/.config/dotagent/audit.log
~/.config/dotagent/logs/
Format
NDJSON, hash-chained (prev_hash)
NDJSON (tracing style)
Producer
daemon, on consequential events
daemon's tracing macros
Frequency
sparse (5–50 entries/day)
dense (thousands/day)
Rotation
by size (32MB), across a hash seam
daily, gzip, then delete
Retention
indefinite — segments are never auto-deleted
configurable (default 30/14 days)
Mutability
append-only; tamper detectable
overwritten on rotation
Schema
strict (typed AuditEvent variants)
unstructured (tracing Fields)
Use case
forensics, security audit
debugging, tail-the-app
Rotation is the one thing the two now share, and they share only the word. An operational log rolls and is eventually deleted; the audit log rolls into a sealed segment stitched to the new file by a hash seam, and dotagent never removes it. Pruning old segments is supported and stays legible to verification — the reasoning is in security/threat-model.md.
You should grep the audit log when answering "was this run authorized? who changed manifest X? when did the daemon last say a plugin was phantom?". You should jq the operational log when answering "why did the last 5 ticks take so long?".
Troubleshooting
Log directory missing after install
dotagent creates logs/daemon/ on first daemon start. Run:
Log file not rotating
tracing-appender rotates at first write past midnight. If the daemon is idle (no event after midnight), the rename is deferred until the next log line. Force one:
Sweeper not running
The sweeper fires at 03:00 ± 30min and is single-shot per calendar day. Check the daemon log for the line log retention sweep completed. If absent for >24h, file an issue with the daemon log timestamps.
Disk filling despite retention
Check
retention_daysisn't unreasonable: 90+ days × verbose level = real disk.Check that the sweeper has write permission.
logs/agents/<name>/inherits the daemon's umask; if your agent script chowns its log dir, the sweep fails silently.
OTel pipeline not exporting
Confirm the daemon picked up the config:
Common gotchas:
TLS error: the OTLP endpoint requires
https://(gRPC over TLS). Local Jaeger useshttp://localhost:4317.Headers: vendor-specific auth goes in
OTEL_EXPORTER_OTLP_HEADERS, not inconfig.toml. Comma-separatedk1=v1,k2=v2.Reload required: changing config without restarting the daemon is a no-op for OTel. Use
dotagent reload.
Logs full of noise
Lower the verbosity:
Or per-target via env:
Related
docs/concepts/agents.md— what an agent isdocs/concepts/plugins.md— how plugins emit eventsdocs/reference/agent-spec.md— manifest fieldsdocs/security/threat-model.md— audit log's role in detection
Last updated
Was this helpful?