Skip to content

feat: OTLP telemetry transport for workers and containers - #139

Merged
prisis merged 2 commits into
alphafrom
feat/observability-otlp
Jul 11, 2026
Merged

feat: OTLP telemetry transport for workers and containers#139
prisis merged 2 commits into
alphafrom
feat/observability-otlp

Conversation

@prisis

@prisis prisis commented Jul 11, 2026

Copy link
Copy Markdown
Member

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/runtimeotlpSink({ endpoint, token, headers, serviceName }) beside the existing sinks (consoleSink/webhookSink/sentrySink/analyticsEngineSink). Maps each RPC ObservabilityEvent → an OTLP span (POST {endpoint}/v1/traces) and each LogEvent → an OTLP log record (POST {endpoint}/v1/logs), fire-and-forget via context.waitUntil. token is a bearer-auth convenience applied last so it wins over any authorization in headers. endpoint/token are meant to be read from env so the platform can inject them at deploy with no app-code change. Honors onlyErrors.
  • @lunora/container/otelcreateContainerTelemetry(...): 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. Reads LUNORA_OTLP_ENDPOINT/LUNORA_OTLP_TOKEN/LUNORA_SERVICE_NAME from the container env; silent no-op (enabled === false) when no endpoint resolves. Injectable fetch for testing. Exposes trace() / emitSpan() / emitLog() / flush().
  • Docs — new concepts/observability page: the sink table, combineSinks, the otlpSink options, 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/spanId as lowercase hex (the documented OTLP/JSON exception), timeUnixNano as decimal-string nanos, attributes as the AnyValue union. Worker spans are kind: 2 (SERVER) with lunora.* attributes; container spans are kind: 1 (INTERNAL). Log severityNumber maps debug→5, info/log→9, warn→13, error→17. Full tables in the docs page.

Verification

  • @lunora/runtime499 passed (incl. new otlpSink well-formed-OTLP tests for ok/error/fan-out + token precedence).
  • @lunora/container127 passed (incl. 15 new otel.ts tests: disabled no-op, env resolution, ns precision, attribute kinds, severity mapping, header/token merge, trace() ok/error, error surfacing).
  • All changed files clean under eslint / tsc / prettier (and re-verified by the pre-commit hook).

Notes

  • Pure OSS, no cloud dependency — targets alpha.
  • Privacy: OTLP spans carry error.type/error messages and logs carry the rendered message (may contain user input); point endpoint only at a trusted collector, and use onlyErrors to narrow egress. Documented on the concept page.
  • Next: Phase 3 (cloud ingest POST /v1/telemetry + TelemetryStore adapter + hosted Issues/Incidents) stacks on the cloud branch (feat(cloud): lunora cloud control plane on workers for platforms #85), not on alpha.

🤖 Generated with Claude Code

https://claude.ai/code/session_018sRFb1136YE8KDmDbFMYmm

Summary by CodeRabbit

  • New Features

    • Added OTLP-over-HTTP export support for worker telemetry (RPC operations and application logs), including configurable endpoint, authentication, headers, optional service naming, and onlyErrors filtering.
    • Added container-side OTLP telemetry via a new public @lunora/container/otel entry point with createContainerTelemetry, including flush for in-flight exports.
  • Documentation

    • Added a new observability concepts page to the docs, detailing sink configuration, delivery behavior, OTLP JSON “wire contract,” and privacy considerations.
  • Chores

    • Updated container TypeScript configuration to avoid build/type issues.

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
@netlify

netlify Bot commented Jul 11, 2026

Copy link
Copy Markdown

Deploy Preview for lunorash ready!

Name Link
🔨 Latest commit 7f83ebb
🔍 Latest deploy log https://app.netlify.com/projects/lunorash/deploys/6a52724bd8701f000883b441
😎 Deploy Preview https://deploy-preview-139--lunorash.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for confirming the Contributor License Agreement! 🙏

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8f0b705a-a4eb-4ae1-adda-2c5eac85bee3

📥 Commits

Reviewing files that changed from the base of the PR and between a3314e9 and 7f83ebb.

⛔ Files ignored due to path filters (2)
  • packages/container/__tests__/otel.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • shared/otlp.ts is excluded by none and included by none
📒 Files selected for processing (4)
  • apps/docs/src/content/docs/concepts/observability.mdx
  • packages/container/src/otel.ts
  • packages/container/tsconfig.json
  • packages/runtime/src/observability-sinks.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/docs/src/content/docs/concepts/observability.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/runtime/src/observability-sinks.ts

Walkthrough

Adds 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.

Changes

OTLP observability

Layer / File(s) Summary
Worker OTLP sink
packages/runtime/src/observability-sinks.ts, packages/runtime/src/index.ts
Adds OTLP trace and log encoding, authentication, endpoint handling, onlyErrors filtering, fire-and-forget delivery, and public sink option exports.
Container telemetry exporter
packages/container/src/otel.ts, packages/container/package.json, packages/container/tsconfig.json
Adds createContainerTelemetry, OTLP encoding and delivery, disabled-mode no-ops, in-flight tracking, flushing, traced execution, and the public ./otel package export.
Shared observability contract
apps/docs/src/content/docs/concepts/observability.mdx, apps/docs/src/content/docs/meta.json
Documents worker and container setup, OTLP wire formats, span/log fields, delivery behavior, navigation, and privacy details.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

Worker OTLP export

sequenceDiagram
  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
Loading

Container telemetry export

sequenceDiagram
  participant ContainerCode
  participant createContainerTelemetry
  participant OTLPCollector
  ContainerCode->>createContainerTelemetry: trace or emit log
  createContainerTelemetry->>OTLPCollector: POST OTLP JSON
  ContainerCode->>createContainerTelemetry: flush pending exports
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed but does not follow the required template and omits the Summary, Linked issues, Test plan, Checklist, Notes, and CLA sections. Rewrite the PR description using the repository template and include all required sections, especially Summary, Linked issues, Test plan, Checklist, Notes for reviewers, and the CLA text.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: OTLP telemetry transport for workers and containers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/observability-otlp

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for following the naming conventions! 🙏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/container/src/otel.ts (1)

24-26: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Discard the response body before returning. fetch returns 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 and await res.body?.cancel() (or read it to completion) inside dispatch.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 81e9df8 and a3314e9.

⛔ Files ignored due to path filters (2)
  • packages/container/__tests__/otel.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/runtime/__tests__/observability-sinks.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
📒 Files selected for processing (6)
  • apps/docs/src/content/docs/concepts/observability.mdx
  • apps/docs/src/content/docs/meta.json
  • packages/container/package.json
  • packages/container/src/otel.ts
  • packages/runtime/src/index.ts
  • packages/runtime/src/observability-sinks.ts

Comment thread packages/container/src/otel.ts
@codspeed-hq

codspeed-hq Bot commented Jul 11, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 11.05%

⚡ 1 improved benchmark
✅ 159 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
baseline (Object.keys + toInternal + path spread per field) 72.3 µs 65.1 µs +11.05%

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-otlp (7f83ebb) with alpha (6805615)1

Open in CodSpeed

Footnotes

  1. No successful run was found on alpha (81e9df8) during the generation of this report, so 6805615 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant