Skip to content

Configuration and Payload Reference

The complete pg_relay.otlp contract: the channel config (actions.action), the notify() payload, how failures are classified, and the grants involved.

Channel config

Set at register()/update() time as the p_action argument — a JSON object, not SQL:

SELECT pgrelay.register('otel_metrics', '{"endpoint": "...", ...}', p_action_type := 'pg_relay.otlp');
SELECT pgrelay.update('otel_metrics', p_action := '{"endpoint": "...", ...}');
Field Type Required Default Notes
endpoint string yes The full URL the metric export request is POSTed to, e.g. http://localhost:4318/v1/metrics
service_name string no pg_relay Sent as the service.name resource attribute on every point from this channel
service_namespace string no Sent as the service.namespace resource attribute — see Resource attributes
deployment_environment string no Sent as the deployment.environment.name resource attribute (prod, staging, dev, …)
resource_attributes object no Any other resource attributes, key → value; see Resource attributes
auth object no See Auth below. Omitted entirely (not even an empty object) → no Authorization header sent at all
timeout_seconds integer no 30 (capped at 120) Per-send HTTP timeout

host.name is never configurable — every point carries this Processor instance's own OS hostname, automatically, the same way pg_relay.health answers report their host.

Resource attributes

OTLP splits a point's labels into two kinds, and backends treat them differently. Resource attributes describe the emitter — which service, in which namespace, in which environment, on which host — and are the same for everything that emitter sends. Data-point attributes (the payload's attributes) are dimensions of the measurement — which database, which shard. Convention-aware backends group and filter by the resource attributes: Grafana derives a service's job label as <service.namespace>/<service.name> (or just service.name when no namespace is set) and maps each service to an environment by deployment.environment.name; without that key, services can appear ungrouped or in the wrong environment.

Three conventional keys are first-class config fields, emitted under their OpenTelemetry semantic-convention names:

Config key Resource attribute emitted Notes
service_name service.name Default pg_relay. Avoid / — it is the job label separator
service_namespace service.namespace Groups related services. Avoid / for the same reason
deployment_environment deployment.environment.name The current convention key (semconv ≥ 1.27). The older deployment.environment is deprecated and pg_relay does not emit it

Anything else goes in resource_attributes — a flat object of key → value, typed from each JSON value exactly like data-point attributes (Attribute typing) and sorted by key on the wire. Typical entries: service.version, service.instance.id, cloud.provider, cloud.region. The four keys with dedicated handling — service.name, service.namespace, deployment.environment.name, and host.name — are rejected inside resource_attributes, so a free-form entry can never shadow a first-class field; a resource_attributes value that is not an object, or a nested value inside it, is a permanent error with no request sent.

_env:VAR_NAME references resolve in all of these, exactly as in auth — so "deployment_environment": "_env:DEPLOY_ENV" lets one channel definition follow the environment the Processor is deployed into, rather than hard-coding prod into the database:

SELECT pgrelay.register('otel_metrics',
    '{"endpoint": "http://alloy.internal:4318/v1/metrics",
      "service_name": "pg-collector",
      "service_namespace": "platform",
      "deployment_environment": "_env:DEPLOY_ENV",
      "resource_attributes": {"service.version": "1.5.0", "cloud.region": "ap-southeast-2"}}',
    p_action_type := 'pg_relay.otlp');

Auth

Identical vocabulary and behaviour to every other HTTP-speaking part of pg_relay (the webhook notification transport, the lifecycle-notification webhook path):

auth.style Required sub-fields What it does
bearer_header secret sets Authorization: Bearer <secret>
custom_header header_name, secret sets the named header to secret
basic_auth username, secret sets HTTP Basic authentication

auth.secret (and auth.username, where applicable) accept _env:VAR_NAME references — resolved from the Processor host's own environment immediately before the send, never cached, never stored in the database. An unset referenced variable is a permanent error naming the variable. A malformed or unrecognised auth.style, or a required sub-field missing for the style you chose, is likewise a permanent error, resolved before any request is sent.

The metric payload

The notify() payload is one gauge data point:

{"name": "pg_connections_active", "value": 42,
 "timestamp": "2026-09-01T10:00:00Z",
 "attributes": {"database": "prod", "shard_id": 3, "is_replica": true, "cpu_pct": 87.5}}
