Skip to content

Search docs

Jump between documentation pages.

Browse docs

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:

ts
import { App } from "@daloyjs/core";

const app = new App({
  telemetry: true,
});

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.

Push pipeline
  1. 01platform-injectedOTEL_* env varsendpoint, tenant headers, resource attributes
  2. 02logger + hookslog lines tee; http.server.request.duration per request
  3. 03batched exportersOTLP/HTTP JSON: /v1/logs and /v1/metrics
  4. 04collectorroutes on tenant headers, converts, forwards
  5. 05standard dashboardsGrafanasemconv names and buckets work unchanged
The logger write sink and the semconv HTTP hook feed two batched exporters. Both post OTLP/HTTP JSON to the collector named by the platform-injected environment variables; the collector fans out to Loki-style log stores and Mimir/Prometheus-style metric stores.

What gets exported

  • Logs: every line the app logger writes. JSON lines are decomposed into an OTLP record: msg / message / event becomes the body, level maps 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 attributes http.request.method (normalized to the well-known set, else _OTHER), http.route (the matched route template, e.g. /books/:id, from ctx.routePath), http.response.status_code, url.scheme, and error.type on 5xx. 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:

ts
const app = new App({
  telemetry: {
    logs: true, // tee the app logger to OTLP (default true)
    metrics: true, // semconv HTTP metrics (default true)
    exporter: {
      // all optional; defaults from OTEL_EXPORTER_OTLP_* env vars
      endpoint: "http://collector.internal:4318",
      headers: { tenant_id: "acme-prod" },
      resourceAttributes: { "service.name": "orders-api" },
      flushIntervalMs: 15_000,
    },
  },
});

// flush on demand (also flushed automatically on shutdown)
await app.telemetry?.flush();

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:

ts
import {
  createOtlpMetricsExporter,
  semconvHttpMetrics,
} from "@daloyjs/core/otlp";

const metrics = createOtlpMetricsExporter(); // null without an endpoint

// business counters with your own names and attributes
metrics?.count("orders_placed_total", { plan: "pro" });
metrics?.record("checkout_amount", { currency: "USD" }, 129.99, {
  unit: "1",
  boundaries: [10, 50, 100, 500, 1000],
});

// or install just the semconv HTTP hook on an existing app
const app = new App({ hooks: metrics ? semconvHttpMetrics(metrics) : {} });

Pull vs push, and tracing

  • app.metrics() (Prometheus) is the pull pillar: a scrape endpoint with Prometheus naming. Use it when something scrapes you. Use telemetry when a collector expects pushes. They coexist; the httpMetrics() route label also benefits from ctx.routePath now.
  • 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_*:

promql
# p95 latency per route
histogram_quantile(0.95, sum by (http_route, le) (
  rate(http_server_request_duration_seconds_bucket[5m])
))

# request volume by status
sum by (http_response_status_code) (
  rate(http_server_request_duration_seconds_count[5m])
)