# libtrace Specification v2.1.0 *Execution-trace client library: two primitives, structured logging, and the datagram record a collector receives.* **Status:** current. Describes `include/trace/Trace.h` and the records `src/Trace.cpp` emits, as shipped. ## 0. Scope libtrace is a **client**. It formats events and sends them fire-and-forget to a collector; it holds no database, opens no file, and performs no persistence. Any statement that this library is "SQLite-backed" describes a collector, not this code — the header's own note is the accurate one: *services never touch SQLite.* **The collector is not part of this package**, and since 2026-08-17 the reference collector is not distributed at all. That makes §4 — the record format — the load-bearing half of this document: it is the contract anyone writing a collector implements against, and it previously existed only in `src/Trace.cpp`. ## 1. Connection ```cpp bool init(const std::string& service_name, const std::string& socket_path = "/run/traced/traced.sock"); void shutdown(); bool connected(); ``` - `init` creates an `AF_UNIX` `SOCK_DGRAM` socket and autobinds the client (an empty bind address, so the kernel assigns one). `service_name` identifies the process in every record it subsequently emits. - **`init` returning `false` is not fatal and MUST NOT be treated as one.** Tracing degrades to stderr-only. A service that refuses to start because a collector is absent has inverted the dependency this library exists to avoid. - `init` is idempotent: a second call while already connected returns `true` without reopening. - `shutdown` is safe to call when `init` was never called. - `connected()` reports the current state. ## 2. Span — the RAII primitive ```cpp Span(const char* scope, const char* intent); ``` `scope` names the part of the code; `intent` says what and why in human terms. A `Span` emits a `span_start` record on construction and a `span` record on destruction carrying the measured duration. | Method | Effect | |---|---| | `set(key, value)` | attach a field; later `set` on the same key replaces it | | `setTraceId(id)` | attach a correlation id for cross-process joining | | `markError()` | adds the field `error="true"` to the exit record | | `cancel()` | **suppresses the exit record**; a `span_cancel` is sent instead | `Span` is non-copyable and non-movable. Duration is measured on `std::chrono::steady_clock`, so it is unaffected by wall-clock adjustment, and is reported in **whole milliseconds** — a span shorter than a millisecond reports `0`, which is a resolution limit and not an error. ## 3. Point events and logging ```cpp void emit(const char* category, const char* message, std::initializer_list> fields = {}); enum LogLevel { DEBUG, INFO, WARN, ERROR }; void log(LogLevel level, const char* fmt, ...); // printf-checked void vlog(LogLevel level, const char* fmt, va_list args); std::string generateTraceId(); ``` `emit` records a decision, status or observation with no duration. `log` writes to **stderr and** to the collector — stderr for journald, the record for the trace store. It is `printf`-format-checked at compile time. The level appears in the record as a field, not as a distinct record type (§4.3). ## 4. Record format One JSON object per datagram, sent with `sendto(..., MSG_DONTWAIT)`. **Delivery is fire-and-forget**: if the collector is down or the socket buffer is full the record is dropped silently. This is deliberate — the alternative couples the emitting service's latency to collector availability. A collector MUST NOT assume it has seen every record a service produced. ### 4.1 Common keys | Key | Type | Present | Meaning | |---|---|---|---| | `ts` | integer | always | Unix time in **seconds** (`std::time`) | | `svc` | string | always | the `service_name` given to `init` | | `type` | string | always | one of `span_start`, `span`, `span_cancel`, `emit` | | `cat` | string | span + emit | span `scope`, or emit `category` | | `msg` | string | span + emit | span `intent`, or emit `message` | | `span_id` | string | span records | see §4.2 | | `dur_ms` | integer | `span` only | measured milliseconds | | `trace_id` | string | when set | omitted entirely when empty | | `fields` | object | when non-empty | flat string→string map; omitted when empty | `ts` is **second-resolution**, which is coarser than `dur_ms`. A collector ordering records within a second must use arrival order or `span_id`, not `ts`. ### 4.2 `span_id` Formatted `s%04x%08lx`: the literal `s`, the process id in 4 hex digits, and a per-process sequence number in 8 hex digits. It is unique **within a process** and is not a global identifier — two processes can collide, which is what `svc` and `trace_id` are for. ### 4.3 Record types - `span_start` — span construction. - `span` — span destruction; the only type carrying `dur_ms`. `markError()` adds `fields.error = "true"`. - `span_cancel` — emitted instead of `span` when `cancel()` was called. - `emit` — both `emit()` and `log()`. A `log` record is an `emit` whose `fields.level` is one of `DEBUG`, `INFO`, `WARN`, `ERROR`. **There is no separate log record type**, so a collector that wants only logs filters on the presence of `fields.level`. ### 4.4 String escaping All string values are JSON-escaped. Control characters below `0x20` are emitted as `\u%04x`. There is no length cap in the library; a datagram larger than the socket buffer is dropped by §4's fire-and-forget rule. ## 5. Threading A process-wide mutex guards the socket, so `emit`, `log` and `Span` construction/destruction are safe from any thread. `init` and `shutdown` take the same lock. `Span` objects themselves are not shared between threads — each belongs to the scope that created it. ## 6. Conformance A collector conforms if it: 1. accepts an `AF_UNIX` `SOCK_DGRAM` socket at a configured path and reads one JSON object per datagram; 2. tolerates absent optional keys (`trace_id`, `fields`, `dur_ms`) rather than requiring them; 3. tolerates gaps — dropped records are normal, not corruption; 4. treats `span_id` as process-scoped and does not assume global uniqueness. A client conforms if `init` failure degrades to stderr rather than aborting, and if emission never blocks the calling thread on collector availability.