feat(observability): structured ctx.log fields, trace correlation, durable log sink - #147
Conversation
Enrich the existing `ctx.log` emission so the framework produces the rich, filterable, trace-linked logs a log-management backend (the Cloud log viewer, or any OTLP collector) needs — all backward-compatible and usable with no cloud dependency. - Structured fields: `ctx.log.info(message, fields)` and a chainable `ctx.log.with(fields)` child logger. Fields become OTLP log-record attributes and ride the Workers-Logs console event (raw positional args still do not, preserving the PII boundary). The `(string, object)` call is the structured form; every other shape stays console-style, so existing calls are unchanged. - Full severity ramp: add `trace` (1) and `fatal` (21) to the console tiers so `ctx.log` spans the whole OpenTelemetry range. - Trace correlation: log records carry the dispatch's `traceId`/`spanId` (from the inbound `traceparent`), linking a line to its RPC span. - Move the emitted logger closure into a `makeLogger` DO-base method (arg parsing, field merge, buffer/console/sink fan-out) so the codegen template stays thin; regenerate the golden fixture + example _generated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an opt-in built-in sink that persists every `ctx.log` line to a
Cloudflare Pipeline → R2, so an app has a queryable log store (read back
with R2 SQL) in its own account with no cloud dependency — the durable
counterpart to the streaming `otlpSink`.
- `pipelineLogSink({ pipeline })` writes one structured record per line
(message, level, functionPath, fields, trace/span ids, shard, user,
ts). Log-only; RPC-span metrics stay in `analyticsEngineSink`.
- Thread a `waitUntil` context through the DO `LogSink.onLog` contract
(from `state.waitUntil`) so a durable send survives isolate teardown;
falls back to fire-and-forget when unavailable.
- Document the sink, the structured `ctx.log` fields/`.with(...)` API, and
the extended severity ramp in the observability concept doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The observability commit added `trace`, `fatal`, and the chainable `with` to `LunoraLogger`, but `@lunora/testing`'s `noopLog` test double still only implemented the original five methods (TS2739). Complete it — `with` returns the noop itself so `.with(...).with(...)` chains stay inert. (Surfaced by the oxc/isolatedDeclarations lint:types on alpha; pre-existing gap in this branch, not the migration. Squash into the trace-levels commit if desired.)
Close the follow-up gaps so the structured `ctx.log` fields added earlier are actually usable across every surface, not just the OTLP/pipeline sinks. - Sinks: `webhookSink` and `sentrySink` now forward `ctx.log` lines (`onLog`) — webhook takes a `transformLog` redactor, sentry an opt-in `captureLog`. Previously logs only reached otlpSink/pipelineLogSink. - Dev terminal: the shared `formatLunoraEvent` renders structured fields as compact `key=value` pairs, appends a short `trace=` suffix, and maps `trace`/`debug`→info, `fatal`→error (used by both the CLI and Vite). - Studio Logs panel: the buffer entry + getLogs carry `fields`; the panel renders them and the search box matches on field values. - Docs: flag the `(string, object)` behavior change (structured, not a joined message) and note which sinks forward logs. Verified end-to-end against the blog example under a live dev server: a real `posts:list` dispatch emitted the fields on the console event with a populated trace_id/span_id (trace correlation working on a real request). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the two-reviewer thermo audit of the structured-logging change.
Bugs:
- Normalize `ctx.log` fields once at parse time — coerce each value to a
JSON-safe primitive, snapshot into a fresh object, drop an empty bag. Fixes
a `bigint`/circular value crashing the `getLogs` serialization (Studio Logs
panel), a mutate-after-log aliasing footgun, and an empty `{}` riding every
surface.
- Dedupe OTLP log-record attributes by key (Map) so a field reusing a reserved
`lunora.*` key overrides it instead of emitting a duplicate KeyValue.
- Loud upgrade callout: `webhookSink` now egresses `ctx.log` lines; existing
configs need `transformLog` and `onlyErrors` doesn't gate logs.
Structure (dedup the hand-mirrored contracts into `shared/`, inlined so no
cross-package dependency edge — the same pattern this feature already used for
`shared/otlp.ts`):
- `shared/log-fields.ts` — one field renderer/coercer/normalizer, replacing the
three copies in runtime/config/studio (two byte-identical).
- `shared/log-event.ts` — one `LogEvent` shape, `ContextLogLevel` union, and
`BUFFER_LEVEL`, replacing the do↔runtime hand-mirrors; the `onLog` call is now
structurally guaranteed. `shared/otlp.ts` reuses the level union too.
- Move the pure `parseLogArgs`/`isLogFields` helpers out of the 8k-line
shard-do.ts into request-log.ts, next to their collaborators.
- Studio: render fields once per row and skip an empty chip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ Deploy Preview for lunorash ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Thank you for following the naming conventions! 🙏 |
|
Thank you for confirming the Contributor License Agreement! 🙏 |
|
Warning Review limit reached
Next review available in: 31 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThe PR adds structured ChangesStructured observability logging
Tooling and formatting alignment
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Application
participant ShardDO
participant ObservabilitySink
participant Pipeline
Application->>ShardDO: ctx.log(message, fields)
ShardDO->>ObservabilitySink: onLog(event, waitUntil)
ObservabilitySink->>Pipeline: send structured log record
Pipeline-->>ShardDO: durable delivery promise
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/runtime/src/observability-sinks.ts (1)
197-205: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winConsider an explicit opt-in/opt-out for
ctx.logforwarding inwebhookSink, mirroringsentrySink's opt-incaptureLog.
onLogis always installed here — an existingwebhookSink({ url, transform })consumer will start forwarding everyctx.logline to the same endpoint after upgrading, with no toggle besides writing atransformLog: () => null.sentrySinktook the opposite (safer) default: log capture is off unlesscaptureLogis supplied. A boolean gate (or requiringtransformLogto be a filter rather than the sole disable mechanism) would make the two sinks' privacy/cost posture consistent.Also applies to: 229-267
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/runtime/src/observability-sinks.ts` around lines 197 - 205, The webhookSink log-forwarding path should be opt-in, matching sentrySink’s captureLog behavior, rather than always installing onLog. Update webhookSink and its options so ctx.log events are forwarded only when an explicit captureLog-style boolean is enabled, while preserving transformLog processing for enabled forwarding and leaving existing non-log webhook events unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/docs/src/content/docs/concepts/observability.mdx`:
- Around line 41-45: Correct the observability documentation sentence beginning
“All five network/log sinks” so its count matches the four listed sinks:
otlpSink, webhookSink, pipelineLogSink, and sentrySink. Update only the count or
wording needed to accurately describe these sinks.
In `@packages/do/src/request-log.ts`:
- Around line 372-391: Update isLogFields to accept only actual plain objects,
excluding Error, Date, Map, custom class instances, and other non-plain objects
while retaining null and array rejection. Keep parseLogArgs unchanged so only a
string plus validated plain-object fields uses structured logging; all other
arguments must continue through renderLogMessage.
---
Nitpick comments:
In `@packages/runtime/src/observability-sinks.ts`:
- Around line 197-205: The webhookSink log-forwarding path should be opt-in,
matching sentrySink’s captureLog behavior, rather than always installing onLog.
Update webhookSink and its options so ctx.log events are forwarded only when an
explicit captureLog-style boolean is enabled, while preserving transformLog
processing for enabled forwarding and leaving existing non-log webhook events
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8fc1de7f-6a12-48c3-acd4-05379598aecc
⛔ Files ignored due to path filters (15)
examples/auth-playground/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/blog/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/expo/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/offline-rejections/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/payment-demo/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/realtime-cursors/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/todo-app/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by nonepackages/codegen/__tests__/fixtures/simple/expected/_generated/shard.tsis excluded by!**/_generated/**,!**/__tests__/**and included bypackages/**packages/config/__tests__/log-format.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/do/__tests__/shard-do.admin.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/runtime/__tests__/observability-sinks.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/features/logs/logs-panel.test.tsxis excluded by!**/__tests__/**and included bypackages/**shared/log-event.tsis excluded by none and included by noneshared/log-fields.tsis excluded by none and included by noneshared/otlp.tsis excluded by none and included by none
📒 Files selected for processing (16)
apps/docs/src/content/docs/concepts/observability.mdxpackages/codegen/src/emit.tspackages/config/src/log-format.tspackages/config/tsconfig.jsonpackages/do/src/log-buffer.tspackages/do/src/request-log.tspackages/do/src/shard-do.tspackages/runtime/src/index.tspackages/runtime/src/observability-sinks.tspackages/runtime/src/observability.tspackages/server/src/index.tspackages/server/src/types.tspackages/studio/src/features/logs/logs-panel.tsxpackages/studio/src/lib/admin.tspackages/studio/tsconfig.jsonpackages/testing/src/harness.ts
- `pnpm run api:update` — the structured-logging exports drifted the public API of @lunora/runtime, /server, /do, /studio, and the lunora umbrella (`pipelineLogSink`, `LogFields`, `LunoraLogMethod`, `LogEntry.fields`, the shared `LogEvent` shape). Also catches up errors/nuxt snapshots that drifted on alpha independently of this change. - Format the config log-format test to the root Prettier config the CI `prettier --check .` gate uses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `pnpm/action-setup` steps pinned `version: 11.5.3` while package.json's `packageManager` is `pnpm@11.15.0`, so the setup action aborted with "Multiple versions of pnpm specified" (Benchmarks / CodSpeed, release). Drop the pin so the action reads `packageManager` — one source of truth, no drift. Align the CONTRIBUTING note accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/CONTRIBUTING.md:
- Line 10: Update the Corepack setup guidance in CONTRIBUTING so it works for
the documented Node.js range including 25+. Add the standalone Corepack
installation step before corepack enable, or revise the supported Node.js range
to exclude 25 and later; keep the packageManager pinning guidance consistent
with the selected approach.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 32e2a9d0-377e-42f4-830b-73c1517f9681
⛔ Files ignored due to path filters (8)
api-snapshots/do.api.mdis excluded by none and included by noneapi-snapshots/errors.api.mdis excluded by none and included by noneapi-snapshots/lunora.api.mdis excluded by none and included by noneapi-snapshots/nuxt.api.mdis excluded by none and included by noneapi-snapshots/runtime.api.mdis excluded by none and included by noneapi-snapshots/server.api.mdis excluded by none and included by noneapi-snapshots/studio.api.mdis excluded by none and included by nonepackages/config/__tests__/log-format.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**
📒 Files selected for processing (3)
.github/CONTRIBUTING.md.github/workflows/codspeed.yml.github/workflows/semantic-release.yml
Merging this PR will improve performance by 28.36%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | compare cached argsKey (new) |
78.3 µs | 61 µs | +28.36% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing feat/observability-logs (4fe4b07) with alpha (a2d60e5)2
Footnotes
-
1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports. ↩
-
No successful run was found on
alpha(ce59723) during the generation of this report, so a2d60e5 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
`prettier --check .` (the CI gate) flagged 8 files unrelated to this branch — AGENTS.md and 7 studio UI components — that predate it under the root Prettier config. Formatting them (plus the config test fixed earlier) makes the prettier gate pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itive - E2E: `scaffold.spec.ts` resolved `typescript/lib/tsc.js`, which TS7-native's package `exports` no longer expose (nor `bin/tsc`). Read the launcher path from the compiler's own `package.json` `bin.tsc` — version-agnostic across classic and native. - Secrets: `projectId` in a values type-test tripped the scanner's Cypress rule; annotate `secret-scanner:allow` — it's a schema field name, not a credential. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI statusFixed on this branch (were failing due to the TS7/oxc migration on
|
Address CodeRabbit review on #147. - `isLogFields` accepted any non-array object, so `ctx.log.error("failed", err)` (the standard console-style idiom) routed the `Error` into the structured branch, where it has no own enumerable fields and was silently dropped. Restrict to plain objects (Object.prototype / null-prototype) so Errors, Dates, Maps, and class instances stay console-style; add a regression test. - docs: "All five network/log sinks" → "four" (only four are listed). - CONTRIBUTING: note Corepack isn't bundled on Node.js 25+ (install it, or install pnpm directly) since the supported range includes 25+. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/docs/src/content/docs/concepts/observability.mdx (2)
80-82: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winClarify “no cloud required.”
Cloudflare Pipeline → R2 is itself cloud-backed. If the intent is “no Lunora-managed cloud required,” state that explicitly to avoid promising local/offline durability.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/docs/src/content/docs/concepts/observability.mdx` around lines 80 - 82, Update the observability documentation text describing Cloudflare Pipeline → R2 to clarify that “no cloud required” means no Lunora-managed cloud or hosted service is required, not that storage is local or offline. Preserve the explanation that logs are durably stored in the user’s own Cloudflare account.
92-94: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDocument optional fields and conditional
waitUntil.The implementation only adds
fields,shardKey,userId,traceId, andspanIdwhen defined, and callswaitUntilonly when available. Replace “Each record carries” and “The send is registered” with conditional wording.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/docs/src/content/docs/concepts/observability.mdx` around lines 92 - 94, Update the observability documentation around the record-field description to state that fields, shardKey, userId, traceId, and spanId are included only when defined. Also describe waitUntil registration as conditional on its availability, rather than implying every request provides it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/docs/src/content/docs/concepts/observability.mdx`:
- Around line 40-44: Update the observability documentation around `onlyErrors`
to state that it applies only to sinks whose APIs support RPC filtering,
excluding `pipelineLogSink`. Keep the separate description of log-event
forwarding and `transformLog` behavior accurate, and do not imply that every
sink accepts `onlyErrors`.
---
Outside diff comments:
In `@apps/docs/src/content/docs/concepts/observability.mdx`:
- Around line 80-82: Update the observability documentation text describing
Cloudflare Pipeline → R2 to clarify that “no cloud required” means no
Lunora-managed cloud or hosted service is required, not that storage is local or
offline. Preserve the explanation that logs are durably stored in the user’s own
Cloudflare account.
- Around line 92-94: Update the observability documentation around the
record-field description to state that fields, shardKey, userId, traceId, and
spanId are included only when defined. Also describe waitUntil registration as
conditional on its availability, rather than implying every request provides it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 69c5b7f7-362a-4bf0-95c9-5c4883f033cd
⛔ Files ignored due to path filters (1)
packages/do/__tests__/shard-do.admin.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**
📒 Files selected for processing (3)
.github/CONTRIBUTING.mdapps/docs/src/content/docs/concepts/observability.mdxpackages/do/src/request-log.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- .github/CONTRIBUTING.md
- packages/do/src/request-log.ts
CodeRabbit review on #147: `pipelineLogSink` is log-only (`{ pipeline }`, no `onlyErrors`), so "every sink accepts onlyErrors" was inaccurate. Qualify it to sinks that carry RPC events. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The in-memory LogBuffer folded the seven ctx.log severities onto four tiers (trace→debug, log/info→info, fatal→error), so a line logged at trace or fatal was indistinguishable from debug or error in the Studio Logs panel — the two tiers #147 added were unreadable at the only place they surface locally. Buffer the level the caller actually logged at, widen the studio's mirrored LogLevel union to match, and source the panel's chip list + grouped summary from a new LOG_LEVEL_ORDER in the shared contract so the ordering has one home. Also drops a stale comment on otlpSink claiming trace correlation is "a later phase" — it shipped in #143 and the sink already reuses event.traceId/spanId. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0197KjhmBDB3PXAc6TXiPqZ9
…dispatch PR #143 propagated W3C trace context worker→shard→container and #147 stamped traceId/spanId onto every ctx.log line, but a handler still had no way to instrument a sub-operation: one SERVER span per dispatch was the entire worker trace surface. A slow request was one opaque bar. Adds ctx.trace(name, fn, attributes?) on Query/Mutation/ActionCtx. It returns the body's value unchanged and re-throws a failure after recording it as an error span — instrumentation, never flow control. Nesting is lexical, so the shape of the code is the shape of the waterfall. Design notes: - The span stack lives in a closure scoped to the ctx, NOT on `this` like the surrounding currentRequest* fields. Those are set-at-entry/cleared-at-exit and so are only sound across code that doesn't span an interleaving point, whereas a span stack is by definition held across the awaited body. Scoping it to the ctx makes concurrent dispatches structurally unable to corrupt each other's nesting. - The trace anchor IS resolved once per dispatch on `this`, so ctx.trace and the synthetic root span agree on the ids even with no inbound traceparent. - The root span is recorded only when the dispatch actually produced spans: minting one per request would fill the bounded ring with single-bar traces from uninstrumented handlers and evict the instrumented ones the panel exists for. It is not sent to onSpan — the runtime already emits the dispatch to onRpc, and a collector would otherwise show it twice. - foldTraces orders rows by (offset, depth), not arrival. Spans are recorded on completion, so a child is buffered before its parent, and at millisecond resolution the two routinely share a startTs — ordering has to come from the structure. The anchor is likewise picked structurally (the span whose parent is absent), since a fast parent/child pair is indistinguishable by timing. Caught by a test; both cases are now regression-covered. - The fold is total: the ring routinely holds partial traces (eviction can drop a parent, and a trace can be read before its root settles), so an orphan is re-parented onto the anchor rather than dropped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0197KjhmBDB3PXAc6TXiPqZ9
What & why
Enriches the framework's structured logging (
ctx.log) so an app produces rich, filterable, trace-linked logs that a log-management backend (the Cloud log viewer, or any OTLP collector) can use — all backward-compatible and fully usable with zero cloud dependency. This closes the framework side of the "full log management" gap.ctx.log, a full sink layer, and the OTLP-JSON wire contract already existed; this is enrichment, not greenfield.What's new
ctx.log.info(message, fields)and a chainablectx.log.with(fields)child logger. Fields become OTLP log-record attributes and ride the Workers-Logs console event (raw positional args still don't, preserving the PII boundary).trace(1) andfatal(21) soctx.logspans the whole OpenTelemetry range.traceId/spanId(from the inboundtraceparent), linking a line to its RPC span.pipelineLogSink— persists everyctx.logline to a Cloudflare Pipeline → R2 (queryable via R2 SQL) in the app's own account, no cloud required — the durable counterpart to the streamingotlpSink.webhookSink/sentrySinknow forward logs (onLog, withtransformLog/captureLogredactors); the dev-terminal formatter (CLI + Vite) renders fields + a shorttrace=suffix; the Studio Logs panel renders fields and searches on them.Behavior change
A two-arg call whose second argument is a plain object —
ctx.log.info("saved", user)— is now the structured form (message"saved",user→fields) instead of a joined message. Non-object second args, or 3+ args, are unchanged. Documented with a caution callout inconcepts/observability.mdx.Verification
posts:listdispatch emitted the fields on the console event with a populatedtrace_id/span_id.lint:typesacross all 59 projects; touched suites green (runtime, do, studio, config, container, testing).Thermo review pass (2 reviewers) — all findings fixed
Bugs:
bigint/circular infieldscrashinggetLogs(normalize to JSON-safe primitives at parse time), mutate-after-log aliasing (fresh snapshot), duplicate OTLP attribute keys (Map dedup, field wins), and a loud upgrade callout forwebhookSinkegress. Each with a regression test.Structure: consolidated three field-renderers into
shared/log-fields.tsand the hand-mirroredLogEvent/level-union intoshared/log-event.ts(bundler-inlined, no new dependency edge — same pattern asshared/otlp.ts), making the cross-packageonLogcall structurally guaranteed; moved the pureparseLogArgs/isLogFieldshelpers out of the 8k-lineshard-do.ts.🤖 Generated with Claude Code
Summary by CodeRabbit
ctx.logsupport with(message, fields)andwith(fields).pipelineLogSinkfor persisted application logs.captureLog(Sentry) andtransformLog(webhook).fatallikeerror).