Skip to content
Browse documentation

Reliability

OpenTelemetry export

Send one span per delivery attempt and delivery success-rate and latency metrics to any OTLP/HTTP backend, without touching the delivery path.

Hooks can export delivery traces and metrics to any OpenTelemetry collector that speaks OTLP/HTTP JSON — Honeycomb, Grafana Alloy or the OpenTelemetry Collector, for example. Export is configured per workspace and is best effort: a collector outage never delays, retries or fails a webhook delivery.

Set Workspace → Delivery → Trace & metric export in the console, or PATCH the project:

PATCH /v1/project
Content-Type: application/json
{
"otelEndpoint": "https://api.honeycomb.io",
"otelHeaders": [
{ "name": "x-honeycomb-team", "value": "{{secret.HONEYCOMB_API_KEY}}" }
]
}
Field Description
otelEndpoint Base OTLP/HTTP URL. Hooks appends /v1/traces and /v1/metrics. Empty clears export.
otelHeaders Up to 20 headers sent with every export. Values may reference a secret as {{secret.NAME}}.

A pasted signal path (/v1/traces) is stripped, so either form works. Header values may use {{secret.NAME}}; the referenced project secret must exist when you save, and the value is decrypted only at export time. Literal header values are stored in plain text, so put credentials in secrets.

Saving requires the workspace owner or an admin session. The endpoint must be http or https.

One span per settled delivery attempt:

Span field Value
Name webhook.delivery.attempt
Kind CLIENT
traceId Stable per event: retries of the same event share one trace id
spanId Stable per delivery attempt
Attributes hooks.delivery_id, hooks.event_id, hooks.endpoint_id, hooks.destination_id, hooks.attempt, hooks.outcome, http.response.status_code, hooks.error_code
Status OK on success, ERROR on failed/dead, UNSET for retry/filtered
Events One per lifecycle stage (stage.action) with status and duration

hooks.outcome is one of succeeded, retry, failed, dead or filtered.

One export per workspace per queue batch, emitted after the batch is acknowledged:

Metric Type Notes
hooks.delivery.attempts Sum Settled attempts, split by the hooks.outcome attribute
hooks.delivery.duration Histogram Duration in ms, bounds 50 ms … 30 s
hooks.delivery.success_ratio Gauge succeeded / all settled attempts since the exporter started

The resource carries service.name = hooks-delivery and hooks.project_id, so a backend can group by workspace.

Counters and histograms use cumulative temporality: each export repeats the running total for the worker instance rather than a per-batch delta, because Prometheus’ OTLP receiver discards delta sums and histograms. A worker restart starts a new series at zero, which Prometheus records as a counter reset — values never jump backwards within a running instance. success_ratio therefore reflects the lifetime of the worker instance, not a single batch.

Only ids, timings, status codes, error codes and lifecycle stage names are exported. Payload bodies, destination URLs, request headers (except the configured OTLP headers) and secret values never leave the workspace.

Observations are collected in memory during one queue batch and exported after every message in the batch is acknowledged. If the collector is unreachable, times out (5 s) or rejects the request, Hooks logs a single otel.export_failed line containing the workspace id, the collector host and the reason, then moves on. No delivery is retried or delayed because of export.

The OTLP endpoint passes through the same SSRF guard as outbound destinations, so a workspace cannot point export at internal services. Self-hosted deployments that run a collector next to the Worker set OTEL_ALLOW_PRIVATE_ENDPOINTS=true in the deployment environment; the default is false.

  1. Save an endpoint that points at your collector.
  2. Send a test event to an endpoint with a destination.
  3. In the backend, filter on service.name = hooks-delivery: each attempt appears as a webhook.delivery.attempt span, and hooks.delivery.* metrics arrive within the next queue batch.

Sending to a collector that does not exist is safe: the delivery still succeeds and the console delivery log is unchanged.