Skip to content

[Feature] OTLP/HTTP JSON log sink — the universal integration (Loki, Datadog, SigNoz, Axiom, Honeycomb, collectors) #1154

Description

@pathosDev

Part of the multi-sink logging effort (meta issue below). Builds on the core pipeline and
BatchingSink issues.

Why this one first among the integrations

One OTLP/HTTP JSON sink reaches Grafana Loki ≥ 3.0 (/otlp/v1/logs), Parseable, SigNoz,
Datadog, Axiom, Honeycomb, New Relic and any OpenTelemetry Collector — so it covers most of
the integration matrix before a single vendor-specific sink exists. The OTLP spec declares
the logs signal Stable and specifies a JSON encoding as a first-class alternative to
binary protobuf, so no protobuf library is needed.

The official JS exporter is deliberately not taken as a dependency: it is still an
experimental 0.x package whose README warns that any release may break. Hand-rolling the
wire format against a stable spec is the lower-risk option, and it matches how the OTel
tracing and logs bridge adapters already work here.

Scope

src/logging/OtlpHttpSink.ts + options family, extends BatchingSink.

Body: ExportLogsServiceRequest in proto3 JSON — resourceLogs[].scopeLogs[].logRecords[],
lowerCamelCase keys, enum values as integers (the spec forbids the name strings).

  • timeUnixNano — a string, computed as (BigInt(timestampMs) * 1_000_000n).toString().
    A float multiply would exceed Number.MAX_SAFE_INTEGER and silently corrupt the low
    digits.
  • severityNumber: Debug → 5, Info → 9, Warn → 13, Error → 17, plus severityText.
  • body: { stringValue: message }.
  • attributes from source, displayName, fields and args, mapped to the typed
    stringValue / intValue / doubleValue / boolValue union.
  • resource attribute service.name, defaulting to the system name captured at attach.

Transport: bare global fetch (the accepted cross-runtime position here), guarded like
SseActor does, with an AbortController timeout (requestTimeoutMs, default 10 s) and an
injectable fetchFn?: typeof fetch option as the test seam. Optional gzip request body via
compressorFor('gzip') with Content-Encoding: gzip — servers must support it per spec.

Retry classification per the spec: 429, 502, 503, 504 only are retryable (honouring
Retry-After); every other non-2xx is non-retryable and the batch is dropped rather than
looped on.

Options: url (default http://localhost:4318/v1/logs), gzip, serviceName,
requestTimeoutMs, headers (code-only — credentials do not belong in HOCON, cf. #590 /
#592 / #741), delivery. HOCON block actor-ts.logger.sinks.otlp without headers.

Acceptance criteria

  • byte-exact request assertions against a fake fetchFn: URL, headers, and the full
    JSON body shape including nanosecond strings and integer severity numbers
  • BigInt nanosecond conversion covered by a test that would fail under float maths
  • retry classification: 429/502/503/504 retry, Retry-After honoured, 400 and 401 drop
    immediately
  • gzip path sets the header and produces a body the test can gunzip and compare
  • docs section in observability/logging/integrations (page created here), EN + DE,
    including the coverage table (which platforms this one sink reaches)
  • CHANGELOG entry
  • bun run typecheck + bun test green on every commit; coverage gate before merge

Reference

Part of #1150.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: highTop priority — high impact, plan next

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions