OTLP export (OpenTelemetry push)
Many container platforms run an in-cluster OpenTelemetry collector and expect workloads to push telemetry: they inject the standard OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_EXPORTER_OTLP_HEADERS / OTEL_RESOURCE_ATTRIBUTES / OTEL_SERVICE_NAME variables into every container and scrape nothing, not stdout and not a /metrics route. On such platforms, one App option turns on a dependency-free OTLP/HTTP pipeline:
That flag tees the app logger's output to the collector as OTLP logs and records http.server.request.duration per the OTel HTTP semantic conventions. With no endpoint configured it is a silent no-op, so it is safe to keep enabled in development. No OTel SDK, no loader hooks, no runtime dependencies.
- 01platform-injectedOTEL_* env varsendpoint, tenant headers, resource attributes
- 02logger + hookslog lines tee; http.server.request.duration per request
- 03batched exportersOTLP/HTTP JSON: /v1/logs and /v1/metrics
- 04collectorroutes on tenant headers, converts, forwards
- 05standard dashboardsGrafanasemconv names and buckets work unchanged
What gets exported
- Logs: every line the app logger writes. JSON lines are decomposed into an OTLP record:
msg/message/eventbecomes the body,levelmaps to the OTLP severity, and remaining fields ship as attributes (structured metadata in Loki-style backends). Non-JSON lines ship verbatim. http.server.request.duration: a histogram with the spec bucket boundaries and attributeshttp.request.method(normalized to the well-known set, else_OTHER),http.route(the matched route template, e.g./books/:id, fromctx.routePath),http.response.status_code,url.scheme, anderror.typeon5xx. Unmatched 404s record nothing (the 404 fast path builds no request context), so raw paths can never mint metric series.
Configuration
Everything defaults from the standard environment variables. Override per signal or per exporter:
The conventional injected endpoint is the gRPC form (http://host:4317); a trailing :4317 is rewritten to :4318, the collector's OTLP/HTTP port. Header values often carry multi-tenant routing credentials; DaloyJS never logs them.
Fail-safe by contract
- A dead or misconfigured collector never affects request serving: export errors are swallowed and counted in
droppedBatches. - The log queue is bounded (drop-oldest); metric series are capped and attribute values length-truncated, so hostile cardinality cannot grow memory.
- Metrics use cumulative temporality: totals survive a failed push and the next successful push carries them.
Standalone exporters
The pieces compose individually from @daloyjs/core/otlp for custom signals, for example domain-specific counters next to the built-in HTTP histogram:
Pull vs push, and tracing
app.metrics()(Prometheus) is the pull pillar: a scrape endpoint with Prometheus naming. Use it when something scrapes you. Usetelemetrywhen a collector expects pushes. They coexist; thehttpMetrics()route label also benefits fromctx.routePathnow.otelTracing()remains the tracing pillar (bring your own tracer). OTLP trace export is a planned follow-up.
Querying in Grafana
After the collector's Prometheus conversion the histogram appears as http_server_request_duration_seconds_*: