OpenTelemetry Collector for PostgreSQL
Kloudfuse can receive PostgreSQL metrics collected by the OpenTelemetry Collector postgresqlreceiver.
The receiver connects directly to PostgreSQL over TCP, queries the pg_stat_* system views, and exports the results to Kloudfuse via the OTLP HTTP exporter.
This approach works in any environment — Kubernetes, virtual machines, or bare metal — without requiring the Kloudfuse observability agent.
Overview
The postgresqlreceiver is part of the OpenTelemetry Collector Contrib distribution (otel/opentelemetry-collector-contrib).
You configure it with a host, port, and credentials, and it periodically connects to PostgreSQL to query:
-
pg_stat_database— connections, commits, rollbacks, deadlocks, row activity, block I/O -
pg_stat_bgwriter— checkpoint and background writer activity -
pg_stat_replication— replication lag (primary only) -
pg_stat_statements— per-query execution statistics (requires the extension)
The collector emits these as OTLP metrics and forwards them to the Kloudfuse ingestion endpoint, where they are stored as time-series data queryable via PromQL and FuseQL.
Prerequisites
Enable pg_stat_statements
Add pg_stat_statements to shared_preload_libraries in postgresql.conf:
shared_preload_libraries = 'pg_stat_statements'
If you manage PostgreSQL with Helm (for example, Bitnami’s postgresql chart):
postgresql:
primary:
postgresqlSharedPreloadLibraries: "pg_stat_statements"
Restart PostgreSQL, then create the extension in each database to monitor:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
Create a Monitoring User
Create a dedicated low-privilege user for the collector:
CREATE USER kfmon WITH PASSWORD '<password>' CONNECTION LIMIT 20;
GRANT pg_monitor TO kfmon; -- PostgreSQL 10+
GRANT SELECT ON pg_stat_database TO kfmon;
GRANT SELECT ON pg_stat_statements TO kfmon;
For PostgreSQL 9.6 and earlier, replace the pg_monitor role with the explicit grants listed in the Datadog pre-flight steps (the required grants are the same regardless of which collector you use).
On PostgreSQL 14+, also grant access to the statements info view:
GRANT SELECT ON pg_stat_statements_info TO kfmon;
Set CONNECTION LIMIT to at least 20 (not 5). The postgresqlreceiver opens one connection per database per metric query, so monitoring 4 databases requires up to ~24 concurrent connections on each collection cycle. A limit of 5 causes too many connections for role errors on every scrape.
|
Ensure pg_hba.conf allows connections from the collector host or Kubernetes pod CIDR using md5 or scram-sha-256 authentication.
Label Enrichment
How Labels Are Attached
The postgresqlreceiver connects directly to PostgreSQL over TCP.
It emits one metric series per database with a small set of receiver-provided labels:
| Label | Source |
|---|---|
|
Database name from |
|
Set via the |
|
Set automatically to |
Unlike infrastructure agents that run as a DaemonSet on the same node as PostgreSQL, the OTel Collector does not observe the pod directly — it opens a TCP connection to a service DNS name.
This means the k8sattributes processor cannot automatically enrich these metrics with Kubernetes pod labels (kube_namespace, kube_service, pod_name, etc.) because the connection source IP belongs to the collector pod, not the PostgreSQL pod.
Adding Kubernetes Labels Manually
Since the PostgreSQL pod’s metadata is stable and known, add it explicitly via the resource processor.
This achieves label parity with agent-based collectors (such as the Datadog Agent) that attach Kubernetes metadata automatically:
processors:
resource:
attributes:
- key: service.name
value: <postgresql-service> # e.g. kfuse-configdb
action: upsert
- key: db.system
value: postgresql
action: upsert
# Kubernetes identity labels — set to match the target StatefulSet/Deployment
- key: env
value: <environment> # e.g. prod
action: upsert
- key: kube_namespace
value: <postgresql-namespace>
action: upsert
- key: kube_service
value: <postgresql-service>
action: upsert
- key: kube_stateful_set
value: <postgresql-statefulset> # if deployed as a StatefulSet
action: upsert
- key: host
value: <postgresql-service>.<postgresql-namespace>.svc.cluster.local
action: upsert
- key: port
value: "5432"
action: upsert
- key: replication_role
value: master # or replica
action: upsert
- key: postgresql_version
value: "<major.minor>" # e.g. 14.18
action: upsert
The k8sattributes processor enriches metrics using the source pod IP of incoming data.
Because the postgresqlreceiver makes outbound TCP connections (not inbound scrapes), the processor sees the collector pod’s IP, not the PostgreSQL pod’s IP.
Pod label discovery does not apply to TCP-based receivers — only to receivers that accept inbound data from instrumented pods (such as the OTLP receiver or the Prometheus receiver in pull mode).
|
Configure the Collector
Collector Configuration File
The following config.yaml configures the postgresqlreceiver and the otlphttp exporter pointed at Kloudfuse:
receivers:
postgresql:
endpoint: <postgresql-host>:5432
username: kfmon
password: <password>
databases:
- <database-name> # list each database to monitor; omit to monitor all
tls:
insecure: true # set to false and add ca_file for TLS connections
collection_interval: 60s
processors:
batch:
timeout: 10s
resource:
attributes:
- key: service.name
value: postgresql
action: upsert
- key: db.system
value: postgresql
action: upsert
# Add any static labels needed for filtering — see the Label Enrichment section
# for why k8sattributes cannot be used with the postgresqlreceiver.
exporters:
otlphttp:
metrics_endpoint: https://<kloudfuse-hostname>/ingester/otlp/metrics
tls:
insecure: false
service:
pipelines:
metrics:
receivers: [postgresql]
processors: [resource, batch]
exporters: [otlphttp]
Replace the following placeholders:
| Placeholder | Value |
|---|---|
|
Hostname or service name of the PostgreSQL instance |
|
Password for the monitoring user |
|
One or more database names (one entry per database) |
|
External hostname of your Kloudfuse cluster |
Deploy on Kubernetes (Helm)
-
Add the OpenTelemetry Helm repository:
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts helm repo update -
Store the PostgreSQL password in a Kubernetes secret:
kubectl create secret generic otel-postgres-credentials \ --from-literal=password='<password>' \ -n <collector-namespace> -
Create a
values.yamlfor the collector. The password is injected via an environment variable to avoid embedding it in the config:image: repository: otel/opentelemetry-collector-contrib mode: deployment extraEnvs: - name: PG_PASSWORD valueFrom: secretKeyRef: name: otel-postgres-credentials key: password config: receivers: postgresql: endpoint: <postgresql-service>.<postgresql-namespace>.svc.cluster.local:5432 username: kfmon password: ${env:PG_PASSWORD} databases: - <database-name> tls: insecure: true collection_interval: 60s processors: batch: timeout: 10s resource: attributes: - key: service.name value: <postgresql-service> action: upsert - key: db.system value: postgresql action: upsert # Kubernetes identity — set to match the target pod metadata - key: env value: <environment> action: upsert - key: kube_namespace value: <postgresql-namespace> action: upsert - key: kube_service value: <postgresql-service> action: upsert - key: host value: <postgresql-service>.<postgresql-namespace>.svc.cluster.local action: upsert - key: port value: "5432" action: upsert exporters: otlphttp: metrics_endpoint: https://<kloudfuse-hostname>/ingester/otlp/metrics service: pipelines: metrics: receivers: [postgresql] processors: [resource, batch] exporters: [otlphttp]yaml -
Install the collector:
helm upgrade --install otel-postgres open-telemetry/opentelemetry-collector \ -f values.yaml \ -n <collector-namespace> -
Confirm the pod is running:
kubectl get pods -n <collector-namespace> -l app.kubernetes.io/name=opentelemetry-collector
Log Collection (Optional)
To collect PostgreSQL container logs on Kubernetes, add a filelog receiver.
When PostgreSQL writes logs to stdout (the default for most Kubernetes deployments), configure the receiver to read from the pod log path:
receivers:
filelog:
include:
- /var/log/pods/<postgresql-namespace>_<pod-name-prefix>*/**/*.log
operators:
- type: regex_parser
regex: '^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+) \w+ (?P<severity>\w+): (?P<message>.*)'
timestamp:
parse_from: attributes.timestamp
layout: '%Y-%m-%d %H:%M:%S.%f'
severity:
parse_from: attributes.severity
Add logs_endpoint to the exporter and a logs pipeline:
exporters:
otlphttp:
metrics_endpoint: https://<kloudfuse-hostname>/ingester/otlp/metrics
logs_endpoint: https://<kloudfuse-hostname>/ingester/otlp/v1/logs
service:
pipelines:
metrics:
receivers: [postgresql]
processors: [resource, batch]
exporters: [otlphttp]
logs:
receivers: [filelog]
processors: [resource, batch]
exporters: [otlphttp]
Verify Metrics Are Arriving
After deploying the collector, confirm metrics are arriving in the Kloudfuse UI:
-
Click the Metrics tab, then select Explorer from the drop-down menu.
-
Set the time range to the last 15 minutes using the interval picker in the top-right corner.
-
In the metric selector, type
postgresql_backendsand select it from the suggestions list. Confirm a chart appears with data points. -
Use the label filters to narrow results by database — for example, filter by
db="mydb"to isolate metrics for a specific database.
Key metrics emitted by the postgresqlreceiver:
| Metric (PromQL name) | Description |
|---|---|
|
Number of active client connections (backends) |
|
Transactions committed per interval |
|
Transactions rolled back per interval |
|
Database size in bytes |
|
Deadlock count per interval |
|
Rows returned, fetched, inserted, updated, deleted (labelled by |
|
Blocks read from disk vs buffer cache (labelled by |
|
Buffers allocated by the background writer |
|
Number of checkpoints performed (labelled by |
|
Replication lag in bytes (primary only) |
To verify logs are arriving (when log collection is enabled), click the Logs tab, filter by source postgresql in the Filters panel, and confirm log entries appear in the list.
Troubleshooting
No Metrics in Kloudfuse
If postgresql_backends returns no data:
-
Check the collector pod logs for scrape errors:
kubectl logs -n <collector-namespace> -l app.kubernetes.io/name=opentelemetry-collectorLook for
"Error scraping postgresql"or"connection refused"messages. -
Confirm the collector can reach the PostgreSQL service:
kubectl exec -n <collector-namespace> <collector-pod-name> -- \ nc -zv <postgresql-service>.<postgresql-namespace>.svc.cluster.local 5432 -
Verify the exporter is successfully reaching Kloudfuse by checking for
"Exporting failed"in the logs.
Too Many Connections Error
If the collector logs show pq: too many connections for role "kfmon" (53300) on every scrape:
The postgresqlreceiver opens a separate connection for each database per metric query.
Monitoring 4 databases requires up to ~24 concurrent connections per scrape cycle.
Increase the connection limit for the monitoring user:
ALTER USER kfmon CONNECTION LIMIT 20;
A limit of 5 is insufficient when monitoring more than one database.
Authentication Errors
If the collector logs show FATAL: password authentication failed for user "kfmon":
-
Confirm the password in the Kubernetes secret matches the PostgreSQL user password.
-
Verify the user exists:
SELECT usename FROM pg_user WHERE usename='kfmon'; -
Confirm
pg_hba.confallows connections from the collector pod CIDR.
Statement Metrics Are Missing
-
Confirm
pg_stat_statementsis inshared_preload_librariesand PostgreSQL was restarted after the change. -
Run
SELECT * FROM pg_stat_statements LIMIT 1;as the monitoring user. -
On PostgreSQL 14+, also grant
SELECT ON pg_stat_statements_infoto the monitoring user.
Replica Metrics Are Missing
postgresql_replication_data_delay is only populated on the primary.
To collect replica-side metrics, add a second postgresql receiver block pointing to each replica endpoint:
receivers:
postgresql/primary:
endpoint: <primary-host>:5432
username: kfmon
password: ${env:PG_PASSWORD}
databases: [<database-name>]
postgresql/replica:
endpoint: <replica-host>:5432
username: kfmon
password: ${env:PG_PASSWORD}
databases: [<database-name>]
service:
pipelines:
metrics:
receivers: [postgresql/primary, postgresql/replica]
processors: [resource, batch]
exporters: [otlphttp]