OTLP metrics push: the zero-agent alternative
The Prometheus /metrics endpoint needs something to reach in: a scraper, a hostname it can hit, a credential in its configuration. OTLP push is the other direction. On every scheduler tick, if a destination is configured and enabled, the engine builds the same backup-health metrics into an OTLP/HTTP JSON body and posts it straight to your collector. Nothing on your side polls for it, and no agent runs anywhere: the engine is the client, your collector’s ingest endpoint is the server, exactly the shape forwarding the audit log to your SIEM (push) describes for the audit trail.
That direction matters for the same structural reason the audit push page gives. The engine dialling out is not an inbound request to your console hostname, so a Cloudflare Access perimeter in front of that hostname never sees it. That is an advantage over a scrape: the metrics page shows an Access perimeter turning an unattended /metrics scrape away at the edge unless you carve an exemption for it, whereas a push has nothing on that hostname for Access to gate at all. If your collector has no native Prometheus scrape support, or you would rather the engine reach out than expose a scrape target, push is the path that needs neither a scraper nor an Access exemption.
Setting up the destination
A destination is one endpoint, one auth header name and value, and whether it is enabled.
An Owner opens the metrics vendor's tile
In the console, open the Integrations screen and choose the tile for your metrics vendor (Datadog’s documentation states that its OTLP intake accepts JSON). The tile opens the OTLP push panel itself rather than a separately-titled section, so the endpoint, header and enable controls are what you see once it is open. They are Owner-gated at the panel: a non-owner gets an information banner in place of the form, saying that configuring the OTLP metrics push is owner only, so the gate is stated rather than silent.
Configure the destination
Supply the collector’s Endpoint URL (
https, screened the same way a webhook or SIEM push destination is: no private or loopback target, with no opt-in to allow one, and noworkers.devhost), the Auth header name your vendor expects (most commonlyAuthorization, or a vendor header such as Datadog’sDD-API-KEY), and the write-only auth secret. The secret field always starts blank, even when you are replacing an existing destination, because the engine never returns a stored secret. In the console, Replace needs a new secret, and Enable or Disable keeps the sealed one. Through the API, a blank value on a later set keeps the sealed secret, and only a first-ever create requires it.This set sits in the same high-blast-radius dual-control tier as setting the archive destination itself: once a second Owner exists on the account, saving queues a pending approval rather than repointing the egress immediately, and the console surfaces that as queued for a second owner rather than a false save (the full mechanics are at four-eyes change control).
Or configure it against the API
An Owner can set the same destination directly. This is the route the console calls, so the dual-control gate applies either way:
POST /admin/otlp-push HTTP/1.1 Host: console.example.com Content-Type: application/json { "enabled": true, "endpoint": "https://otlp.example.com/v1/metrics", "authHeaderName": "DD-API-KEY", "authHeaderValue": "<your vendor API key>" }GET /admin/otlp-pushreturns the redacted view (the endpoint, the header name, whether it is enabled, who set it and when, and the delivery trail); the header value never appears in a read.Enable it and wait for the next tick
There is no test-send for this destination, unlike the SIEM push and the generic notify-channel test send: the engine exposes no such route. The first real evidence it works is the delivery trail after the next scheduler tick, so enable it once you are confident in the endpoint and header, and check the trail shortly after.
Watch the delivery trail
The panel shows the most recent 50 attempts (an outcome, an HTTP status where one exists, a coarse reason on failure, and the number of downpipes actually shaped into that POST). It never carries a body, a URL beyond what you configured, or the secret.
Clearing the destination is Owner-only and is not dual-control gated, the same asymmetry every egress destination in this product follows: closing a path is the safe direction, so it needs no second approval. It is step-up gated. A cookie-borne session must give a fresh passkey assertion first, unless its passkey sign-in is less than five minutes old. And it is change-controlled, which is a different axis from dual control: with Require Change Number on, the clear asks for a change reference and the engine refuses it without one. Closing an egress at speed is exactly when you may have no CAB number to hand, so raise it as an Emergency Change with a justification rather than waiting on a ticket. See change control.
The metric set, as OTLP
The metric names and meanings are identical to the scrape endpoint’s, with one addition. Every metric is an OTLP gauge, rendered as one resourceMetrics entry naming the engine as the resource, one data point per downpipe (or per downpipe-plus-destination for the health series), timestamped to the moment of the push, and carrying the same downpipe_id and downpipe_name labels, with the same rename caveat that applies on the metrics page.
| Metric | Present on /metrics? |
|---|---|
downpipe_backup_last_success_timestamp_seconds | Yes |
downpipe_backup_success | Yes |
downpipe_backup_recent_attempts / _recent_successes / _recent_failures | Yes (the same windowed-gauge caveat applies; see the metrics page) |
downpipe_backup_duration_seconds | Yes |
downpipe_backup_size_bytes | Yes |
downpipe_destination_healthy | Yes |
downpipe_enabled | No. OTLP push is the only surface that carries this one: 1 for a live downpipe, 0 for one that is paused or decommissioned, so a consumer can gate a staleness alert on enabled == 1 and not page you about a downpipe you deliberately turned off. |
A metric with zero data points this tick (nothing configured has any destination-health state yet, say) is left out of the body entirely. The omission mirrors how a real exporter omits an empty series rather than shipping a metric family with nothing in it. The engine sends the snapshot in POSTs of at most 300 downpipes each, and records each POST as its own trail row. The whole push is capped at 5000 downpipes. Past that, each trail row records the actual count sent, flags the push as truncated and counts the downpipes left out.
Every tick is a fresh snapshot, not a cursor
Unlike the audit-log push, which cursors forward by sequence number so nothing is missed, the metrics push carries no cursor at all. Every tick reads the current state and sends a fresh snapshot; a tick that fails, or a collector that is briefly unreachable, is simply a gap in your own time series, never a backlog to catch up on and never a redelivery concern. Treat a missing data point the way you would treat a missed scrape: as a gap, not as lost history, because the engine never tries to backfill one.
The 15-minute cadence can trip a default no-data monitor
The push rides the engine’s own reconciliation cron, which ticks every fifteen minutes, so a fresh data point lands no more often than that. Plenty of monitoring vendors default a “no data” or “missing metric” monitor to a much shorter window, commonly five to ten minutes. The default is tuned for exporters that push far more often than this one does. Left at that default, the monitor can fire on every cycle, not because anything is actually unhealthy, but because it checked for freshness on a cadence tighter than the push’s own. Widen the no-data window to comfortably clear fifteen minutes, twenty to thirty gives headroom for a delayed tick. Alternatively, use the pull-based /metrics scrape instead, where your own scrape interval controls the cadence end to end and this mismatch cannot arise.
Which vendors’ OTLP intake actually accepts this
The engine’s OTLP push sends an OTLP/HTTP JSON body over a plain fetch() call, with no protobuf encoder, because the Worker carries no protobuf runtime. The OTLP/HTTP specification allows a server to accept either a binary protobuf body or a JSON-encoded one, but whether a given vendor’s own intake actually implements both is a per-vendor fact, not something this product can configure around.
| Vendor | Does its OTLP intake accept the JSON body this engine sends? |
|---|---|
| Datadog | Yes. Datadog’s documentation states its OTLP metrics intake accepts both HTTP protobuf and HTTP JSON. |
| New Relic | Unconfirmed. Test the push against your own New Relic account before you rely on it. |
| Dynatrace | No. Dynatrace’s OTLP ingest documentation states that JSON is not supported for its OTLP metrics API; only the binary protobuf encoding is accepted. |
| Elastic Observability | No. Elastic’s APM Server accepts OTLP/HTTP only as binary protobuf. JSON-encoded protobuf support is an open feature request against the project. |
| Splunk Observability Cloud | No. Its documented OTLP metrics endpoint accepts protobuf payloads only. |
Confirm this against your vendor's current documentation
Vendor intake behaviour can change. Where a vendor is marked “No” above, this engine’s push cannot reach its direct OTLP intake. Where New Relic is marked “Unconfirmed”, do not rely on this push reaching it without testing it yourself first. For any of those four, point that vendor at the Prometheus /metrics endpoint instead, which every one of them ingests through a Prometheus-compatible agent or scrape config regardless of this JSON-versus-protobuf question, or interpose an OpenTelemetry Collector configured to receive OTLP/HTTP JSON and export OTLP/HTTP protobuf, which bridges the gap without any change to the engine.
Egress security
The outbound call inherits every guard the alert webhook and the SIEM push carry: the endpoint must be https, a private, loopback, link-local or cloud-metadata address is refused at both configuration time and send time with no opt-in, a redirect is never followed, and the request carries a five second timeout. The engine does not read an error response body. On a success, it reads the body to find the OTLP partial-success count. It does not parse a body larger than 16 KiB, and the trail row gives that case its own reason.
When CONFIG_WRAP_KEY is set, the engine seals the auth header value at rest under that key. The seal carries its own domain-separation label, so a value sealed for another secret class cannot open as this one. The full screening model is described in securing notification webhooks; this destination is held to the same standard.
Where this fits
For the pull alternative this push exists alongside, read the Prometheus /metrics endpoint. For the audit-log push this feature’s egress hygiene and dual-control tier mirror, read forwarding the audit log to your SIEM (push). For the two-audience framing behind why this exists at all, read the monitoring overview. For the incident channels that turn a backup-health signal into an actual page, read incident channels.
Last updated .