Capture & the save order
The central claim of CAOS logging is that context is captured by construction, not by hand. A logging framework bolted onto a platform can only record what the developer thinks to attach; a logging layer built into the kernel records what the kernel already knows. This page describes exactly what the kernel knows, where in the save order it knows it, and how that becomes structured log context with no developer effort.
The principle: the runner already has the context
Section titled “The principle: the runner already has the context”When a developer writes a log line, three kinds of information matter: the message (what the code wanted to say), the context (where in the system this happened, to whom, at what cost), and the correlation (what else belongs to the same unit of work). On a bolt-on logger the developer must assemble all three. In CAOS the developer supplies only the message; the kernel supplies context and correlation because it is the component executing the save and it already tracks them for its own purposes.
| Context dimension | Who knows it | How it’s captured today |
|---|---|---|
| Which save phase is running | The pipeline, via its phase-entry trace | Already recorded as an ordered trace with a per-phase run count |
| Which participant raised the line | The pipeline, which invokes each participant by key | Known at dispatch; not yet stamped onto anything durable |
| Budget spent so far | The save governor’s running counters (fuel, DML, rows, outward calls, value-cells) | Tracked continuously; a breach already produces a limit error |
| The caller and their access | The access resolvers that gate every read and write | Resolved on every operation; the same resolver narrows what the log reader may later see |
| The correlation id | Nobody, yet — it is minted only on the failure path and not threaded | The one missing primitive (below) |
The design does not add measurement. It persists measurement the kernel already performs, and stamps it onto a log entry at the moment the entry is created.
The missing primitive: one correlation id, threaded end to end
Section titled “The missing primitive: one correlation id, threaded end to end”Today a correlation id is minted only when the error normalizer hits its internal catch-all, and it lives only long enough to key a telemetry write. There is no single id that is born with the request, flows through every save phase, rides every outbox message, stamps every error envelope, and lands in telemetry. Introducing that id is the foundational change, because every correlation guarantee — the audit surface’s “walk one failure across all three streams”, a Log record gathering all its entries, an error linking to the trace that raised it — is unenforceable without it.
The id is minted once, at the entry boundary of a unit of work: the start of a save request, an API request, a background job invocation, or a deploy. From there it is carried on the ambient transaction context and stamped, without further developer action, onto:
- every phase-trace entry as the save proceeds,
- every log entry the developer or the kernel creates,
- every outbox message enqueued during the effects phase,
- every error envelope the normalizer emits (not just the catch-all path), and
- the telemetry record for any raw detail.
Where capture hooks into the save order
Section titled “Where capture hooks into the save order”Capture is not a new phase. It is a set of observations at the existing phase boundaries the save order already defines. Nothing here changes the seven-phase sequence; logging rides alongside it.
| Save phase | What is captured | Entry-level context stamped |
|---|---|---|
| Request entry | Mint/inherit the correlation id; open an in-memory Log for this transaction | correlationId, actor, tenant, scenario, startedAt |
| SHAPE | Any developer log.* from a derivation; calc-function diagnostics |
phase: shape, participantKey, budget-so-far |
| ADJUST | Developer logs from before-write automation | phase: adjust, participantKey, the triggering record |
| VALIDATE | A failed rule becomes a WARN/ERROR entry carrying its error envelope | phase: validate, errorEnvelope, fieldApi |
| WRITE | Constraint/RLS/lifecycle-gate outcomes; roll-up maintenance notes | phase: write, the persisted record id |
| EFFECTS | Developer logs from after-write automation; each entry’s log line is staged into the outbox alongside the effect’s own outbound messages | phase: effect, participantKey, budget-so-far |
| COMMIT | The transaction outcome is stamped onto the Log: committed, durationMs, final budget snapshot, phaseRuns |
closes the Log envelope |
| POST-COMMIT | Buffered entries are published as durable Log/Log Entry records (see delivery) | — |
Two facts about this table are load-bearing:
- Every entry inherits its phase and participant automatically. A
log.info('reached pricing branch')written inside an after-write effect is stampedphase: effectand tagged with the effect’s participant key without the developer naming either. On Nebula the equivalent requires the developer to know and attach that context by hand, and most never do. - The budget snapshot is free. Because the governor counts continuously,
budgetAtEntryon each entry and the finalbudgeton the Log are reads of a counter the kernel maintains anyway — you get “this transaction spent 84 of 150 DML and 6.2s of 10s CPU” with no instrumentation.
The Log is assembled, then sealed
Section titled “The Log is assembled, then sealed”A Log is built up in memory across the transaction and sealed at the outcome boundary:
- Entries accumulate against the ambient Log as
log.*calls fire in each phase. - At COMMIT, the kernel seals the envelope: outcome
committed, duration, the final budget snapshot,phaseRuns(proving the single-pass property — each phase ran at most once), the highest level seen, and the entry count. - If the transaction aborts, the seal records
rolled_backand the reason, and delivery switches to the out-of-band path so the failure is not lost with the rows.
Because the envelope is sealed from data the kernel already holds, a Log is a faithful, structured record of the transaction — not a best-effort reconstruction from scattered System.debug strings.
What the developer still controls
Section titled “What the developer still controls”Automatic context does not mean the developer is mute. The developer API lets code add exactly the things the kernel cannot infer:
- the message and its level,
- a scenario naming the unit of work (defaulted per entry point, overridable),
- tags for cross-cutting classification,
- a related record when the meaningful subject differs from the triggering row, and
- structured fields — arbitrary typed key/values for machine-readable detail.
Everything else — phase, participant, budget, actor, access, correlation — is supplied by the kernel.
How Nebula Logger does it, and why the kernel wins here
Section titled “How Nebula Logger does it, and why the kernel wins here”Nebula’s LogEntryEventBuilder exposes a fluent surface — .setRecord(), .setField(), .addTag(), .setExceptionDetails() — precisely because Apex code has no ambient access to the transaction’s execution context. Apex cannot ask “which order-of-execution step am I in?” or “how much of my DML limit have I spent, attributed to this line?”; the platform does not expose per-step attribution to user code. So Nebula’s developer must manually attach every piece of context they want, and the framework enriches what it can from Limits and UserInfo at save time. This is not a Nebula flaw — it is the ceiling of what an app-layer framework on a closed platform can reach.
CAOS is on the other side of that boundary: it is the runner. The phase, the participant, the budget, and the access checks are the kernel’s own working state, so attaching them to a log entry is a field copy, not an API the developer must remember to call. That is the entire reason CAOS logging can be more succinct and richer than Nebula at the same time — less to type, more captured.
Sources
Section titled “Sources”- Nebula Logger — Logging in Apex (wiki) — the
LogEntryEventBuilderfluent methods (setRecord,setField,addTag) developers use to attach context by hand. - Joys of Apex — Advanced Logging Using Nebula Logger — how the framework enriches entries from
Limits/UserInfoat save time, and the boundaries of app-layer context. - Salesforce — Apex Governor Limits — why Apex has no per-step budget attribution to expose to a logger.