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 |
error → retry_scheduled |
Yes |
| Any 5xx | error → retry_scheduled |
Yes |
| Network/DNS/TLS failure, or a timeout | error → retry_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:
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.