Reading Instance Logs
A field guide to reading and analyzing the logs that Codumentor instances have
already written — formats, eras, pitfalls, and scan tooling. Written primarily
for agents (and humans) doing log forensics on a production host. For
configuring logging, see Monitoring. For a worked example of
a full-fleet analysis, see the
2026-07-07 log audit.
Concrete paths below describe the ardinsys production host (user codumentor);
adapt the pattern for other deployments.
1. Where logs live
Each instance is a directory ~/codumentor-<name>/ containing logs/,
its YAML config, audit.db, and a codumentor symlink to the shared checkout.
Each runs as a systemd user service codumentor-api-<name>.service.
ls -d ~/codumentor-*/logs # instances on disk
systemctl --user list-units 'codumentor-api-*' # instances actually running
Always compare those two lists. They can disagree in both directions:
a directory with no service (dead instance), or a running service whose
directory was deleted (ghost instance — it still serves its port and writes
logs to deleted inodes; check ls -l /proc/<MainPID>/cwd and
lsof -p <pid> | grep deleted).
Inside logs/:
| File | What it is |
|---|---|
codumentor.log | current log (text format) |
codumentor.log.YYYY-MM-DD | daily rotation, kept ~30 files |
codumentor.json, codumentor.json.YYYY-MM-DD | legacy JSON era (ended 2026-05-15); orphaned by rotation, never auto-deleted — still valuable history |
Sizes range from ~100 KB to 139 MB per day per instance. Never read a whole
file blindly; see §5.
2. The three format eras
The same instance's history spans up to three formats. Any longitudinal
analysis must handle all three.
Era 1 — JSON lines (until 2026-05-15, codumentor.json*):
{"timestamp": "2026-05-15T09:24:35.093", "level": "INFO", "logger": "codumentor.api",
"message": "...", "module": "main", "function": "lifespan", "line": 447,
"exception": "Traceback ...", "context": {"session_id": "c_937b96..."}}
Richest format: module/function/line, structured exception, full
session id. Parse with json.loads per line (skip non-{ lines).
Era 2 — plain text (2026-05 onward):
2026-07-06 10:40:19,067 - api - INFO - Shutting down Codumentor API server...
Era 3 — text with context prefix (mixed into era-2 files, session-scoped
records):
2026-07-07 08:23:39,735 > session_id=c_a27b741af82c4ae... tool=subagent - telemetry - INFO - Tool read_file ms=10.1
Caveats: context field order is not stable (session_id=... tool=... and
tool=... session_id=... both occur); the session id is **truncated with a
literal ...** — treat it as a prefix, match with grep -F 'c_a27b741', and
resolve full ids via audit.db if needed.
One regex parses eras 2 and 3 (battle-tested against the full corpus):
TEXT_RE = re.compile(
r"^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}),\d{3}\s+(?:>\s+(.*?)\s+)?-\s+([\w.]+)\s+-\s+"
r"(DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+-\s+(.*)$"
) # groups: date, time, context?, logger, level, message
Continuation lines: anything not matching the regex belongs to the previous
record — tracebacks, multi-line payloads, raw subprocess stderr. Up to ~30 % of
all lines in agent-heavy instances. A line-oriented grep therefore misses most
traceback content, and a "records per level" count is not a "lines" count.
3. Timestamps and rotation
- Record timestamps are local time (Europe/Budapest), no offset.
- Rotation happens at midnight UTC (
rotation_utc: true), so each dated file contains ~2 h of the neighboring local day, and DST shifts the overlap. - When correlating with
journalctl(local time) or external systems, keep the 2-hour ambiguity in mind; when precision matters, bracket queries generously.
4. Logger names drift across eras
The same module has logged under three namespaces over time:
codumentor.plugins.branch_switching → plugins.branch_switching →
src.codumentor.plugins.branch_switching (also
codumentor.src.codumentor.plugins...). Grep by suffix, not full name:
grep -E ' - [a-z_.]*branch_switching - ' codumentor.log*
5. Volume safety rules (agents: read this twice)
- Never
cat/Read a whole log file — files reach 139 MB. Usegrep -m,head/tail, byte-cappedhead -c, or the bundled scanner. - Log lines can be tens of KB: the
models,prompt,tools, andworkspace_isolation.strategiesloggers embed full LLM payloads, prompts, and tool results (entire source files) in single records. Pipe grep output throughcut -c1-300before eyeballing. - Payloads poison naive greps. The logs contain source code and documents, which match almost any keyword. Anchor patterns to record structure — the level token with its separators — not bare words:
``bash``
grep -c ' - ERROR - ' file # good: counts ERROR records
grep -c ERROR file # bad: also matches payload content
The same trap applies to journalctl output ("Started" inside a payload is
not a service start; filter with --identifier systemd or -o cat +
^Started ).
- Rough era-2/3 fleet baseline (audit of 2026-07): DEBUG ≈ 85 % of records;
modelsalone ≈ 43 % of bytes. ERROR records are rare (≈ 0.04 %) — scanning only errors/warnings is cheap even fleet-wide (~15 s for 2.7 GB).
6. Standard analysis method
For anything beyond a single lookup, do a signature-clustering pass first,
then sample clusters with context. This turns millions of lines into a few
hundred signatures.
The scanner used for the 2026-07-07 audit is bundled as a project skill:
python3 .claude/skills/reading-instance-logs/scripts/scan_logs.py \
--base /home/codumentor --out /tmp/scan_result.json
It handles all three eras, counts levels per instance, clusters
ERROR/WARNING/CRITICAL by normalized signature (uuids/hex/paths/emails/IPs/
numbers/long-quoted-strings → placeholders), extracts traceback exception
types, builds a per-day error timeline, and attributes bytes per logger.
Read the printed summary, then drill into scan_result.json.
Then pull context per cluster:
grep -n -m3 -B5 -A25 'distinctive message fragment' logs/codumentor.log.2026-07-02 | cut -c1-300
Quick recipes:
# ERROR records today, by logger + message head
grep ' - ERROR - ' logs/codumentor.log | grep "^$(date +%F)" \
| sed -E 's/^[^-]+- ([a-z_.]+) - ERROR - (.{0,60}).*/\1 :: \2/' | sort | uniq -c | sort -rn
# everything for one session (id may be truncated in the log — use the prefix)
grep -F 'session_id=c_a27b741' logs/codumentor.log | cut -c1-300
# true service starts/stops (not payload noise)
journalctl --user -u codumentor-api-<name>.service -o cat --identifier systemd \
| grep -E '^(Started|Stopped) '
# a JSON-era error with its structured exception
grep -m1 '"level": "ERROR".*fragment' logs/codumentor.json.2026-05-13 | python3 -m json.tool
7. Where else to look (the log is not the whole story)
| Source | What's there |
|---|---|
journalctl --user -u codumentor-api-<name> | duplicate of app stdout (full DEBUG firehose) + systemd lifecycle events; survives log deletion |
~/.codumentor/setup-uppers/<repo>/<hash>/log | workspace-isolation project-setup output; the main log only records Setup failed (rc=N); see log at <path> — quarantined attempts get suffix .failed-<epoch> |
~/codumentor-<name>/audit.db | SQLite audit trail (full session ids, user actions) |
~/codumentor-<name>/logs/ after CLI runs | ingest.sh runs the CLI against the same config → a second process writes the same codumentor.log; interleaved records from two pids are possible, and rotation can race |
/var/log/auth.log | ssh activity incl. the LLM tunnel |
8. Known interpretation gotchas
- Trailing-colon errors:
Streaming model call failed: ...response:with nothing after the colon means the exception'sstr()was empty (typicallyhttpx.ReadTimeout). The class name is only in the traceback — look for the continuation lines, or in era 1 theexceptionfield. - Multi-record failures: one plugin-import failure emits 2–3 consecutive ERROR records (
Failed to import.../Module path: .../Python path: ModuleSpec(... at 0x...)). Count root causes, not records. - Cardinality bombs: some messages embed unique ids (
RPC request rpc_bb4132... not found) — hundreds of "distinct" messages can be one stuck retry loop. Normalize before counting (the scanner does). - Interleaved subprocess stderr: git/clone errors splice their stderr into the record stream; a failure's text can appear after the next record.
- Level skew: production runs at DEBUG; there is no CRITICAL discipline — real feature-down conditions may log as ERROR or even WARNING, and the same event can log at different levels from different modules. Cluster first, judge severity from content.
- No instance name in records: once a file is copied off-box, provenance is the filename/path only. Keep instance names in your working paths.
- Repeated identical errors ≠ new events: per-request re-logging (e.g. a broken UI manifest logged on every page load) inflates counts; use first-seen/last-seen windows from the scanner, not raw counts, to judge whether something is new.
9. Fleet-wide error sweep, start to finish
# 1. inventory (and ghost check)
ls -d ~/codumentor-*/logs
systemctl --user list-units 'codumentor-api-*'
# 2. cluster everything (~15 s per 2.7 GB)
python3 .claude/skills/reading-instance-logs/scripts/scan_logs.py --base ~ --out /tmp/scan.json
# 3. read: per-instance level counts, top signatures, incident days
# (script prints these; /tmp/scan.json has the full data)
# 4. sample each significant cluster with grep -B/-A (±25 lines, cut -c1-300)
# 5. cross-check systemd/journal for restarts, disk (df -h), journal size
# (journalctl --disk-usage), and OOM (journalctl -k | grep -i oom)
# 6. write findings with: signature, count, first/last seen, instances,
# severity, sample line, root cause if determined