DATIUSDocs

OpenTelemetry

Datius also accepts telemetry over standard OTLP/HTTP — a second ingestion path alongside the Ingestion API, for teams that already instrument their services with an OpenTelemetry SDK. Point an existing exporter at Datius and it just works: no Datius-specific code, no vendor SDK, no OTel Collector required.

The trade-off for that "just works" simplicity is that OTLP telemetry doesn't arrive pre-labeled with a Datius signal key — an incoming metric or span is only a name and a shape (a number, a duration, a histogram of buckets). OTel mapping rules (configured under Settings → Workspace · OTel Mappings, not in code) tell Datius which metric/span names to route into which signals, and how to pull a value out of each one. Nothing arrives anywhere until at least one enabled rule matches it.

Endpoints

Two endpoints, one per OTLP signal type — both accept a standard ExportMetricsServiceRequest / ExportTraceServiceRequest body, JSON-encoded (OTLP/HTTP+JSON only — Protobuf isn't supported), authenticated the same way as the Ingestion API (an X-API-Key, api-key, or Authorization: Bearer header carrying a workspace-scoped key with the signals:emit scope).

  • POST /api/v1/otlp/v1/metrics — Gauge, Sum, Histogram, and Summary data points. (ExponentialHistogram isn't supported yet.)
  • POST /api/v1/otlp/v1/traces — a span's duration (end time − start time) becomes the signal value, timestamped at the span's end.

Both respond 202 Accepted for any structurally valid OTLP payload — per OTLP's own tolerance for partial acceptance, an unmapped metric/span name, a disabled rule, or a data point missing a required tag is silently dropped rather than rejected; a malformed body is the only 400. As with the Ingestion API, a missing/invalid key is 401 and a key without the signals:emit scope is 403.

Mapping rules

Every rule matches one exact OTel metric or span name (no wildcards) and is exactly one of two shapes — never both, never neither:

  • Scalar — a Gauge, a Sum, or any trace rule. One data point (or one span) becomes exactly one event. Pick a target signal — the value is read straight off the data point (OTLP's asDouble/asInt are a mutually exclusive pair, so whichever one is present is used automatically; nothing to configure).
  • Fan-out — a Histogram or Summary. One data point has no single value, so it fans out into one event per component you add, each with its own target signal:
ComponentApplies toProduces
countHistogram, SummaryThe data point's event count.
sumHistogram, SummaryThe sum of all recorded values.
min / maxHistogramOnly if the exporter recorded them — skipped (not an error) on data points that don't carry one.
bucketHistogramOne event per bucket boundary, tagged le=<bound> (Prometheus convention — the last, unbounded bucket is le=+Inf), with cumulative counts rather than Otel's raw per-bucket counts.
quantileSummary onlyMatches one quantileValues[] entry by its exact quantile value (e.g. 0.99) — a quantile the payload sends with no matching component is never emitted. Doesn't apply to Histogram: a histogram has no pre-computed quantiles, only bucket counts — use bucket instead and compute a percentile downstream, or configure your SDK to also export a Summary if you need one.

Each kind (and each distinct quantile) may appear at most once per rule. Any rule can also carry tag mappings — copy an OTel resource/data-point attribute into a Datius tag under a specific key (any string works — the target key just has to pass the target signal's own tag schema, exactly like a manually-ingested tag does) — but where they live depends on the rule's shape: a scalar rule's tag mappings sit on the rule itself, since it has exactly one target signal. A fan-out rule's tag mappings sit on each component instead, applied only to that component's own target — its components commonly target unrelated signals (one Histogram's count might feed an orders-count signal, its max a latency signal), so there's no single tag schema a rule-wide list could apply to.

Building a rule

In your workspace's Settings → Workspace · OTel Mappings tab: click New rule, pick a Signal type — Metric·Scalar, Metric·Fan-out, or Trace, one dropdown covering all three (a saved rule's signal type can't change afterward, since it's part of the rule's identity, but a metric rule can still switch between Scalar and Fan-out any time) — enter the exact OTel name to match, and pick target signal(s) — each must already exist as a real signal in the workspace; mapping rules never auto-create one. Before saving, paste a sample OTLP JSON export (a real one from your exporter, or a hand-written one) into the rule's Live Payload Debugger and click Test — it evaluates the rule exactly as drafted, without touching storage, and explains why anything didn't match (no rule enabled for that name, a component with no matching data, a tag-schema failure) instead of leaving you to guess from silence.

Node.js quickstart

Any language's OTel SDK works — the endpoints above are standard OTLP/HTTP+JSON, nothing Datius-specific to install. This example wires up a NodeSDK once on process boot and exports both metrics and traces:

Shell
npm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http @opentelemetry/sdk-metrics @opentelemetry/resources @opentelemetry/semantic-conventions
TypeScript
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import {
OTLPMetricExporter,
AggregationTemporalityPreference,
} from "@opentelemetry/exporter-metrics-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
// Exporter failures (bad endpoint, bad API key, network errors) are
// otherwise silent — this surfaces them as console errors instead of
// telemetry that just never arrives anywhere.
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR);
const endpoint = process.env.DATIUS_INGEST_ENDPOINT!; // e.g. https://api.datius.example
const headers = { "X-API-Key": process.env.DATIUS_API_KEY! };
const sdk = new NodeSDK({
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "my-service" }),
traceExporter: new OTLPTraceExporter({
url: `${endpoint}/api/v1/otlp/v1/traces`,
headers,
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: `${endpoint}/api/v1/otlp/v1/metrics`,
headers,
// Counter/Histogram default to *cumulative* temporality — every
// export re-reports the running total since process start, on a
// fixed timer, whether or not anything changed. Datius ingests
// every exported data point as its own new event, so an idle
// process would otherwise keep re-emitting the same ever-growing
// total forever, wildly overcounting anything that sums these
// events over time. Delta reports only the change since the last
// export instead — 0 while idle.
temporalityPreference: AggregationTemporalityPreference.DELTA,
}),
exportIntervalMillis: 5000,
}),
});
sdk.start();

Then use the standard OTel API anywhere in the process — no Datius import needed:

TypeScript
import { metrics, trace } from "@opentelemetry/api";
const tracer = trace.getTracer("my-service");
const meter = metrics.getMeter("my-service");
const ordersCounter = meter.createCounter("checkout.orders.count");
const paymentDuration = meter.createHistogram("checkout.payment.duration", { unit: "ms" });
await tracer.startActiveSpan("checkout_payment", async (span) => {
const startedAt = performance.now();
// ... charge the payment gateway ...
paymentDuration.record(performance.now() - startedAt, { "payment.method": "card" });
ordersCounter.add(1, { "payment.method": "card" });
span.end();
});

Map checkout.orders.count (Sum) and checkout.payment.duration (Histogram) with a mapping rule, and checkout_payment with a trace rule, and this code alone starts producing signal events — nothing else to wire up.

Try it

Every mapping rule's Live Payload Debugger (under Settings → Workspace · OTel Mappings) lets you paste a real OTLP export straight from your own exporter and see exactly what it would produce, without sending anything to storage — the fastest way to confirm a rule before flipping it on for real traffic. For the full request/response schema of the mapping CRUD API itself, see the API Reference; for the @datius/node SDK (the other ingestion path — a Datius-specific client rather than OTel), see SDKs & Code Examples.