Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Logging

Standing up a collector and a trace backend is a real decision. If your telemetry today is structured logs, that’s a lot more work than the two wrappers. You don’t have to do it.

import { codeMode, logTracer } from "@tanvincible/mocon";

const observed = codeMode({
  capabilities: { /* same as before */ },
  tracer: logTracer((record) => logger.info(record)),
});

That’s the only line that changes. No SDK, no exporter, no collector, no backend.

Records

Every finished span becomes one flat record handed to your logger:

{
  "name": "execute_tool inventory_search",
  "kind": "client",
  "trace_id": "f3d8f44c4f3d6a83bd2518356040dd1a",
  "span_id": "33924c3b939b1dcc",
  "parent_span_id": "eceff414b8114181",
  "start": "2026-09-20T10:14:02.118Z",
  "duration_ms": 17.68,
  "code_mode.execution.id": "exec_7f3a",
  "code_mode.crossing.outcome": "output",
  "gen_ai.tool.name": "inventory_search",
  "gen_ai.tool.call.arguments": { "q": "widget" },
  "code_mode.provenance.gen_ai.tool.call.result": "P"
}

The whole attribute set, the provenance labels, the ids, a duration and a status. Payloads come back as real values rather than JSON strings, because a log record can hold an object where a span attribute can’t. Group by code_mode.execution.id and you’ve got the whole run, in the pipeline you already query.

Trade-offs

The things a trace store is actually for. A rendered waterfall, and metrics off spans without aggregating log lines yourself.

Reversible

The ability to change your mind. Switching to a real trace pipeline later means passing a different tracer and touching nothing else.

Flat

Records are flat, one per span, rather than nesting calls inside their run.

Nesting means holding children until the parent closes, and a call the program makes a tick later then never gets written at all. Flat records carry parent_span_id, so you rebuild the tree by grouping instead of trusting the writer to buffer correctly.

Options

logTracer({
  write: (record) => logger.info(record),
  raw: true,   // leave payloads as JSON strings instead of decoding them
});

If your logger throws, the record is dropped and the call carries on. An observability problem should never break the thing it’s watching.