> ## Documentation Index
> Fetch the complete documentation index at: https://docs.querycomment.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Getting started with QueryComment

> Connect QueryComment to PostgreSQL or MySQL with a read-only monitoring user.

Connect your first database by creating a dedicated monitoring user, enabling the statistics source for your engine, and running the OTel agent for your database.

<Note>
  QueryComment reads aggregate query, engine, and infrastructure telemetry. The OTel agent does not proxy application traffic and does not write to your application tables.
</Note>

## Prerequisites

Before you start, make sure you have:

* A QueryComment API ingest from your dashboard
* Permission to create a monitoring user and grant monitoring privileges
* Outbound HTTPS access from the OTel agent host to `ingest.querycomment.com`

| Database   | Default port | Primary query source | Required setup                                           |
| ---------- | ------------ | -------------------- | -------------------------------------------------------- |
| PostgreSQL | `5432`       | `pg_stat_statements` | Enable the extension and grant `pg_monitor`              |
| MySQL      | `3306`       | `performance_schema` | Enable statement digests and grant monitoring privileges |

## Prepare your database

Choose the tab for the database you want to connect.

<Tabs>
  <Tab title="PostgreSQL">
    <Steps>
      <Step title="Enable pg_stat_statements">
        QueryComment uses `pg_stat_statements` to read normalized query statistics.

        Check whether it is already loaded:

        ```sql theme={null}
        SHOW shared_preload_libraries;
        ```

        If `pg_stat_statements` is not listed, add it to `postgresql.conf` or your managed database parameter group:

        ```ini theme={null}
        shared_preload_libraries = 'pg_stat_statements'
        ```

        Restart PostgreSQL, then create the extension in the database you want to monitor:

        ```sql theme={null}
        CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
        ```

        <Warning>
          Changing `shared_preload_libraries` requires a PostgreSQL restart. Schedule this during a maintenance window for production databases.
        </Warning>
      </Step>

      <Step title="Create the monitoring user">
        Connect as a privileged PostgreSQL user and run:

        ```sql theme={null}
        CREATE USER querycomment WITH PASSWORD 'replace-with-strong-password';
        GRANT pg_monitor TO querycomment;
        GRANT pg_read_all_stats TO querycomment;
        ```

        `pg_monitor` gives the OTel agent read access to PostgreSQL monitoring views. `pg_read_all_stats` lets the receiver read `queryid` and query text for statements run by other roles.
      </Step>

      <Step title="Verify access">
        Confirm the monitoring user can read statement statistics:

        ```sql theme={null}
        SET ROLE querycomment;
        SELECT calls, query
        FROM pg_stat_statements
        LIMIT 5;
        RESET ROLE;
        ```

        If this query fails, confirm that `pg_stat_statements` is installed in the same database used by the OTel agent connection string.
      </Step>
    </Steps>

    ### Configure the OTel receiver

    Use the [`otelcol-contrib` package](https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib) with the built-in `postgresql` receiver and the [`sqlquery` receiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/sqlqueryreceiver). The `postgresql` receiver collects instance metrics. The `sqlquery` receiver collects statement metrics from `pg_stat_statements`.

    The collector config includes the general PostgreSQL receiver and statement receivers for PostgreSQL 11-12, 13-16, and 17+. Choose the statement receiver that matches your PostgreSQL major version in `service.pipelines.metrics/postgres.receivers`.

    <Note>
      Only the receiver referenced in the metrics pipeline starts and connects to PostgreSQL. The other receiver definitions are parsed but not started.
    </Note>

    | PostgreSQL version | Receiver                                   |
    | ------------------ | ------------------------------------------ |
    | 11-12              | `sqlquery/pg_statements_postgres_v11_12`   |
    | 13-16              | `sqlquery/pg_statements_postgres_v13_16`   |
    | 17+                | `sqlquery/pg_statements_postgres_v17_plus` |

    PostgreSQL 13-16 is the default:

    ```yaml theme={null}
    service:
      pipelines:
        metrics/postgres:
          receivers: [postgresql, sqlquery/pg_statements_postgres_v13_16]
    ```

    Keep `postgresql` in the receivers list. Change only the `sqlquery/...` receiver for your PostgreSQL version.

    The version-specific query fields are:

    | PostgreSQL version | Query differences                                                                                                                                                  |
    | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | 11-12              | Uses `total_time`, `mean_time`, `min_time`, `max_time`, and `stddev_time`. WAL columns do not exist, so `wal_records` and `wal_bytes` are emitted as `0`.          |
    | 13-16              | Uses `total_exec_time`, `mean_exec_time`, `min_exec_time`, `max_exec_time`, `stddev_exec_time`, `wal_records`, `wal_bytes`, `blk_read_time`, and `blk_write_time`. |
    | 17+                | Uses the PostgreSQL 13-16 execution and WAL columns. Uses `shared_blk_read_time AS blk_read_time` and `shared_blk_write_time AS blk_write_time`.                   |

    Set these environment variables before starting `otelcol-contrib`:

    | Variable           | Description                                                                     |
    | ------------------ | ------------------------------------------------------------------------------- |
    | `PG_HOST`          | PostgreSQL host name or IP address.                                             |
    | `PG_PORT`          | PostgreSQL port. Use `5432` unless your instance uses a different port.         |
    | `PG_USER`          | Monitoring user name.                                                           |
    | `PG_PASSWORD`      | Monitoring user password.                                                       |
    | `PG_DBNAME`        | Database to connect to. Use a database where `pg_stat_statements` is installed. |
    | `PG_SSLMODE`       | PostgreSQL SSL mode, such as `require`, `verify-full`, or `disable`.            |
    | `QC_ENDPOINT`      | QueryComment ingest endpoint, for example `ingest.querycomment.com:443`.        |
    | `QC_INGEST_TOKEN`  | QueryComment API ingest value from your dashboard.                              |
    | `PG_CLUSTER_NAME`  | Cluster name to attach to exported metrics.                                     |
    | `OTEL_ENVIRONMENT` | Deployment environment. Defaults to `production` if omitted.                    |

    Example `.env` file:

    ```bash theme={null}
    PG_HOST=db.example.com
    PG_PORT=5432
    PG_USER=querycomment
    PG_PASSWORD=replace-with-strong-password
    PG_DBNAME=app
    PG_SSLMODE=require

    QC_ENDPOINT=ingest.querycomment.com:443
    QC_INGEST_TOKEN=replace-with-querycomment-api-ingest
    PG_CLUSTER_NAME=production-postgres
    OTEL_ENVIRONMENT=production
    ```

    <Expandable title="otel-collector.yaml">
      ```yaml theme={null}
      extensions:
        health_check:
          endpoint: 0.0.0.0:13133

      processors:
        memory_limiter:
          check_interval: 1s
          limit_percentage: 80
          spike_limit_percentage: 25

        resourcedetection:
          detectors: [env, system]
          timeout: 5s
          override: true
          system:
            hostname_sources: [dns, os]
            resource_attributes:
              host.name: { enabled: true }
              host.id: { enabled: true }
              os.type: { enabled: true }

        resource/postgres:
          attributes:
            - key: db.system
              value: postgresql
              action: upsert
            - key: service.name
              value: ${env:PG_CLUSTER_NAME}
              action: upsert
            - key: deployment.environment
              value: ${env:OTEL_ENVIRONMENT:-production}
              action: upsert
        batch:
          send_batch_size: 8192
          send_batch_max_size: 10000
          timeout: 5s

      exporters:
        otlp/querycomment:
          endpoint: ${env:QC_ENDPOINT}
          headers:
            authorization: ${env:QC_INGEST_TOKEN}
          compression: gzip
          retry_on_failure:
            enabled: true
            initial_interval: 5s
            max_interval: 30s
            max_elapsed_time: 300s
          sending_queue:
            enabled: true
            num_consumers: 4
            queue_size: 5000

        debug:
          verbosity: normal

      receivers:
        postgresql:
          endpoint: ${env:PG_HOST}:${env:PG_PORT}
          transport: tcp
          username: ${env:PG_USER}
          password: ${env:PG_PASSWORD}
          databases: []
          # exclude_databases: [postgres, template0, template1] # optional
          collection_interval: 15s
          tls:
            insecure: false
            insecure_skip_verify: false

        sqlquery/pg_statements_postgres_v13_16:
          driver: postgres
          datasource: "host=${env:PG_HOST} port=${env:PG_PORT} user=${env:PG_USER} password=${env:PG_PASSWORD} dbname=${env:PG_DBNAME} sslmode=${env:PG_SSLMODE}"
          collection_interval: 15s
          max_open_conn: 2
          queries:
            - sql: |
                SELECT
                  pgss.queryid::text AS query_id,
                  LEFT(pgss.query, 500) AS query_text,
                  COALESCE(pg_database.datname, 'unknown') AS database_name,
                  COALESCE(pg_roles.rolname, 'unknown') AS role_name,
                  pgss.calls AS calls,
                  pgss.total_exec_time AS total_exec_time,
                  pgss.mean_exec_time AS mean_exec_time,
                  pgss.min_exec_time AS min_exec_time,
                  pgss.max_exec_time AS max_exec_time,
                  pgss.stddev_exec_time AS stddev_exec_time,
                  pgss.rows AS rows,
                  pgss.shared_blks_hit AS shared_blks_hit,
                  pgss.shared_blks_read AS shared_blks_read,
                  pgss.shared_blks_dirtied AS shared_blks_dirtied,
                  pgss.shared_blks_written AS shared_blks_written,
                  pgss.temp_blks_read AS temp_blks_read,
                  pgss.temp_blks_written AS temp_blks_written,
                  pgss.wal_records AS wal_records,
                  pgss.wal_bytes AS wal_bytes,
                  pgss.blk_read_time AS blk_read_time,
                  pgss.blk_write_time AS blk_write_time
                FROM pg_stat_statements AS pgss
                LEFT JOIN pg_roles ON pgss.userid = pg_roles.oid
                LEFT JOIN pg_database ON pgss.dbid = pg_database.oid
                WHERE pgss.queryid IS NOT NULL
                  AND pgss.query NOT ILIKE '%pg_stat_statements%'
                  AND pgss.query NOT ILIKE '%pg_catalog%'
                  AND pgss.query NOT ILIKE 'EXPLAIN%'
                  AND pgss.query NOT ILIKE 'PREPARE otel_%'
                  AND pgss.query NOT ILIKE 'CREATE %'
                ORDER BY pgss.total_exec_time DESC
                LIMIT 250
              metrics: &stmt_metrics
                - { metric_name: postgresql.stmt.calls, value_column: calls, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative }
                - { metric_name: postgresql.stmt.total_exec_time, value_column: total_exec_time, attribute_columns: [query_id, database_name, role_name, query_text], value_type: double, data_type: sum, monotonic: true, aggregation: cumulative, unit: ms }
                - { metric_name: postgresql.stmt.mean_exec_time, value_column: mean_exec_time, attribute_columns: [query_id, database_name, role_name, query_text], value_type: double, data_type: gauge, unit: ms }
                - { metric_name: postgresql.stmt.min_exec_time, value_column: min_exec_time, attribute_columns: [query_id, database_name, role_name, query_text], value_type: double, data_type: gauge, unit: ms }
                - { metric_name: postgresql.stmt.max_exec_time, value_column: max_exec_time, attribute_columns: [query_id, database_name, role_name, query_text], value_type: double, data_type: gauge, unit: ms }
                - { metric_name: postgresql.stmt.stddev_exec_time, value_column: stddev_exec_time, attribute_columns: [query_id, database_name, role_name, query_text], value_type: double, data_type: gauge, unit: ms }
                - { metric_name: postgresql.stmt.rows, value_column: rows, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative }
                - { metric_name: postgresql.stmt.shared_blks_hit, value_column: shared_blks_hit, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative }
                - { metric_name: postgresql.stmt.shared_blks_read, value_column: shared_blks_read, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative }
                - { metric_name: postgresql.stmt.shared_blks_dirtied, value_column: shared_blks_dirtied, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative }
                - { metric_name: postgresql.stmt.shared_blks_written, value_column: shared_blks_written, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative }
                - { metric_name: postgresql.stmt.temp_blks_read, value_column: temp_blks_read, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative }
                - { metric_name: postgresql.stmt.temp_blks_written, value_column: temp_blks_written, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative }
                - { metric_name: postgresql.stmt.wal_records, value_column: wal_records, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative }
                - { metric_name: postgresql.stmt.wal_bytes, value_column: wal_bytes, attribute_columns: [query_id, database_name, role_name, query_text], value_type: int, data_type: sum, monotonic: true, aggregation: cumulative, unit: By }
                - { metric_name: postgresql.stmt.blk_read_time, value_column: blk_read_time, attribute_columns: [query_id, database_name, role_name, query_text], value_type: double, data_type: sum, monotonic: true, aggregation: cumulative, unit: ms }
                - { metric_name: postgresql.stmt.blk_write_time, value_column: blk_write_time, attribute_columns: [query_id, database_name, role_name, query_text], value_type: double, data_type: sum, monotonic: true, aggregation: cumulative, unit: ms }

        sqlquery/pg_statements_postgres_v17_plus:
          driver: postgres
          datasource: "host=${env:PG_HOST} port=${env:PG_PORT} user=${env:PG_USER} password=${env:PG_PASSWORD} dbname=${env:PG_DBNAME} sslmode=${env:PG_SSLMODE}"
          collection_interval: 15s
          max_open_conn: 2
          queries:
            - sql: |
                SELECT
                  pgss.queryid::text AS query_id,
                  LEFT(pgss.query, 500) AS query_text,
                  COALESCE(pg_database.datname, 'unknown') AS database_name,
                  COALESCE(pg_roles.rolname, 'unknown') AS role_name,
                  pgss.calls AS calls,
                  pgss.total_exec_time AS total_exec_time,
                  pgss.mean_exec_time AS mean_exec_time,
                  pgss.min_exec_time AS min_exec_time,
                  pgss.max_exec_time AS max_exec_time,
                  pgss.stddev_exec_time AS stddev_exec_time,
                  pgss.rows AS rows,
                  pgss.shared_blks_hit AS shared_blks_hit,
                  pgss.shared_blks_read AS shared_blks_read,
                  pgss.shared_blks_dirtied AS shared_blks_dirtied,
                  pgss.shared_blks_written AS shared_blks_written,
                  pgss.temp_blks_read AS temp_blks_read,
                  pgss.temp_blks_written AS temp_blks_written,
                  pgss.wal_records AS wal_records,
                  pgss.wal_bytes AS wal_bytes,
                  pgss.shared_blk_read_time AS blk_read_time,
                  pgss.shared_blk_write_time AS blk_write_time
                FROM pg_stat_statements AS pgss
                LEFT JOIN pg_roles ON pgss.userid = pg_roles.oid
                LEFT JOIN pg_database ON pgss.dbid = pg_database.oid
                WHERE pgss.queryid IS NOT NULL
                  AND pgss.query NOT ILIKE '%pg_stat_statements%'
                  AND pgss.query NOT ILIKE '%pg_catalog%'
                  AND pgss.query NOT ILIKE 'EXPLAIN%'
                  AND pgss.query NOT ILIKE 'PREPARE otel_%'
                  AND pgss.query NOT ILIKE 'CREATE %'
                ORDER BY pgss.total_exec_time DESC
                LIMIT 250
              metrics: *stmt_metrics

        sqlquery/pg_statements_postgres_v11_12:
          driver: postgres
          datasource: "host=${env:PG_HOST} port=${env:PG_PORT} user=${env:PG_USER} password=${env:PG_PASSWORD} dbname=${env:PG_DBNAME} sslmode=${env:PG_SSLMODE}"
          collection_interval: 15s
          max_open_conn: 2
          queries:
            - sql: |
                SELECT
                  pgss.queryid::text AS query_id,
                  LEFT(pgss.query, 500) AS query_text,
                  COALESCE(pg_database.datname, 'unknown') AS database_name,
                  COALESCE(pg_roles.rolname, 'unknown') AS role_name,
                  pgss.calls AS calls,
                  pgss.total_time AS total_exec_time,
                  pgss.mean_time AS mean_exec_time,
                  pgss.min_time AS min_exec_time,
                  pgss.max_time AS max_exec_time,
                  pgss.stddev_time AS stddev_exec_time,
                  pgss.rows AS rows,
                  pgss.shared_blks_hit AS shared_blks_hit,
                  pgss.shared_blks_read AS shared_blks_read,
                  pgss.shared_blks_dirtied AS shared_blks_dirtied,
                  pgss.shared_blks_written AS shared_blks_written,
                  pgss.temp_blks_read AS temp_blks_read,
                  pgss.temp_blks_written AS temp_blks_written,
                  0::bigint AS wal_records,
                  0::numeric AS wal_bytes,
                  pgss.blk_read_time AS blk_read_time,
                  pgss.blk_write_time AS blk_write_time
                FROM pg_stat_statements AS pgss
                LEFT JOIN pg_roles ON pgss.userid = pg_roles.oid
                LEFT JOIN pg_database ON pgss.dbid = pg_database.oid
                WHERE pgss.queryid IS NOT NULL
                  AND pgss.query NOT ILIKE '%pg_stat_statements%'
                  AND pgss.query NOT ILIKE '%pg_catalog%'
                  AND pgss.query NOT ILIKE 'EXPLAIN%'
                  AND pgss.query NOT ILIKE 'PREPARE otel_%'
                  AND pgss.query NOT ILIKE 'CREATE %'
                ORDER BY pgss.total_time DESC
                LIMIT 250
              metrics: *stmt_metrics

      service:
        pipelines:
          metrics/postgres:
            receivers: [postgresql, sqlquery/pg_statements_postgres_v13_16]
            processors: [memory_limiter, resourcedetection, resource/postgres, batch]
            exporters: [otlp/querycomment]
      ```
    </Expandable>

    Run the collector after you save `otel-collector.yaml`:

    ```bash theme={null}
    otelcol-contrib --config otel-collector.yaml
    ```

    Or run it through Docker:

    ```bash theme={null}
    docker run --rm \
      --env-file .env \
      -v "$(pwd)/otel-collector.yaml:/etc/otelcol-contrib/config.yaml:ro" \
      otel/opentelemetry-collector-contrib:<version> \
      --config /etc/otelcol-contrib/config.yaml
    ```
  </Tab>

  <Tab title="MySQL">
    <Steps>
      <Step title="Create the monitoring user">
        Connect as a MySQL user with grant privileges and run:

        ```sql theme={null}
        CREATE USER 'querycomment'@'%' IDENTIFIED BY 'replace-with-strong-password';
        GRANT SELECT, PROCESS, REPLICATION CLIENT ON *.* TO 'querycomment'@'%';
        FLUSH PRIVILEGES;
        ```

        `PROCESS` lets the OTel agent inspect thread state and InnoDB status. `REPLICATION CLIENT` lets it read replication status when replicas are present.

        <Note>
          Replace `'%'` with the OTel agent host or subnet if you want to restrict where the monitoring user can connect from.
        </Note>
      </Step>

      <Step title="Verify performance_schema">
        QueryComment uses `performance_schema.events_statements_summary_by_digest` for normalized query statistics.

        Confirm that `performance_schema` is enabled:

        ```sql theme={null}
        SHOW VARIABLES LIKE 'performance_schema';
        ```

        Confirm that statement digests are being collected:

        ```sql theme={null}
        SELECT NAME, ENABLED
        FROM performance_schema.setup_consumers
        WHERE NAME = 'statements_digest';
        ```

        If the consumer is disabled, enable it as an admin user:

        ```sql theme={null}
        UPDATE performance_schema.setup_consumers
        SET ENABLED = 'YES'
        WHERE NAME = 'statements_digest';
        ```
      </Step>

      <Step title="Verify access">
        Confirm the monitoring user has the required grants and that digest rows exist:

        ```sql theme={null}
        SHOW GRANTS FOR 'querycomment'@'%';

        SELECT COUNT(*)
        FROM performance_schema.events_statements_summary_by_digest;
        ```

        If the digest table is empty, run a representative workload against the database and check again.
      </Step>
    </Steps>
  </Tab>
</Tabs>
