feat: OTLP telemetry transport for workers and containers - #139
Conversation
Phase 2 of the observability plan: worker and container telemetry now
leave Lunora over one standard protocol — OTLP-over-HTTP/JSON — so any
OpenTelemetry collector (or the Lunora cloud ingest) accepts both.
- runtime: add `otlpSink({ endpoint, token, headers, serviceName })`
beside the existing sinks. Maps each RPC `ObservabilityEvent` to an
OTLP span and each `LogEvent` to an OTLP log record, POSTed
fire-and-forget to `{endpoint}/v1/traces` and `/v1/logs`. `token` is a
bearer convenience applied last so it wins over any `authorization`
header.
- container: add `@lunora/container/otel` — a zero-dependency,
zero-config exporter (`createContainerTelemetry`) speaking the exact
same wire contract from inside a container process. Silent no-op when
no endpoint resolves.
- docs: add the `concepts/observability` page documenting the sinks and
the single wire contract both paths share.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sRFb1136YE8KDmDbFMYmm
✅ Deploy Preview for lunorash ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Thank you for confirming the Contributor License Agreement! 🙏 |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughAdds OTLP-over-HTTP telemetry export for worker RPC and log events plus container spans and logs, exposes the related APIs, and documents configuration, payload conventions, delivery lifecycle, and privacy details. ChangesOTLP observability
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)Worker OTLP exportsequenceDiagram
participant Runtime
participant otlpSink
participant OTLPCollector
Runtime->>otlpSink: receive RPC or log event
otlpSink->>OTLPCollector: POST OTLP JSON to /v1/traces or /v1/logs
otlpSink->>Runtime: register delivery with waitUntil
Container telemetry exportsequenceDiagram
participant ContainerCode
participant createContainerTelemetry
participant OTLPCollector
ContainerCode->>createContainerTelemetry: trace or emit log
createContainerTelemetry->>OTLPCollector: POST OTLP JSON
ContainerCode->>createContainerTelemetry: flush pending exports
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
|
Thank you for following the naming conventions! 🙏 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/container/src/otel.ts (1)
24-26: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDiscard the response body before returning.
fetchreturns a response whose body is never read here; on Node/undici that can keep the socket occupied and delay keep-alive reuse. Capture the response andawait res.body?.cancel()(or read it to completion) insidedispatch.🤖 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/container/src/otel.ts` around lines 24 - 26, Update the dispatch function to capture the fetch response and cancel or fully consume res.body before returning, while preserving the existing settlement and status handling. Use the OtelFetchLike response contract and ensure body cleanup occurs for every response.
🤖 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 `@packages/container/src/otel.ts`:
- Around line 292-322: Add an abort/timeout option to OtelFetchLike and use it
in send’s dispatch request so each OTLP POST is bounded even when the collector
hangs. Create and apply an AbortSignal for the configured timeout when calling
post, while preserving existing onError handling and inflight cleanup; ensure
flush/shutdown can complete after the timeout.
---
Nitpick comments:
In `@packages/container/src/otel.ts`:
- Around line 24-26: Update the dispatch function to capture the fetch response
and cancel or fully consume res.body before returning, while preserving the
existing settlement and status handling. Use the OtelFetchLike response contract
and ensure body cleanup occurs for every response.
🪄 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: c2393db1-19cd-4b7c-b2b5-8a7350f77db8
⛔ Files ignored due to path filters (2)
packages/container/__tests__/otel.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/runtime/__tests__/observability-sinks.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**
📒 Files selected for processing (6)
apps/docs/src/content/docs/concepts/observability.mdxapps/docs/src/content/docs/meta.jsonpackages/container/package.jsonpackages/container/src/otel.tspackages/runtime/src/index.tspackages/runtime/src/observability-sinks.ts
Merging this PR will improve performance by 11.05%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes |
Extract the OTLP-over-HTTP/JSON wire primitives the worker `otlpSink` (`@lunora/runtime`) and the container exporter (`@lunora/container`) duplicated into `shared/otlp.ts`, bundler-inlined into both `dist` (like `shared/batch-wire.ts`) so the AnyValue/KeyValue encoding, SeverityNumber map, `*UnixNano`/hex-id encoders, case-insensitive header merge, and the `resourceSpans`/`resourceLogs` envelopes live in one contract instead of two drifting mirrors. No public API change to either package. Fold in the correctness fixes the thermo review surfaced on the container exporter: - Report a non-2xx collector response via `onError` — a bad token, wrong base path, or 5xx is a real send failure, and `onError` is the container's only telemetry feedback channel. - Isolate the response-body `cancel()` in its own try/catch so a rejecting `cancel()` (best-effort socket release) is never misreported as a send failure. - Encode non-finite (`NaN`/±Infinity) attribute values as a string fallback and values beyond 2^53 as `doubleValue`, so a strict collector never rejects a `null` or a `1e+21` int64 decimal string. - Gate the enabled check at `emitSpan`/`emitLog` so the disabled path never builds a body or touches `fetch`. Cover the non-2xx-onError and non-finite/unsafe-integer encoding paths with tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01423xZDDqzhQ5D79vy25huF
Phase 2 — OTLP transport + wire contract (OSS)
Second phase of the observability plan (superlog → Lunora). Phase 1 shipped in #138. This phase gives worker and container telemetry a single, standard egress protocol — OTLP-over-HTTP/JSON — so any OpenTelemetry collector (or, later, the Lunora cloud ingest) accepts both without special-casing.
What's here
@lunora/runtime—otlpSink({ endpoint, token, headers, serviceName })beside the existing sinks (consoleSink/webhookSink/sentrySink/analyticsEngineSink). Maps each RPCObservabilityEvent→ an OTLP span (POST {endpoint}/v1/traces) and eachLogEvent→ an OTLP log record (POST {endpoint}/v1/logs), fire-and-forget viacontext.waitUntil.tokenis a bearer-auth convenience applied last so it wins over anyauthorizationinheaders.endpoint/tokenare meant to be read from env so the platform can inject them at deploy with no app-code change. HonorsonlyErrors.@lunora/container/otel—createContainerTelemetry(...): a zero-dependency, zero-config OTLP exporter for code running inside a container (a separate process, can't use a worker sink). Speaks the exact same wire contract, so container spans land next to worker spans in one collector. ReadsLUNORA_OTLP_ENDPOINT/LUNORA_OTLP_TOKEN/LUNORA_SERVICE_NAMEfrom the container env; silent no-op (enabled === false) when no endpoint resolves. Injectablefetchfor testing. Exposestrace()/emitSpan()/emitLog()/flush().concepts/observabilitypage: the sink table,combineSinks, theotlpSinkoptions, the container exporter, and the single wire contract both paths share (transport, endpoints, headers, encoding rules, span/attribute/log-record shapes + severity mapping) plus a privacy note.Wire contract (summary)
OTLP-over-HTTP with JSON encoding.
traceId/spanIdas lowercase hex (the documented OTLP/JSON exception),timeUnixNanoas decimal-string nanos, attributes as theAnyValueunion. Worker spans arekind: 2(SERVER) withlunora.*attributes; container spans arekind: 1(INTERNAL). LogseverityNumbermaps debug→5, info/log→9, warn→13, error→17. Full tables in the docs page.Verification
@lunora/runtime— 499 passed (incl. newotlpSinkwell-formed-OTLP tests for ok/error/fan-out + token precedence).@lunora/container— 127 passed (incl. 15 newotel.tstests: disabled no-op, env resolution, ns precision, attribute kinds, severity mapping, header/token merge,trace()ok/error, error surfacing).Notes
alpha.error.type/error messages and logs carry the rendered message (may contain user input); pointendpointonly at a trusted collector, and useonlyErrorsto narrow egress. Documented on the concept page.POST /v1/telemetry+TelemetryStoreadapter + hosted Issues/Incidents) stacks on the cloud branch (feat(cloud): lunora cloud control plane on workers for platforms #85), not onalpha.🤖 Generated with Claude Code
https://claude.ai/code/session_018sRFb1136YE8KDmDbFMYmm
Summary by CodeRabbit
New Features
onlyErrorsfiltering.@lunora/container/otelentry point withcreateContainerTelemetry, includingflushfor in-flight exports.Documentation
Chores