Field Type Required Notes
name string yes The OTLP metric name
value number yes A whole number maps to OTLP's asInt; any value with a fractional part maps to asDouble
timestamp string yes RFC 3339. The moment the measurement was taken — see Why timestamp is required
attributes object no Flat key/value labels attached to the data point

Attribute typing

Each attribute value's OTLP wrapper is inferred from its JSON type — there is no separate type field to set:

JSON value OTLP attribute Example
String stringValue "database": "prod"{"stringValue": "prod"}
Boolean boolValue "is_replica": true{"boolValue": true}
Number, no fractional part intValue (OTLP encodes 64-bit ints as strings) "shard_id": 3{"intValue": "3"}
Number, fractional part doubleValue "cpu_pct": 87.5{"doubleValue": 87.5}
Nested object or array rejected — permanent error naming the key

A missing name, value, or timestamp, an unparseable timestamp, or a nested attribute value is a permanent error (no request is sent to the receiver at all) — the audit row's error column names exactly what was wrong.

The OTLP envelope pg_relay builds

Given the config and payload above (with service_namespace: "platform", deployment_environment: "prod", and a service.version resource attribute), the request body POSTed to endpoint is the standard OTLP HTTP/JSON metrics export shape:

{
  "resourceMetrics": [{
    "resource": {"attributes": [
      {"key": "service.name", "value": {"stringValue": "pg-collector"}},
      {"key": "host.name", "value": {"stringValue": "pg18-prod-01"}},
      {"key": "service.namespace", "value": {"stringValue": "platform"}},
      {"key": "deployment.environment.name", "value": {"stringValue": "prod"}},
      {"key": "service.version", "value": {"stringValue": "1.5.0"}}
    ]},
    "scopeMetrics": [{
      "metrics": [{
        "name": "pg_connections_active",
        "gauge": {"dataPoints": [{
          "timeUnixNano": "1788256800000000000",
          "asInt": "42",
          "attributes": [
            {"key": "database", "value": {"stringValue": "prod"}},
            {"key": "shard_id", "value": {"intValue": "3"}},
            {"key": "is_replica", "value": {"boolValue": true}},
            {"key": "cpu_pct", "value": {"doubleValue": 87.5}}
          ]
        }]}
      }]
    }]
  }]
}

One resourceMetrics entry, one scopeMetrics, one metric, one gauge data point — every event maps to exactly one such request; pg_relay does not batch multiple queue rows into one OTLP request (each is a separate, independently retryable HTTP call).

Classification and retries

Condition Outcome Retried?
2xx response ok
Malformed payload or channel config (including a missing endpoint) error, permanent No — no request is even sent
3xx redirect error, permanent No — never followed, mirroring the webhook transport
Any other 4xx error, permanent No
429 Too Many Requests errorretry_scheduled Yes
Any 5xx errorretry_scheduled Yes
Network/DNS/TLS failure, or a timeout errorretry_scheduled Yes

Transient failures follow pg_relay's ordinary retry policy (3 seconds, then 5, then 10, up to the channel's max_retries) — the same chain every other channel uses; there is nothing OTLP-specific to configure. A dial/TLS/DNS failure's error text never includes the endpoint URL itself (which may carry a resolved _env: secret in a signed push URL) — it is stripped before reaching pgrelay.log.error.

Grants

pg_relay.otlp needs no new grant of any kind — a Processor already running pg_relay ≥ 1.1 picks up the whole feature on upgrade to 1.5 with zero operator action. The claim, config-read, audit, and retry sequence reuses the same operating-set functions every action type already relies on (_queue_claim, _fetch_action, _write_log, _queue_mark_done, _queue_insert_retry), so pgrelay.preflight() gains no new check. Registering and updating an otlp channel needs the same register/update grant as any other channel (pgrelay.grant_user(role)).

Monitoring

pgrelay.queue_stats() reports pg_relay.otlp exactly like any other action type — pending, processed, and error counts per channel within a trailing window:

SELECT * FROM pgrelay.queue_stats() WHERE action_type = 'pg_relay.otlp';

Old-binary note: a Processor running a version that predates pg_relay.otlp (pre-1.5) routes an otlp row to process_one(), which resolves it as unsupported_action_type — one audit row per occurrence, never a retry loop. Upgrade the binary to start sending.


See Building a Metric Document for the developer's step-by-step guide to assembling a payload, Wiring Up Grafana Alloy for a worked receiver setup, or back to the Overview.