Wiring Up Grafana Alloy¶
Grafana Alloy is Grafana's telemetry collector — the modern replacement for the Grafana Agent, built around the same OpenTelemetry Collector core used everywhere else in the OTel ecosystem. It accepts OTLP over HTTP or gRPC and forwards whatever it receives to wherever your metrics actually live: a self-hosted Prometheus or Mimir, Grafana Cloud, or another downstream OTLP receiver. pg_relay talks to Alloy exactly the way it talks to any other OTLP receiver — this page is the worked example, using Alloy because it is the most common target, not because pg_relay has any special integration with it.
What Alloy needs configured¶
Alloy's configuration language is River. The minimum pipeline for pg_relay's metrics is three components: an OTLP receiver, a batch processor, and something to forward the batched metrics onward. A typical config.alloy looks like this:
// Accept OTLP over HTTP (and gRPC, if you want it) on the usual ports.
otelcol.receiver.otlp "pg_relay" {
http {
endpoint = "0.0.0.0:4318"
}
grpc {
endpoint = "0.0.0.0:4317"
}
output {
metrics = [otelcol.processor.batch.pg_relay.input]
}
}
// Batch points instead of forwarding one at a time — standard practice for
// any OTLP pipeline, pg_relay's traffic included.
otelcol.processor.batch "pg_relay" {
output {
metrics = [otelcol.exporter.prometheus.pg_relay.input]
}
}
// Convert OTLP metrics into Prometheus's data model...
otelcol.exporter.prometheus "pg_relay" {
forward_to = [prometheus.remote_write.mimir.receiver]
}
// ...and push them to wherever you actually store metrics (Mimir here;
// swap the URL for Grafana Cloud's remote_write endpoint, or a plain
// Prometheus instance's, without changing anything upstream).
prometheus.remote_write "mimir" {
endpoint {
url = "http://mimir:9009/api/v1/push"
}
}
Exact component names and available options move between Alloy releases — treat the shape above as the reference architecture (otelcol.receiver.otlp → otelcol.processor.batch → otelcol.exporter.prometheus → prometheus.remote_write) and check your installed Alloy version's own documentation for the current field names.
Pointing pg_relay at it¶
Alloy's OTLP HTTP receiver listens on port 4318 by default and exposes the standard OTLP paths — /v1/metrics for metrics. Register a channel whose endpoint is that path on your Alloy host:
SELECT pgrelay.register('otel_metrics',
'{"endpoint": "http://alloy.internal:4318/v1/metrics",
"service_name": "pg-collector",
"service_namespace": "platform",
"deployment_environment": "prod"}',
p_action_type := 'pg_relay.otlp');
If the Processor and Alloy run on the same host — a common setup — localhost (or Alloy's container/pod name, under Docker or Kubernetes) works instead of a hostname.
service_namespace and deployment_environment are optional, but worth setting from the start if Grafana is the destination: Alloy forwards them untouched as the service.namespace and deployment.environment.name resource attributes, and it is Grafana that acts on them — the Prometheus exporter and Grafana Cloud both derive the job label as platform/pg-collector, and Grafana Cloud's Application Observability uses the environment key to file the service under prod rather than leaving it ungrouped. See Resource attributes for the full set, including the free-form resource_attributes object for service.version and friends.
Sending a test point¶
SELECT pgrelay.notify('otel_metrics',
'{"name": "pg_connections_active", "value": 42,
"timestamp": "2026-09-01T10:00:00Z",
"attributes": {"database": "prod"}}');
Within about a second, check pg_relay's own audit trail first — it tells you whether the send succeeded, independent of whatever Alloy does with the point afterwards:
SELECT status, error, elapsed_ms FROM pgrelay.log WHERE channel = 'otel_metrics' ORDER BY actioned_at DESC LIMIT 1;
status = 'ok' means Alloy's receiver answered with a 2xx — the point is in Alloy's pipeline. From there, Alloy's own debug tooling takes over: otelcol.exporter.prometheus publishes what it has received on Alloy's built-in Prometheus scrape endpoint (http://alloy.internal:12345/metrics by default), or query it directly in your downstream store once prometheus.remote_write has pushed it — pg_connections_active{database="prod"} should appear within a scrape interval or two. If nothing shows up on the Alloy side but pg_relay's own audit row says ok, the problem is downstream of Alloy's receiver (the batch/exporter/remote_write chain), not in pg_relay.
Authenticating to Alloy¶
A local, network-isolated Alloy instance is usually left unauthenticated at the receiver — leave auth out of the channel config entirely, and pg_relay sends no Authorization header. If your Alloy deployment sits behind a reverse proxy or gateway that expects a bearer token, add an auth block (the same shape used by every other HTTP endpoint pg_relay talks to):
SELECT pgrelay.register('otel_metrics',
'{"endpoint": "https://alloy.example.com/v1/metrics",
"service_name": "pg-collector",
"auth": {"style": "bearer_header", "secret": "_env:ALLOY_GATEWAY_TOKEN"}}',
p_action_type := 'pg_relay.otlp');
auth.secret accepts an _env:VAR_NAME reference, resolved from the Processor host's own environment at send time — the token never sits in the database. See the Configuration and Payload Reference for the full auth shape (bearer_header, custom_header, basic_auth).
Sending straight to Grafana Cloud instead¶
If you don't run Alloy yourself, Grafana Cloud exposes its own OTLP gateway that accepts metrics directly — same wire format, no local collector needed:
SELECT pgrelay.register('otel_metrics_cloud',
'{"endpoint": "https://otlp-gateway-prod-au-southeast-0.grafana.net/otlp/v1/metrics",
"service_name": "pg-collector",
"auth": {"style": "basic_auth", "username": "<your instance ID>", "secret": "_env:GRAFANA_CLOUD_API_KEY"}}',
p_action_type := 'pg_relay.otlp');
Grafana Cloud's OTLP gateway authenticates with HTTP Basic auth: your Grafana Cloud instance ID as the username, and an API key (with the metrics:write scope) as the password. The exact gateway hostname is specific to your Grafana Cloud stack and region — copy it from your stack's "Send metrics via OTLP" page in the Grafana Cloud portal rather than assuming the example above.
Troubleshooting¶
status = 'error'inpgrelay.log, with a 4xx in the message — a configuration problem: a wrong path (missing/v1/metrics), a rejected/missing auth token, or a malformed payload. pg_relay never retries this class of failure (see Classification and retries) — fix the channel config or payload and send again.status = 'retry_scheduled', then eventually'error'— Alloy was unreachable or returned a 5xx/429 across every retry attempt. Check that Alloy is actually running and that the Processor host can reach it (curl -X POST <endpoint> -d '{}'from the Processor host is the fastest sanity check — Alloy will reject the empty body but a connection-level failure looks very different from a 4xx).- pg_relay says
ok, but nothing shows up in Grafana — the point reached Alloy's receiver; look downstream from there (the batch processor's queue, the exporter's conversion, theremote_writepush) using Alloy's own UI/logs, not pg_relay's. - Timestamps look wrong — remember
timestampin the payload is the measurement time you supplied, not send time (see why this is required). If your producing function computesnow()well before thenotify()call executes, that gap — not queue delay — is what you're seeing.
Continue to the Configuration and Payload Reference for the complete contract, or to Building a Metric Document for the developer's step-by-step guide to the payload.