diff --git a/README.md b/README.md index c63cf39..d741174 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Prometheus exporter for PgBouncer. Exports metrics at `9127/metrics` +## Requirements + +- PgBouncer 1.8 or higher, since PgBouncer exporter 0.11.0 + ## Building and running make build diff --git a/collector.go b/collector.go index fb527dd..ffa38a6 100644 --- a/collector.go +++ b/collector.go @@ -44,16 +44,19 @@ var ( "paused": {GAUGE, "paused", 1, "1 if this database is currently paused, else 0"}, "disabled": {GAUGE, "disabled", 1, "1 if this database is currently disabled, else 0"}, }, - "stats": { - "database": {LABEL, "N/A", 1, "N/A"}, - "total_query_count": {COUNTER, "queries_pooled_total", 1, "Total number of SQL queries pooled"}, - "total_query_time": {COUNTER, "queries_duration_seconds_total", 1e-6, "Total number of seconds spent by pgbouncer when actively connected to PostgreSQL, executing queries"}, - "total_received": {COUNTER, "received_bytes_total", 1, "Total volume in bytes of network traffic received by pgbouncer, shown as bytes"}, - "total_requests": {COUNTER, "queries_total", 1, "Total number of SQL requests pooled by pgbouncer, shown as requests"}, - "total_sent": {COUNTER, "sent_bytes_total", 1, "Total volume in bytes of network traffic sent by pgbouncer, shown as bytes"}, - "total_wait_time": {COUNTER, "client_wait_seconds_total", 1e-6, "Time spent by clients waiting for a server in seconds"}, - "total_xact_count": {COUNTER, "sql_transactions_pooled_total", 1, "Total number of SQL transactions pooled"}, - "total_xact_time": {COUNTER, "server_in_transaction_seconds_total", 1e-6, "Total number of seconds spent by pgbouncer when connected to PostgreSQL in a transaction, either idle in transaction or executing queries"}, + "stats_totals": { + "database": {LABEL, "N/A", 1, "N/A"}, + "query_count": {COUNTER, "queries_pooled_total", 1, "Total number of SQL queries pooled"}, + "query_time": {COUNTER, "queries_duration_seconds_total", 1e-6, "Total number of seconds spent by pgbouncer when actively connected to PostgreSQL, executing queries"}, + "bytes_received": {COUNTER, "received_bytes_total", 1, "Total volume in bytes of network traffic received by pgbouncer, shown as bytes"}, + "requests": {COUNTER, "queries_total", 1, "Total number of SQL requests pooled by pgbouncer, shown as requests"}, + "bytes_sent": {COUNTER, "sent_bytes_total", 1, "Total volume in bytes of network traffic sent by pgbouncer, shown as bytes"}, + "wait_time": {COUNTER, "client_wait_seconds_total", 1e-6, "Time spent by clients waiting for a server in seconds"}, + "xact_count": {COUNTER, "sql_transactions_pooled_total", 1, "Total number of SQL transactions pooled"}, + "xact_time": {COUNTER, "server_in_transaction_seconds_total", 1e-6, "Total number of seconds spent by pgbouncer when connected to PostgreSQL in a transaction, either idle in transaction or executing queries"}, + "client_parse_count": {COUNTER, "client_parses_total", 1, "Total number of prepared statement Parse messages received from clients"}, + "server_parse_count": {COUNTER, "server_parses_total", 1, "Total number of prepared statement Parse messages sent by pgbouncer to PostgreSQL"}, + "bind_count": {COUNTER, "binds_total", 1, "Total number of prepared statements readied for execution with a Bind message"}, }, "pools": { "database": {LABEL, "N/A", 1, "N/A"}, diff --git a/collector_test.go b/collector_test.go index 2570b5b..f9c2d5e 100644 --- a/collector_test.go +++ b/collector_test.go @@ -129,3 +129,82 @@ func TestQueryShowConfig(t *testing.T) { t.Errorf("there were unfulfilled exceptions: %s", err) } } + +func TestQueryShowDatabases(t *testing.T) { + rows := sqlmock.NewRows([]string{"name", "host", "port", "database", "pool_size"}). + AddRow("pg0_db", "10.10.10.1", "5432", "pg0", 20) + + expected := []MetricResult{ + {labels: labelMap{"name": "pg0_db", "host": "10.10.10.1", "port": "5432", "database": "pg0", "force_user": "", "pool_mode": ""}, metricType: dto.MetricType_GAUGE, value: 20}, + } + + testQueryNamespaceMapping(t, "databases", rows, expected) +} + +func TestQueryShowStats(t *testing.T) { + // columns are listed in the order PgBouncers exposes them, a value of -1 means pgbouncer_exporter does not expose this value as a metric + rows := sqlmock.NewRows([]string{"database", + "server_assignment_count", + "xact_count", "query_count", "bytes_received", "bytes_sent", + "xact_time", "query_time", "wait_time", "client_parse_count", "server_parse_count", "bind_count"}). + AddRow("pg0", -1, 10, 40, 220, 460, 6, 8, 9, 5, 55, 555) + + // expected metrics are returned in the same order as the colums + expected := []MetricResult{ + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 10}, // xact_count + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 40}, // query_count + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 220}, // bytes_received + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 460}, // bytes_sent + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 6e-6}, // xact_time + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 8e-6}, // query_time + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 9e-6}, // wait_time + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 5}, // client_parse_count + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 55}, // server_parse_count + {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 555}, // bind_count + } + + testQueryNamespaceMapping(t, "stats_totals", rows, expected) +} + +func TestQueryShowPools(t *testing.T) { + rows := sqlmock.NewRows([]string{"database", "user", "cl_active"}). + AddRow("pg0", "postgres", 2) + + expected := []MetricResult{ + {labels: labelMap{"database": "pg0", "user": "postgres"}, metricType: dto.MetricType_GAUGE, value: 2}, + } + + testQueryNamespaceMapping(t, "pools", rows, expected) +} + +func testQueryNamespaceMapping(t *testing.T, namespaceMapping string, rows *sqlmock.Rows, expected []MetricResult) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub db connection: %s", err) + } + defer db.Close() + + mock.ExpectQuery("SHOW " + namespaceMapping + ";").WillReturnRows(rows) + + logger := slog.Default() + + metricMap := makeDescMap(metricMaps, namespace, logger) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + if _, err := queryNamespaceMapping(ch, db, namespaceMapping, metricMap[namespaceMapping], logger); err != nil { + t.Errorf("Error running queryNamespaceMapping: %s", err) + } + }() + + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + m := readMetric(<-ch) + convey.So(m, convey.ShouldResemble, expect) + } + }) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled exceptions: %s", err) + } +}