OpenTelemetry Collector for PostgreSQL :: Kloudfuse Docs

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:

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
postgresql.database_name Database name from pg_stat_database
service.name Set via the resource processor
service.instance.id Set automatically to <endpoint> (host:port)

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
<postgresql-host> Hostname or service name of the PostgreSQL instance
<password> Password for the monitoring user
<database-name> One or more database names (one entry per database)
<kloudfuse-hostname> External hostname of your Kloudfuse cluster

Deploy on Kubernetes (Helm)

  1. Add the OpenTelemetry Helm repository:
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update
  1. Store the PostgreSQL password in a Kubernetes secret:
kubectl create secret generic otel-postgres-credentials \
     --from-literal=password='<password>' \
     -n <collector-namespace>
  1. Create a values.yaml for 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]
  1. Install the collector:
helm upgrade --install otel-postgres open-telemetry/opentelemetry-collector \
     -f values.yaml \
     -n <collector-namespace>
  1. 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:

  1. Click the Metrics tab, then select Explorer from the drop-down menu.
  2. Set the time range to the last 15 minutes using the interval picker in the top-right corner.
  3. In the metric selector, type postgresql_backends and select it from the suggestions list. Confirm a chart appears with data points.
  4. 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
postgresql_backends Number of active client connections (backends)
postgresql_commits Transactions committed per interval
postgresql_rollbacks Transactions rolled back per interval
postgresql_db_size Database size in bytes
postgresql_deadlocks Deadlock count per interval
postgresql_rows Rows returned, fetched, inserted, updated, deleted (labelled by state)
postgresql_blocks_read Blocks read from disk vs buffer cache (labelled by source)
postgresql_bgwriter_buffers_allocated Buffers allocated by the background writer
postgresql_bgwriter_checkpoint_count Number of checkpoints performed (labelled by type)
postgresql_replication_data_delay 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:

  1. Check the collector pod logs for scrape errors:
kubectl logs -n <collector-namespace> -l app.kubernetes.io/name=opentelemetry-collector

Look for "Error scraping postgresql" or "connection refused" messages. 2. 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
  1. 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":

Statement Metrics Are Missing

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]

External References