LogCapture
The shared, platform-independent capture core.
Maps a NormalizedLogEvent to an OTLP-shaped LogRecord and exports it into the durable buffer. Every per-platform capture edge funnels through this one type, so the mapping — level, body, attributes, identity, timestamps — is identical on every target.
Self-capture exclusion (safety invariant)
Capture hooks the process-global logging config, so it sees every event in the process. The durable exporter itself logs on its hot path (a buffer-cap eviction warning, store-failure errors), so capturing those would feed a captured eviction-warn back into export → evict again → warn again — a self-sustaining loop that crowds out real application logs. To make that impossible, any event whose loggerName is under the exporter's own package (us.tractat.kuilt.otel) is dropped before a record is built. Both capture and captureAll reach that decision through one private mapper, so batching a run of events cannot weaken it into a per-run filter.
The exclusion is scoped to only the exporter's own us.tractat.kuilt.otel.* loggers — narrow enough to break the export feedback loop, but no broader. In particular it does not exclude kuilt's library loggers (us.tractat.kuilt.session.*, ...liveness.*, ...raft.*, ...nw.*, …): a consumer that uses kuilt as its networking library depends on those library diagnostics being captured just like its own application logs, and any consumer running under the us.tractat.kuilt package (but outside .otel) is captured normally. This is a non-negotiable invariant, not a configurable filter, and every capture edge inherits it through this one core.
Injected dependencies
Both time and randomness are dependencies, never reached for directly:
clock supplies the record timestamps. A test injects a virtual clock; a production install passes
kotlin.time.Clock.System.random supplies the fresh 8-byte
recordIdper record. A test injects a seeded Random; a production install passesRandom.Default.
Edge resolution (emit-time semantics)
Anything that depends on when and where the line was logged is resolved at the synchronous capture edge, on the caller, and carried on the queued NormalizedLogEvent — never re-derived on the drain coroutine. That covers the ambient trace context (#1034), the CaptureConfig.attributeMapper (#1630) and the instant the line was logged (#1993). A queueing edge calls resolveAtEdge once; capture then reads the snapshot. Resolving any of them on the drain stamps records with whatever the ambient state has become by then, which the consumer cannot detect or repair.
Parameters
the durable log buffer this capture writes into.
which events to keep and how to shape their attributes.
source of the record timestamps (required — never the wall clock). Read twice per record on a queueing edge: once by resolveAtEdge for the event time, once by capture for the observed time.
source of the per-record id bytes (required — never an unseeded default).
optional trace/sampling gate. When null (the M1 default) capture is always-on and records carry no trace ids. When set, the trace is resolved at the synchronous capture edge via resolveAtEdge and carried on NormalizedLogEvent.activeTrace; capture then gates on that snapshot (see resolveAtEdge and capture).
Constructors
Functions
Map a run of events to LogRecords and export them as one write turn.
Snapshot everything that must be sampled at the moment the line was logged onto event, for a queueing capture edge to hand to the drain.
Resolve the trace active on the current call.