From a9101936094a6cad86424ad19016db9a4bee11bb Mon Sep 17 00:00:00 2001 From: Greg Pattison Date: Mon, 29 Jun 2026 08:20:20 -0400 Subject: [PATCH 1/4] Modernize pgbouncer-mixin to grafonnet v11 and signals architecture --- pgbouncer-mixin/config.libsonnet | 10 ++ pgbouncer-mixin/dashboards.libsonnet | 6 +- .../{clusterOverview => clusterOverview.json} | 45 +++-- .../dashboards_out/{logs => logs.json} | 0 .../{overview => overview.json} | 161 +++++++++++------ pgbouncer-mixin/links.libsonnet | 6 +- pgbouncer-mixin/main.libsonnet | 11 +- pgbouncer-mixin/panels.libsonnet | 64 ++++--- pgbouncer-mixin/signals/cluster.libsonnet | 79 +++++++++ pgbouncer-mixin/signals/config.libsonnet | 41 +++++ pgbouncer-mixin/signals/connections.libsonnet | 129 ++++++++++++++ pgbouncer-mixin/signals/stats.libsonnet | 95 ++++++++++ pgbouncer-mixin/targets.libsonnet | 165 ------------------ pgbouncer-mixin/variables.libsonnet | 2 +- 14 files changed, 555 insertions(+), 259 deletions(-) rename pgbouncer-mixin/dashboards_out/{clusterOverview => clusterOverview.json} (89%) rename pgbouncer-mixin/dashboards_out/{logs => logs.json} (100%) rename pgbouncer-mixin/dashboards_out/{overview => overview.json} (83%) create mode 100644 pgbouncer-mixin/signals/cluster.libsonnet create mode 100644 pgbouncer-mixin/signals/config.libsonnet create mode 100644 pgbouncer-mixin/signals/connections.libsonnet create mode 100644 pgbouncer-mixin/signals/stats.libsonnet delete mode 100644 pgbouncer-mixin/targets.libsonnet diff --git a/pgbouncer-mixin/config.libsonnet b/pgbouncer-mixin/config.libsonnet index c4508010d..2fc73c6ba 100644 --- a/pgbouncer-mixin/config.libsonnet +++ b/pgbouncer-mixin/config.libsonnet @@ -1,4 +1,5 @@ { + local this = self, // any modular library should include as inputs: // 'dashboardNamePrefix' - Use as prefix for all Dashboards and (optional) rule groups // 'filteringSelector' - Static selector to apply to ALL dashboard variables of type query, panel queries, alerts and recording rules. @@ -16,6 +17,7 @@ dashboardTags: [self.uid], uid: 'pgbouncer', dashboardNamePrefix: '', + metricsSource: ['prometheus'], // additional params can be added if needed dashboardPeriod: 'now-1h', @@ -34,4 +36,12 @@ extraLogLabels: ['level'], logsVolumeGroupBy: 'level', showLogsVolume: true, + + // Signals configuration + signals+: { + connections: (import './signals/connections.libsonnet')(this), + stats: (import './signals/stats.libsonnet')(this), + config: (import './signals/config.libsonnet')(this), + cluster: (import './signals/cluster.libsonnet')(this), + }, } diff --git a/pgbouncer-mixin/dashboards.libsonnet b/pgbouncer-mixin/dashboards.libsonnet index 87aa95e90..87b626168 100644 --- a/pgbouncer-mixin/dashboards.libsonnet +++ b/pgbouncer-mixin/dashboards.libsonnet @@ -15,7 +15,7 @@ local logslib = import 'logs-lib/logs/main.libsonnet'; local panels = this.grafana.panels; local stat = g.panel.stat; { - overview: + 'overview.json': g.dashboard.new(prefix + 'PgBouncer overview') + g.dashboard.withPanels( g.util.grid.wrapPanels( @@ -45,7 +45,7 @@ local logslib = import 'logs-lib/logs/main.libsonnet'; ) // hide link to self + root.applyCommon(vars.singleInstance, uid + '-overview', tags, links { pgbouncerOverview+:: {} }, annotations, timezone, refresh, period), - clusterOverview: + 'clusterOverview.json': g.dashboard.new(prefix + 'PgBouncer cluster overview') + g.dashboard.withPanels( g.util.grid.wrapPanels( @@ -64,7 +64,7 @@ local logslib = import 'logs-lib/logs/main.libsonnet'; + if this.config.enableLokiLogs then { - logs: + 'logs.json': logslib.new( prefix + 'PgBouncer logs', datasourceName=this.grafana.variables.datasources.loki.name, diff --git a/pgbouncer-mixin/dashboards_out/clusterOverview b/pgbouncer-mixin/dashboards_out/clusterOverview.json similarity index 89% rename from pgbouncer-mixin/dashboards_out/clusterOverview rename to pgbouncer-mixin/dashboards_out/clusterOverview.json index 93a9008d6..eea01a26a 100644 --- a/pgbouncer-mixin/dashboards_out/clusterOverview +++ b/pgbouncer-mixin/dashboards_out/clusterOverview.json @@ -57,15 +57,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, pgbouncer_pools_client_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",})", - "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}}" + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, pgbouncer_pools_client_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"})", + "format": "time_series", + "instant": false, + "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}}", + "refId": "Top databases by active connections" } ], "title": "Top databases by active connections", @@ -133,15 +136,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",}[$__rate_interval]))", - "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}}" + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__rate_interval]))", + "format": "time_series", + "instant": false, + "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}}", + "refId": "Top databases by queries processed" } ], "title": "Top databases by queries processed", @@ -190,15 +196,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, 1000 * increase(pgbouncer_stats_queries_duration_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",}[$__interval:]), 1))", - "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}}" + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, 1000 * increase(pgbouncer_stats_queries_duration_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__interval:]), 1))", + "format": "time_series", + "instant": false, + "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}}", + "refId": "Top databases by average query duration" } ], "title": "Top databases by average query duration", @@ -248,23 +257,29 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_received_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",}[$__rate_interval]))", - "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}} - received" + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_received_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__rate_interval]))", + "format": "time_series", + "instant": false, + "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}} - received", + "refId": "Top databases by network traffic received" }, { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_sent_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",}[$__rate_interval]))", - "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}} - sent" + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_sent_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__rate_interval]))", + "format": "time_series", + "instant": false, + "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}} - sent", + "refId": "Top databases by network traffic sent" } ], "title": "Top databases by network traffic", @@ -310,7 +325,7 @@ "label": "Pgbouncer_cluster", "multi": true, "name": "pgbouncer_cluster", - "query": "label_values(pgbouncer_databases_current_connections{,job=~\"$job\"}, pgbouncer_cluster)", + "query": "label_values(pgbouncer_databases_current_connections{job=~\"$job\"}, pgbouncer_cluster)", "refresh": 2, "sort": 1, "type": "query" diff --git a/pgbouncer-mixin/dashboards_out/logs b/pgbouncer-mixin/dashboards_out/logs.json similarity index 100% rename from pgbouncer-mixin/dashboards_out/logs rename to pgbouncer-mixin/dashboards_out/logs.json diff --git a/pgbouncer-mixin/dashboards_out/overview b/pgbouncer-mixin/dashboards_out/overview.json similarity index 83% rename from pgbouncer-mixin/dashboards_out/overview rename to pgbouncer-mixin/dashboards_out/overview.json index 6d33e1fa4..826e5c64c 100644 --- a/pgbouncer-mixin/dashboards_out/overview +++ b/pgbouncer-mixin/dashboards_out/overview.json @@ -55,14 +55,18 @@ "options": { "graphMode": "none" }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "sum(pgbouncer_pools_client_waiting_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",})" + "expr": "sum(\n pgbouncer_pools_client_waiting_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}\n)", + "format": "time_series", + "instant": false, + "legendFormat": "", + "refId": "Client waiting connections" } ], "title": "Client waiting connections", @@ -106,14 +110,18 @@ ] } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "sum(pgbouncer_pools_client_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",})" + "expr": "sum(\n pgbouncer_pools_client_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}\n)", + "format": "time_series", + "instant": false, + "legendFormat": "", + "refId": "Active client connections" } ], "title": "Active client connections", @@ -157,14 +165,18 @@ ] } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "sum(pgbouncer_pools_server_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",})" + "expr": "sum(\n pgbouncer_pools_server_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}\n)", + "format": "time_series", + "instant": false, + "legendFormat": "", + "refId": "Active server connections" } ], "title": "Active server connections", @@ -208,14 +220,18 @@ ] } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "sum(pgbouncer_databases_max_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",})" + "expr": "sum(\n pgbouncer_databases_max_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}\n)", + "format": "time_series", + "instant": false, + "legendFormat": "", + "refId": "Max database connections" } ], "title": "Max database connections", @@ -259,14 +275,18 @@ ] } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "sum(pgbouncer_config_max_user_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",})" + "expr": "sum(\n pgbouncer_config_max_user_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\"}\n)", + "format": "time_series", + "instant": false, + "legendFormat": "", + "refId": "Max user connections" } ], "title": "Max user connections", @@ -310,14 +330,18 @@ ] } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "sum(pgbouncer_config_max_client_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",})" + "expr": "sum(\n pgbouncer_config_max_client_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\"}\n)", + "format": "time_series", + "instant": false, + "legendFormat": "", + "refId": "Max client connections" } ], "title": "Max client connections", @@ -378,15 +402,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "rate(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}[$__rate_interval])", - "legendFormat": "{{database}}" + "expr": "rate(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}}", + "refId": "Queries processed" } ], "title": "Queries processed", @@ -435,15 +462,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "1000 * increase(pgbouncer_stats_queries_duration_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}[$__interval:]), 1)", - "legendFormat": "{{database}}" + "expr": "1000 * increase(pgbouncer_stats_queries_duration_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:]), 1)", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}}", + "refId": "Average query duration" } ], "title": "Queries average duration / $__interval", @@ -507,23 +537,29 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "rate(pgbouncer_stats_received_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}[$__rate_interval])", - "legendFormat": "{{database}} - received" + "expr": "rate(pgbouncer_stats_received_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}} - received", + "refId": "Network received" }, { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "rate(pgbouncer_stats_sent_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}[$__rate_interval])", - "legendFormat": "{{database}} - sent" + "expr": "rate(pgbouncer_stats_sent_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}} - sent", + "refId": "Network sent" } ], "title": "Network traffic", @@ -584,15 +620,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "rate(pgbouncer_stats_sql_transactions_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}[$__rate_interval])", - "legendFormat": "{{database}}" + "expr": "rate(pgbouncer_stats_sql_transactions_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}}", + "refId": "SQL transaction rate" } ], "title": "SQL transaction rate", @@ -641,15 +680,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "1000 * increase(pgbouncer_stats_server_in_transaction_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}[$__interval:]) / clamp_min(increase(pgbouncer_stats_sql_transactions_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}[$__interval:]), 1)", - "legendFormat": "{{database}}" + "expr": "1000 * increase(pgbouncer_stats_server_in_transaction_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:]) / clamp_min(increase(pgbouncer_stats_sql_transactions_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:]), 1)", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}}", + "refId": "Average transaction duration" } ], "title": "SQL average transaction duration / $__interval", @@ -711,39 +753,51 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "pgbouncer_pools_server_idle_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}", - "legendFormat": "{{database}} - idle" + "expr": "pgbouncer_pools_server_idle_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}} - idle", + "refId": "Server idle connections" }, { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "pgbouncer_pools_server_used_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}", - "legendFormat": "{{database}} - used" + "expr": "pgbouncer_pools_server_used_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}} - used", + "refId": "Server used connections" }, { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "pgbouncer_pools_server_login_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}", - "legendFormat": "{{database}} - login" + "expr": "pgbouncer_pools_server_login_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}} - login", + "refId": "Server login connections" }, { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "pgbouncer_pools_server_testing_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}", - "legendFormat": "{{database}} - testing" + "expr": "pgbouncer_pools_server_testing_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}} - testing", + "refId": "Server testing connections" } ], "title": "Server connections", @@ -804,15 +858,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "pgbouncer_pools_client_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}", - "legendFormat": "{{database}}" + "expr": "pgbouncer_pools_client_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}}", + "refId": "Active client connections" } ], "title": "Active client connections", @@ -861,15 +918,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "pgbouncer_pools_client_waiting_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}", - "legendFormat": "{{database}}" + "expr": "pgbouncer_pools_client_waiting_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}}", + "refId": "Client waiting connections" } ], "title": "Waiting clients", @@ -918,15 +978,18 @@ "sort": "desc" } }, - "pluginVersion": "v11.0.0", + "pluginVersion": "v11.4.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "pgbouncer_pools_client_maxwait_seconds{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\",}", - "legendFormat": "{{database}}" + "expr": "pgbouncer_pools_client_maxwait_seconds{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{database}}", + "refId": "Max client wait time" } ], "title": "Max client wait time", @@ -972,7 +1035,7 @@ "label": "Pgbouncer_cluster", "multi": true, "name": "pgbouncer_cluster", - "query": "label_values(pgbouncer_databases_current_connections{,job=~\"$job\"}, pgbouncer_cluster)", + "query": "label_values(pgbouncer_databases_current_connections{job=~\"$job\"}, pgbouncer_cluster)", "refresh": 2, "sort": 1, "type": "query" @@ -987,7 +1050,7 @@ "label": "Instance", "multi": false, "name": "instance", - "query": "label_values(pgbouncer_databases_current_connections{,job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}, instance)", + "query": "label_values(pgbouncer_databases_current_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}, instance)", "refresh": 2, "sort": 1, "type": "query" diff --git a/pgbouncer-mixin/links.libsonnet b/pgbouncer-mixin/links.libsonnet index 8cf725f75..9808914d3 100644 --- a/pgbouncer-mixin/links.libsonnet +++ b/pgbouncer-mixin/links.libsonnet @@ -4,17 +4,17 @@ local g = import './g.libsonnet'; new(this): { pgbouncerOverview: - link.link.new('PgBouncer overview', '/d/' + this.grafana.dashboards.overview.uid) + link.link.new('PgBouncer overview', '/d/' + this.grafana.dashboards['overview.json'].uid) + link.link.options.withKeepTime(true), pgbouncerClusterOverview: - link.link.new('PgBouncer cluster overview', '/d/' + this.grafana.dashboards.clusterOverview.uid) + link.link.new('PgBouncer cluster overview', '/d/' + this.grafana.dashboards['clusterOverview.json'].uid) + link.link.options.withKeepTime(true), } + if this.config.enableLokiLogs then { logs: - link.link.new('PgBouncer logs', '/d/' + this.grafana.dashboards.logs.uid) + link.link.new('PgBouncer logs', '/d/' + this.grafana.dashboards['logs.json'].uid) + link.link.options.withKeepTime(true), } else {}, diff --git a/pgbouncer-mixin/main.libsonnet b/pgbouncer-mixin/main.libsonnet index 8ac3be57c..665d9773d 100644 --- a/pgbouncer-mixin/main.libsonnet +++ b/pgbouncer-mixin/main.libsonnet @@ -4,8 +4,8 @@ local dashboards = import './dashboards.libsonnet'; local g = import './g.libsonnet'; local links = import './links.libsonnet'; local panels = import './panels.libsonnet'; -local targets = import './targets.libsonnet'; local variables = import './variables.libsonnet'; +local commonlib = import 'common-lib/common/main.libsonnet'; { @@ -17,10 +17,17 @@ local variables = import './variables.libsonnet'; local this = self, config: config, + signals: + { + [sig]: commonlib.signals.unmarshallJsonMulti( + this.config.signals[sig], + type=this.config.metricsSource + ) + for sig in std.objectFields(this.config.signals) + }, grafana: { variables: variables.new(this, varMetric='pgbouncer_databases_current_connections'), - targets: targets.new(this), annotations: {}, links: links.new(this), panels: panels.new(this), diff --git a/pgbouncer-mixin/panels.libsonnet b/pgbouncer-mixin/panels.libsonnet index 5c059c5aa..ada9b7d51 100644 --- a/pgbouncer-mixin/panels.libsonnet +++ b/pgbouncer-mixin/panels.libsonnet @@ -1,10 +1,9 @@ local g = import './g.libsonnet'; local commonlib = import 'common-lib/common/main.libsonnet'; -local utils = commonlib.utils; { new(this): { - local t = this.grafana.targets, + local signals = this.signals, local stat = g.panel.stat, local alertList = g.panel.alertList, @@ -12,7 +11,9 @@ local utils = commonlib.utils; clientsWaitingConnections: commonlib.panels.generic.stat.base.new( 'Client waiting connections', - targets=[t.clientsWaitingConnections], + targets=[ + signals.connections.pools_client_waiting_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), + ], description='Current number of client connections waiting on a server connection.' ) + stat.options.withGraphMode('none') @@ -28,35 +29,45 @@ local utils = commonlib.utils; activeClientConnections: commonlib.panels.generic.stat.info.new( 'Active client connections', - targets=[t.activeClientConnections], + targets=[ + signals.connections.pools_client_active_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), + ], description='Current number of active client connections.' ) + stat.options.withGraphMode('none'), activeServerConnections: commonlib.panels.generic.stat.info.new( 'Active server connections', - targets=[t.activeServerConnections], + targets=[ + signals.connections.pools_server_active_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), + ], description='Current number of client connections that are linked to a server connection and able to process queries.' ) + stat.options.withGraphMode('none'), maxDatabaseConnections: commonlib.panels.generic.stat.info.new( 'Max database connections', - targets=[t.maxDatabaseConnections], + targets=[ + signals.connections.databases_max_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), + ], description='Maximum number of allowed connections for database.' ) + stat.options.withGraphMode('none'), maxUserConnections: commonlib.panels.generic.stat.info.new( 'Max user connections', - targets=[t.maxUserConnections], + targets=[ + signals.config.config_max_user_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), + ], description='Maximum number of server connections per user allowed.' ) + stat.options.withGraphMode('none'), maxClientConnections: commonlib.panels.generic.stat.info.new( 'Max client connections', - targets=[t.maxClientConnections], + targets=[ + signals.config.config_max_client_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), + ], description='Maximum number of client connections allowed.' ) + stat.options.withGraphMode('none'), @@ -64,7 +75,7 @@ local utils = commonlib.utils; queriesPooled: commonlib.panels.generic.timeSeries.base.new( 'Queries processed', - targets=[t.queriesPooled], + targets=[signals.stats.stats_queries_pooled_total.asTarget()], description=||| Rate of SQL queries pooled by PgBouncer. ||| @@ -74,7 +85,7 @@ local utils = commonlib.utils; queryDuration: commonlib.panels.generic.timeSeries.base.new( 'Queries average duration / $__interval', - targets=[t.queryDuration], + targets=[signals.stats.stats_query_avg_duration.asTarget()], description=||| Average duration of queries being processed by PgBouncer. ||| @@ -84,7 +95,10 @@ local utils = commonlib.utils; networkTraffic: commonlib.panels.network.timeSeries.traffic.new( 'Network traffic', - targets=[t.networkTrafficRecieved, t.networkTrafficSent], + targets=[ + signals.stats.stats_received_bytes_total.asTarget(), + signals.stats.stats_sent_bytes_total.asTarget(), + ], description=||| Volume in bytes of network traffic received by PgBouncer. ||| @@ -96,7 +110,7 @@ local utils = commonlib.utils; transactionRate: commonlib.panels.generic.timeSeries.base.new( 'SQL transaction rate', - targets=[t.transactionRate], + targets=[signals.stats.stats_sql_transactions_pooled_total.asTarget()], description=||| Rate of SQL transactions pooled. ||| @@ -106,7 +120,7 @@ local utils = commonlib.utils; transactionAverageDuration: commonlib.panels.generic.timeSeries.base.new( 'SQL average transaction duration / $__interval', - targets=[t.transactionAverageDuration], + targets=[signals.stats.stats_transaction_avg_duration.asTarget()], description=||| Average duration of SQL transactions pooled. ||| @@ -116,7 +130,12 @@ local utils = commonlib.utils; serverConnections: commonlib.panels.generic.timeSeries.base.new( 'Server connections', - targets=[t.serverIdleConnections, t.serverUsedConnections, t.serverLoginConnections, t.serverTestingConnections], + targets=[ + signals.connections.pools_server_idle_connections.asTarget(), + signals.connections.pools_server_used_connections.asTarget(), + signals.connections.pools_server_login_connections.asTarget(), + signals.connections.pools_server_testing_connections.asTarget(), + ], description=||| Number of various server connection states. ||| @@ -128,7 +147,7 @@ local utils = commonlib.utils; granularActiveClientConnections: commonlib.panels.generic.timeSeries.base.new( 'Active client connections', - targets=[t.granularActiveClientConnections], + targets=[signals.connections.pools_client_active_connections.asTarget()], description=||| Current number of active client connections. ||| @@ -138,7 +157,7 @@ local utils = commonlib.utils; clientsWaiting: commonlib.panels.generic.timeSeries.base.new( 'Waiting clients', - targets=[t.clientsWaiting], + targets=[signals.connections.pools_client_waiting_connections.asTarget()], description=||| Current number of client connections waiting on a server connection. ||| @@ -148,7 +167,7 @@ local utils = commonlib.utils; maxClientWaitTime: commonlib.panels.generic.timeSeries.base.new( 'Max client wait time', - targets=[t.maxClientWaitTime], + targets=[signals.connections.pools_client_maxwait_seconds.asTarget()], description=||| Age of the oldest unserved client connection in seconds. ||| @@ -162,7 +181,7 @@ local utils = commonlib.utils; topDatabaseActiveConnection: commonlib.panels.generic.timeSeries.base.new( 'Top databases by active connections', - targets=[t.topDatabaseActiveConnection], + targets=[signals.cluster.top_database_active_connection.asTarget()], description=||| Top databases by current number of active client connections. ||| @@ -171,7 +190,7 @@ local utils = commonlib.utils; topDatabaseQueryPooled: commonlib.panels.generic.timeSeries.base.new( 'Top databases by queries processed', - targets=[t.topDatabaseQueryProcessed], + targets=[signals.cluster.top_database_query_processed.asTarget()], description=||| Top databases by rate of SQL queries pooled by PgBouncer. ||| @@ -180,7 +199,7 @@ local utils = commonlib.utils; topDatabaseQueryDuration: commonlib.panels.generic.timeSeries.base.new( 'Top databases by average query duration', - targets=[t.topDatabaseQueryDuration], + targets=[signals.cluster.top_database_query_duration.asTarget()], description=||| Top databases by average duration of queries being processed by PgBouncer. ||| @@ -189,7 +208,10 @@ local utils = commonlib.utils; topDatabaseNetworkTraffic: commonlib.panels.generic.timeSeries.base.new( 'Top databases by network traffic', - targets=[t.topDatabaseNetworkTrafficReceived, t.topDatabaseNetworkTrafficSent], + targets=[ + signals.cluster.top_database_network_received.asTarget(), + signals.cluster.top_database_network_sent.asTarget(), + ], description=||| Top databases by volume of network traffic. ||| diff --git a/pgbouncer-mixin/signals/cluster.libsonnet b/pgbouncer-mixin/signals/cluster.libsonnet new file mode 100644 index 000000000..c12c5ad2d --- /dev/null +++ b/pgbouncer-mixin/signals/cluster.libsonnet @@ -0,0 +1,79 @@ +// Cluster-overview "top database" signals for PgBouncer. +// These reproduce the legacy topk panels: the inner selector is group-only +// (job, pgbouncer_cluster via %(queriesSelectorGroupOnly)s) and the topk by-clause / +// $top_database_count parameter are baked into raw expressions. +function(this) + { + filteringSelector: this.filteringSelector, + groupLabels: this.groupLabels, + instanceLabels: this.instanceLabels, + enableLokiLogs: this.enableLokiLogs, + aggLevel: 'none', + aggFunction: 'sum', + alertsInterval: '5m', + discoveryMetric: { + prometheus: 'pgbouncer_databases_current_connections', + }, + signals: { + top_database_active_connection: { + name: 'Top databases by active connections', + description: 'Top databases by current number of active client connections.', + type: 'raw', + unit: 'conn', + sources: { + prometheus: { + expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, pgbouncer_pools_client_active_connections{%(queriesSelectorGroupOnly)s})', + legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}}', + }, + }, + }, + top_database_query_processed: { + name: 'Top databases by queries processed', + description: 'Top databases by rate of SQL queries pooled by PgBouncer.', + type: 'raw', + unit: 'ops', + sources: { + prometheus: { + expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_queries_pooled_total{%(queriesSelectorGroupOnly)s}[$__rate_interval]))', + legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}}', + }, + }, + }, + top_database_query_duration: { + name: 'Top databases by average query duration', + description: 'Top databases by average duration of queries being processed by PgBouncer.', + type: 'raw', + unit: 's', + sources: { + prometheus: { + expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, 1000 * increase(pgbouncer_stats_queries_duration_seconds_total{%(queriesSelectorGroupOnly)s}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{%(queriesSelectorGroupOnly)s}[$__interval:]), 1))', + legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}}', + }, + }, + }, + top_database_network_received: { + name: 'Top databases by network traffic received', + description: 'Top databases by volume of network traffic received.', + type: 'raw', + unit: 'Bps', + sources: { + prometheus: { + expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_received_bytes_total{%(queriesSelectorGroupOnly)s}[$__rate_interval]))', + legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}} - received', + }, + }, + }, + top_database_network_sent: { + name: 'Top databases by network traffic sent', + description: 'Top databases by volume of network traffic sent.', + type: 'raw', + unit: 'Bps', + sources: { + prometheus: { + expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_sent_bytes_total{%(queriesSelectorGroupOnly)s}[$__rate_interval]))', + legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}} - sent', + }, + }, + }, + }, + } diff --git a/pgbouncer-mixin/signals/config.libsonnet b/pgbouncer-mixin/signals/config.libsonnet new file mode 100644 index 000000000..a4758402c --- /dev/null +++ b/pgbouncer-mixin/signals/config.libsonnet @@ -0,0 +1,41 @@ +// Per-instance configuration limit signals for PgBouncer. +// These are instance-scoped settings (not per-database), so instanceLabels is +// pureInstanceLabels (['instance']) -> selector job, pgbouncer_cluster, instance, +// reproducing the legacy instanceQueriesSelector. aggLevel 'none'; panels supply sum(). +function(this) + { + filteringSelector: this.filteringSelector, + groupLabels: this.groupLabels, + instanceLabels: this.pureInstanceLabels, + enableLokiLogs: this.enableLokiLogs, + aggLevel: 'none', + aggFunction: 'sum', + alertsInterval: '5m', + discoveryMetric: { + prometheus: 'pgbouncer_databases_current_connections', + }, + signals: { + config_max_user_connections: { + name: 'Max user connections', + description: 'Maximum number of server connections per user allowed.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_config_max_user_connections{%(queriesSelector)s}', + }, + }, + }, + config_max_client_connections: { + name: 'Max client connections', + description: 'Maximum number of client connections allowed.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_config_max_client_connections{%(queriesSelector)s}', + }, + }, + }, + }, + } diff --git a/pgbouncer-mixin/signals/connections.libsonnet b/pgbouncer-mixin/signals/connections.libsonnet new file mode 100644 index 000000000..81d7fa25c --- /dev/null +++ b/pgbouncer-mixin/signals/connections.libsonnet @@ -0,0 +1,129 @@ +// Connection pool signals for PgBouncer (per-database group) +// Selector: job, pgbouncer_cluster, instance, database. +// aggLevel is kept 'none' so panels supply aggregation explicitly (matching legacy sum/raw). +function(this) + { + filteringSelector: this.filteringSelector, + groupLabels: this.groupLabels, + instanceLabels: this.instanceLabels, + enableLokiLogs: this.enableLokiLogs, + aggLevel: 'none', + aggFunction: 'sum', + alertsInterval: '5m', + discoveryMetric: { + prometheus: 'pgbouncer_databases_current_connections', + }, + signals: { + // Client connection states + pools_client_waiting_connections: { + name: 'Client waiting connections', + description: 'Current number of client connections waiting on a server connection.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_pools_client_waiting_connections{%(queriesSelector)s}', + legendCustomTemplate: '{{database}}', + }, + }, + }, + pools_client_active_connections: { + name: 'Active client connections', + description: 'Current number of active client connections.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_pools_client_active_connections{%(queriesSelector)s}', + legendCustomTemplate: '{{database}}', + }, + }, + }, + pools_client_maxwait_seconds: { + name: 'Max client wait time', + description: 'Age of the oldest unserved client connection in seconds.', + type: 'gauge', + unit: 's', + sources: { + prometheus: { + expr: 'pgbouncer_pools_client_maxwait_seconds{%(queriesSelector)s}', + legendCustomTemplate: '{{database}}', + }, + }, + }, + + // Server connection states + pools_server_active_connections: { + name: 'Active server connections', + description: 'Current number of server connections linked to a client connection.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_pools_server_active_connections{%(queriesSelector)s}', + }, + }, + }, + pools_server_idle_connections: { + name: 'Server idle connections', + description: 'Current number of server connections idle and ready for a client query.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_pools_server_idle_connections{%(queriesSelector)s}', + legendCustomTemplate: '{{database}} - idle', + }, + }, + }, + pools_server_used_connections: { + name: 'Server used connections', + description: 'Current number of server connections idle more than server_check_delay.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_pools_server_used_connections{%(queriesSelector)s}', + legendCustomTemplate: '{{database}} - used', + }, + }, + }, + pools_server_login_connections: { + name: 'Server login connections', + description: 'Current number of server connections in login phase.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_pools_server_login_connections{%(queriesSelector)s}', + legendCustomTemplate: '{{database}} - login', + }, + }, + }, + pools_server_testing_connections: { + name: 'Server testing connections', + description: 'Current number of server connections being tested.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_pools_server_testing_connections{%(queriesSelector)s}', + legendCustomTemplate: '{{database}} - testing', + }, + }, + }, + + // Per-database connection limit + databases_max_connections: { + name: 'Max database connections', + description: 'Maximum number of server connections allowed per database.', + type: 'gauge', + unit: 'short', + sources: { + prometheus: { + expr: 'pgbouncer_databases_max_connections{%(queriesSelector)s}', + }, + }, + }, + }, + } diff --git a/pgbouncer-mixin/signals/stats.libsonnet b/pgbouncer-mixin/signals/stats.libsonnet new file mode 100644 index 000000000..9e4f0f3f4 --- /dev/null +++ b/pgbouncer-mixin/signals/stats.libsonnet @@ -0,0 +1,95 @@ +// Query, transaction, and network statistics signals for PgBouncer (per-database group). +// Selector: job, pgbouncer_cluster, instance, database. aggLevel 'none'; counters default +// to rate() in asTarget(); the average-duration signals are raw (ratio-of-increases). +function(this) + { + filteringSelector: this.filteringSelector, + groupLabels: this.groupLabels, + instanceLabels: this.instanceLabels, + enableLokiLogs: this.enableLokiLogs, + aggLevel: 'none', + aggFunction: 'sum', + alertsInterval: '5m', + discoveryMetric: { + prometheus: 'pgbouncer_stats_queries_pooled_total', + }, + signals: { + // Query statistics + stats_queries_pooled_total: { + name: 'Queries processed', + description: 'Rate of SQL queries pooled by PgBouncer.', + type: 'counter', + unit: 'ops', + sources: { + prometheus: { + expr: 'pgbouncer_stats_queries_pooled_total{%(queriesSelector)s}', + legendCustomTemplate: '{{database}}', + }, + }, + }, + stats_query_avg_duration: { + name: 'Average query duration', + description: 'Average duration of queries processed by PgBouncer per interval.', + type: 'raw', + unit: 'ms', + sources: { + prometheus: { + expr: '1000 * increase(pgbouncer_stats_queries_duration_seconds_total{%(queriesSelector)s}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{%(queriesSelector)s}[$__interval:]), 1)', + legendCustomTemplate: '{{database}}', + }, + }, + }, + + // Transaction statistics + stats_sql_transactions_pooled_total: { + name: 'SQL transaction rate', + description: 'Rate of SQL transactions pooled by PgBouncer.', + type: 'counter', + unit: 'ops', + sources: { + prometheus: { + expr: 'pgbouncer_stats_sql_transactions_pooled_total{%(queriesSelector)s}', + legendCustomTemplate: '{{database}}', + }, + }, + }, + stats_transaction_avg_duration: { + name: 'Average transaction duration', + description: 'Average duration of SQL transactions pooled by PgBouncer per interval.', + type: 'raw', + unit: 'ms', + sources: { + prometheus: { + expr: '1000 * increase(pgbouncer_stats_server_in_transaction_seconds_total{%(queriesSelector)s}[$__interval:]) / clamp_min(increase(pgbouncer_stats_sql_transactions_pooled_total{%(queriesSelector)s}[$__interval:]), 1)', + legendCustomTemplate: '{{database}}', + }, + }, + }, + + // Network statistics + stats_received_bytes_total: { + name: 'Network received', + description: 'Rate of bytes received by PgBouncer from clients.', + type: 'counter', + unit: 'Bps', + sources: { + prometheus: { + expr: 'pgbouncer_stats_received_bytes_total{%(queriesSelector)s}', + legendCustomTemplate: '{{database}} - received', + }, + }, + }, + stats_sent_bytes_total: { + name: 'Network sent', + description: 'Rate of bytes sent by PgBouncer to clients.', + type: 'counter', + unit: 'Bps', + sources: { + prometheus: { + expr: 'pgbouncer_stats_sent_bytes_total{%(queriesSelector)s}', + legendCustomTemplate: '{{database}} - sent', + }, + }, + }, + }, + } diff --git a/pgbouncer-mixin/targets.libsonnet b/pgbouncer-mixin/targets.libsonnet deleted file mode 100644 index 890472155..000000000 --- a/pgbouncer-mixin/targets.libsonnet +++ /dev/null @@ -1,165 +0,0 @@ -local g = import './g.libsonnet'; -local prometheusQuery = g.query.prometheus; -local commonlib = import 'common-lib/common/main.libsonnet'; -local utils = commonlib.utils { - labelsToPanelLegend(labels): std.join(' - ', ['{{%s}}' % [label] for label in labels]), -}; -local lokiQuery = g.query.loki; - -{ - new(this): { - local vars = this.grafana.variables, - local config = this.config, - - clientsWaitingConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'sum(pgbouncer_pools_client_waiting_connections{%(queriesSelector)s})' % vars - ), - activeClientConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'sum(pgbouncer_pools_client_active_connections{%(queriesSelector)s})' % vars - ), - activeServerConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'sum(pgbouncer_pools_server_active_connections{%(queriesSelector)s})' % vars - ), - maxDatabaseConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'sum(pgbouncer_databases_max_connections{%(queriesSelector)s})' % vars - ), - maxUserConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'sum(pgbouncer_config_max_user_connections{%(instanceQueriesSelector)s})' % vars - ), - maxClientConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'sum(pgbouncer_config_max_client_connections{%(instanceQueriesSelector)s})' % vars - ), - queriesPooled: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'rate(pgbouncer_stats_queries_pooled_total{%(queriesSelector)s}[$__rate_interval])' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.legendLabels)), - queryDuration: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - '1000 * increase(pgbouncer_stats_queries_duration_seconds_total{%(queriesSelector)s}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{%(queriesSelector)s}[$__interval:]), 1)' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.legendLabels)), - - networkTrafficRecieved: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'rate(pgbouncer_stats_received_bytes_total{%(queriesSelector)s}[$__rate_interval])' % vars - ) - + prometheusQuery.withLegendFormat('%s - received' % utils.labelsToPanelLegend(this.config.legendLabels)), - - networkTrafficSent: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'rate(pgbouncer_stats_sent_bytes_total{%(queriesSelector)s}[$__rate_interval])' % vars - ) - + prometheusQuery.withLegendFormat('%s - sent' % utils.labelsToPanelLegend(this.config.legendLabels)), - - transactionRate: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'rate(pgbouncer_stats_sql_transactions_pooled_total{%(queriesSelector)s}[$__rate_interval])' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.legendLabels)), - - transactionAverageDuration: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - '1000 * increase(pgbouncer_stats_server_in_transaction_seconds_total{%(queriesSelector)s}[$__interval:]) / clamp_min(increase(pgbouncer_stats_sql_transactions_pooled_total{%(queriesSelector)s}[$__interval:]), 1)' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.legendLabels)), - - serverIdleConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'pgbouncer_pools_server_idle_connections{%(queriesSelector)s}' % vars - ) - + prometheusQuery.withLegendFormat('%s - idle' % utils.labelsToPanelLegend(this.config.legendLabels)), - - serverUsedConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'pgbouncer_pools_server_used_connections{%(queriesSelector)s}' % vars - ) - + prometheusQuery.withLegendFormat('%s - used' % utils.labelsToPanelLegend(this.config.legendLabels)), - - serverTestingConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'pgbouncer_pools_server_testing_connections{%(queriesSelector)s}' % vars - ) - + prometheusQuery.withLegendFormat('%s - testing' % utils.labelsToPanelLegend(this.config.legendLabels)), - - serverLoginConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'pgbouncer_pools_server_login_connections{%(queriesSelector)s}' % vars - ) - + prometheusQuery.withLegendFormat('%s - login' % utils.labelsToPanelLegend(this.config.legendLabels)), - - granularActiveClientConnections: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'pgbouncer_pools_client_active_connections{%(queriesSelector)s}' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.legendLabels)), - - clientsWaiting: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'pgbouncer_pools_client_waiting_connections{%(queriesSelector)s}' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.legendLabels)), - - maxClientWaitTime: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'pgbouncer_pools_client_maxwait_seconds{%(queriesSelector)s}' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.legendLabels)), - - topDatabaseActiveConnection: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'topk by(database, instance, pgbouncer_cluster)($top_database_count, pgbouncer_pools_client_active_connections{%(clusterQuerySelector)s})' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.clusterLegendLabel)), - topDatabaseQueryProcessed: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_queries_pooled_total{%(clusterQuerySelector)s}[$__rate_interval]))' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.clusterLegendLabel)), - topDatabaseQueryDuration: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'topk by(database, instance, pgbouncer_cluster)($top_database_count, 1000 * increase(pgbouncer_stats_queries_duration_seconds_total{%(clusterQuerySelector)s}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{%(clusterQuerySelector)s}[$__interval:]), 1))' % vars - ) - + prometheusQuery.withLegendFormat('%s' % utils.labelsToPanelLegend(this.config.clusterLegendLabel)), - topDatabaseNetworkTrafficReceived: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_received_bytes_total{%(clusterQuerySelector)s}[$__rate_interval]))' % vars - ) - + prometheusQuery.withLegendFormat('%s - received' % utils.labelsToPanelLegend(this.config.clusterLegendLabel)), - topDatabaseNetworkTrafficSent: - prometheusQuery.new( - '${' + vars.datasources.prometheus.name + '}', - 'topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_sent_bytes_total{%(clusterQuerySelector)s}[$__rate_interval]))' % vars - ) - + prometheusQuery.withLegendFormat('%s - sent' % utils.labelsToPanelLegend(this.config.clusterLegendLabel)), - }, -} diff --git a/pgbouncer-mixin/variables.libsonnet b/pgbouncer-mixin/variables.libsonnet index beefbbc96..8225ae771 100644 --- a/pgbouncer-mixin/variables.libsonnet +++ b/pgbouncer-mixin/variables.libsonnet @@ -44,7 +44,7 @@ local utils = commonlib.utils; asc=true, caseInsensitive=false ); - std.mapWithIndex(chainVarProto, utils.chainLabels(groupLabels + instanceLabels, [filteringSelector])), + std.mapWithIndex(chainVarProto, utils.chainLabels(groupLabels + instanceLabels, if filteringSelector != '' then [filteringSelector] else [])), datasources: { prometheus: var.datasource.new('prometheus_datasource', 'prometheus') From 7b94cb4336247c4f6c15583e4d59eaa17a2180c4 Mon Sep 17 00:00:00 2001 From: Greg Pattison Date: Thu, 16 Jul 2026 09:13:04 -0400 Subject: [PATCH 2/4] Fixed enableLoki to use config value --- pgbouncer-mixin/mixin.libsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgbouncer-mixin/mixin.libsonnet b/pgbouncer-mixin/mixin.libsonnet index f9bc627e8..c7f52061b 100644 --- a/pgbouncer-mixin/mixin.libsonnet +++ b/pgbouncer-mixin/mixin.libsonnet @@ -7,7 +7,7 @@ local pgbouncer = { filteringSelector: config.filteringSelector, uid: 'pgbouncer', - enableLokiLogs: true, + enableLokiLogs: config.enableLokiLogs, } ); From 3ab15fdc2857933096a1a8b6e8066c32e6673f5a Mon Sep 17 00:00:00 2001 From: Greg Pattison Date: Fri, 24 Jul 2026 10:25:48 -0400 Subject: [PATCH 3/4] pgbouncer-mixin: convert single-signal panels to asStat()/asTimeSeries() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback (Case 2, the asTarget() feedback). Case 1 did not apply: the signal files already spell out every signal object, with no local signal-builder helper to expand. Single-signal stat and timeSeries panels now use signals...asStat() / .asTimeSeries() + commonlib.panels.generic..stylize() + the unchanged panel-specific mixins, instead of commonlib...base.new(targets=[sig.asTarget()]). Panel title/unit/description are sourced from the signal spec; panel-level withUnit() and inline description/targets are removed. Two signals shared between a stat (no unit) and a timeSeries (with unit) keep their spec matched to the stat and set title/unit at the timeSeries call site via withName()/withUnit(). Panels that must stay target-based (documented in panels.libsonnet): the three multi-signal timeSeries (Network traffic, Server connections, Top databases by network traffic) and the alertList panel — common-lib provides no signal constructor for multi-series traffic panels or alert lists. Verified with a Python per-panel comparator against the grafana/master pre-modernization baseline: every panel of every dashboard matches modulo the excluded editor/id/datasource fields. logs.json is byte-identical. Accepted output deltas on converted panels: 1. one per-query fieldConfig override (matcher byFrameRefID, single unit property == signal unit) plus fieldConfig.defaults.unit (== '' / absent on the unit-less stat panels); 2. converted timeSeries descriptions drop the trailing newline that the legacy ||| block descriptions carried (cosmetic; markdown renders identically). No expr, legend, title, or interval deltas. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dashboards_out/clusterOverview.json | 54 +++- pgbouncer-mixin/dashboards_out/overview.json | 234 ++++++++++++++++-- pgbouncer-mixin/panels.libsonnet | 166 ++++--------- pgbouncer-mixin/signals/config.libsonnet | 4 +- pgbouncer-mixin/signals/connections.libsonnet | 16 +- pgbouncer-mixin/signals/stats.libsonnet | 10 +- 6 files changed, 319 insertions(+), 165 deletions(-) diff --git a/pgbouncer-mixin/dashboards_out/clusterOverview.json b/pgbouncer-mixin/dashboards_out/clusterOverview.json index eea01a26a..52198ea24 100644 --- a/pgbouncer-mixin/dashboards_out/clusterOverview.json +++ b/pgbouncer-mixin/dashboards_out/clusterOverview.json @@ -19,7 +19,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Top databases by current number of active client connections.\n", + "description": "Top databases by current number of active client connections.", "fieldConfig": { "defaults": { "color": { @@ -38,7 +38,21 @@ "steps": [ ] }, "unit": "conn" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Top databases by active connections" + }, + "properties": [ + { + "id": "unit", + "value": "conn" + } + ] + } + ] }, "gridPos": { "h": 6, @@ -98,7 +112,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Top databases by rate of SQL queries pooled by PgBouncer.\n", + "description": "Top databases by rate of SQL queries pooled by PgBouncer.", "fieldConfig": { "defaults": { "color": { @@ -117,7 +131,21 @@ "steps": [ ] }, "unit": "ops" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Top databases by queries processed" + }, + "properties": [ + { + "id": "unit", + "value": "ops" + } + ] + } + ] }, "gridPos": { "h": 6, @@ -158,7 +186,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Top databases by average duration of queries being processed by PgBouncer.\n", + "description": "Top databases by average duration of queries being processed by PgBouncer.", "fieldConfig": { "defaults": { "color": { @@ -177,7 +205,21 @@ "steps": [ ] }, "unit": "s" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Top databases by average query duration" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + } + ] }, "gridPos": { "h": 6, diff --git a/pgbouncer-mixin/dashboards_out/overview.json b/pgbouncer-mixin/dashboards_out/overview.json index 826e5c64c..d8e3bbb57 100644 --- a/pgbouncer-mixin/dashboards_out/overview.json +++ b/pgbouncer-mixin/dashboards_out/overview.json @@ -42,8 +42,23 @@ "value": 20 } ] + }, + "unit": "" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Client waiting connections" + }, + "properties": [ + { + "id": "unit", + "value": "" + } + ] } - } + ] }, "gridPos": { "h": 4, @@ -92,8 +107,23 @@ "value": null } ] + }, + "unit": "" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Active client connections" + }, + "properties": [ + { + "id": "unit", + "value": "" + } + ] } - } + ] }, "gridPos": { "h": 4, @@ -147,8 +177,23 @@ "value": null } ] + }, + "unit": "" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Active server connections" + }, + "properties": [ + { + "id": "unit", + "value": "" + } + ] } - } + ] }, "gridPos": { "h": 4, @@ -202,8 +247,23 @@ "value": null } ] + }, + "unit": "" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Max database connections" + }, + "properties": [ + { + "id": "unit", + "value": "" + } + ] } - } + ] }, "gridPos": { "h": 4, @@ -257,8 +317,23 @@ "value": null } ] + }, + "unit": "" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Max user connections" + }, + "properties": [ + { + "id": "unit", + "value": "" + } + ] } - } + ] }, "gridPos": { "h": 4, @@ -312,8 +387,23 @@ "value": null } ] + }, + "unit": "" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Max client connections" + }, + "properties": [ + { + "id": "unit", + "value": "" + } + ] } - } + ] }, "gridPos": { "h": 4, @@ -364,7 +454,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Rate of SQL queries pooled by PgBouncer.\n", + "description": "Rate of SQL queries pooled by PgBouncer.", "fieldConfig": { "defaults": { "color": { @@ -383,7 +473,21 @@ "steps": [ ] }, "unit": "ops" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Queries processed" + }, + "properties": [ + { + "id": "unit", + "value": "ops" + } + ] + } + ] }, "gridPos": { "h": 6, @@ -424,7 +528,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Average duration of queries being processed by PgBouncer.\n", + "description": "Average duration of queries being processed by PgBouncer.", "fieldConfig": { "defaults": { "color": { @@ -443,7 +547,21 @@ "steps": [ ] }, "unit": "ms" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Queries average duration / $__interval" + }, + "properties": [ + { + "id": "unit", + "value": "ms" + } + ] + } + ] }, "gridPos": { "h": 6, @@ -473,7 +591,7 @@ "format": "time_series", "instant": false, "legendFormat": "{{database}}", - "refId": "Average query duration" + "refId": "Queries average duration / $__interval" } ], "title": "Queries average duration / $__interval", @@ -582,7 +700,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Rate of SQL transactions pooled.\n", + "description": "Rate of SQL transactions pooled.", "fieldConfig": { "defaults": { "color": { @@ -601,7 +719,21 @@ "steps": [ ] }, "unit": "ops" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "SQL transaction rate" + }, + "properties": [ + { + "id": "unit", + "value": "ops" + } + ] + } + ] }, "gridPos": { "h": 6, @@ -642,7 +774,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Average duration of SQL transactions pooled.\n", + "description": "Average duration of SQL transactions pooled.", "fieldConfig": { "defaults": { "color": { @@ -661,7 +793,21 @@ "steps": [ ] }, "unit": "ms" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "SQL average transaction duration / $__interval" + }, + "properties": [ + { + "id": "unit", + "value": "ms" + } + ] + } + ] }, "gridPos": { "h": 6, @@ -691,7 +837,7 @@ "format": "time_series", "instant": false, "legendFormat": "{{database}}", - "refId": "Average transaction duration" + "refId": "SQL average transaction duration / $__interval" } ], "title": "SQL average transaction duration / $__interval", @@ -820,7 +966,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Current number of active client connections.\n", + "description": "Current number of active client connections.", "fieldConfig": { "defaults": { "color": { @@ -839,7 +985,21 @@ "steps": [ ] }, "unit": "conn" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Active client connections" + }, + "properties": [ + { + "id": "unit", + "value": "conn" + } + ] + } + ] }, "gridPos": { "h": 6, @@ -880,7 +1040,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Current number of client connections waiting on a server connection.\n", + "description": "Current number of client connections waiting on a server connection.", "fieldConfig": { "defaults": { "color": { @@ -899,7 +1059,21 @@ "steps": [ ] }, "unit": "clients" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Waiting clients" + }, + "properties": [ + { + "id": "unit", + "value": "clients" + } + ] + } + ] }, "gridPos": { "h": 6, @@ -929,7 +1103,7 @@ "format": "time_series", "instant": false, "legendFormat": "{{database}}", - "refId": "Client waiting connections" + "refId": "Waiting clients" } ], "title": "Waiting clients", @@ -940,7 +1114,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "description": "Age of the oldest unserved client connection in seconds.\n", + "description": "Age of the oldest unserved client connection in seconds.", "fieldConfig": { "defaults": { "color": { @@ -959,7 +1133,21 @@ "steps": [ ] }, "unit": "s" - } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "Max client wait time" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + } + ] }, "gridPos": { "h": 6, diff --git a/pgbouncer-mixin/panels.libsonnet b/pgbouncer-mixin/panels.libsonnet index ada9b7d51..58d9b5ab6 100644 --- a/pgbouncer-mixin/panels.libsonnet +++ b/pgbouncer-mixin/panels.libsonnet @@ -7,15 +7,12 @@ local commonlib = import 'common-lib/common/main.libsonnet'; local stat = g.panel.stat, local alertList = g.panel.alertList, - // create stat panel using commonlib + // Single-signal stat panels: signal.asStat() + generic stat stylize() reproduces + // the legacy commonlib.panels.generic.stat..new(...) styling, with title, + // unit and description sourced from the signal spec. clientsWaitingConnections: - commonlib.panels.generic.stat.base.new( - 'Client waiting connections', - targets=[ - signals.connections.pools_client_waiting_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), - ], - description='Current number of client connections waiting on a server connection.' - ) + signals.connections.pools_client_waiting_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + + commonlib.panels.generic.stat.base.stylize() + stat.options.withGraphMode('none') + stat.standardOptions.color.withMode('thresholds') + stat.standardOptions.thresholds.withSteps([ @@ -27,71 +24,36 @@ local commonlib = import 'common-lib/common/main.libsonnet'; + stat.thresholdStep.withValue(20), ]), activeClientConnections: - commonlib.panels.generic.stat.info.new( - 'Active client connections', - targets=[ - signals.connections.pools_client_active_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), - ], - description='Current number of active client connections.' - ) + signals.connections.pools_client_active_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), activeServerConnections: - commonlib.panels.generic.stat.info.new( - 'Active server connections', - targets=[ - signals.connections.pools_server_active_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), - ], - description='Current number of client connections that are linked to a server connection and able to process queries.' - ) + signals.connections.pools_server_active_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), maxDatabaseConnections: - commonlib.panels.generic.stat.info.new( - 'Max database connections', - targets=[ - signals.connections.databases_max_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), - ], - description='Maximum number of allowed connections for database.' - ) + signals.connections.databases_max_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), maxUserConnections: - commonlib.panels.generic.stat.info.new( - 'Max user connections', - targets=[ - signals.config.config_max_user_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), - ], - description='Maximum number of server connections per user allowed.' - ) + signals.config.config_max_user_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), maxClientConnections: - commonlib.panels.generic.stat.info.new( - 'Max client connections', - targets=[ - signals.config.config_max_client_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asTarget(), - ], - description='Maximum number of client connections allowed.' - ) + signals.config.config_max_client_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), + // Single-signal timeSeries panels: signal.asTimeSeries() + generic timeSeries stylize(). queriesPooled: - commonlib.panels.generic.timeSeries.base.new( - 'Queries processed', - targets=[signals.stats.stats_queries_pooled_total.asTarget()], - description=||| - Rate of SQL queries pooled by PgBouncer. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('ops'), + signals.stats.stats_queries_pooled_total.asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), queryDuration: - commonlib.panels.generic.timeSeries.base.new( - 'Queries average duration / $__interval', - targets=[signals.stats.stats_query_avg_duration.asTarget()], - description=||| - Average duration of queries being processed by PgBouncer. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('ms'), + signals.stats.stats_query_avg_duration.asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), + // Multi-signal network traffic panel keeps target-based construction. networkTraffic: commonlib.panels.network.timeSeries.traffic.new( 'Network traffic', @@ -108,25 +70,14 @@ local commonlib = import 'common-lib/common/main.libsonnet'; + g.panel.timeSeries.options.legend.withPlacement('right'), transactionRate: - commonlib.panels.generic.timeSeries.base.new( - 'SQL transaction rate', - targets=[signals.stats.stats_sql_transactions_pooled_total.asTarget()], - description=||| - Rate of SQL transactions pooled. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('ops'), + signals.stats.stats_sql_transactions_pooled_total.asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), transactionAverageDuration: - commonlib.panels.generic.timeSeries.base.new( - 'SQL average transaction duration / $__interval', - targets=[signals.stats.stats_transaction_avg_duration.asTarget()], - description=||| - Average duration of SQL transactions pooled. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('ms'), + signals.stats.stats_transaction_avg_duration.asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), + // Multi-signal server connection states panel keeps target-based construction. serverConnections: commonlib.panels.generic.timeSeries.base.new( 'Server connections', @@ -144,67 +95,36 @@ local commonlib = import 'common-lib/common/main.libsonnet'; + g.panel.timeSeries.options.legend.withDisplayMode('table') + g.panel.timeSeries.options.legend.withPlacement('right'), + // Shares the "Active client connections" signal with the stat panel above; the + // granular timeSeries view supplies its own unit. granularActiveClientConnections: - commonlib.panels.generic.timeSeries.base.new( - 'Active client connections', - targets=[signals.connections.pools_client_active_connections.asTarget()], - description=||| - Current number of active client connections. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('conn'), + signals.connections.pools_client_active_connections.withUnit('conn').asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), + // Shares the "Client waiting connections" signal; the timeSeries view uses a + // different title ('Waiting clients') and unit ('clients'). clientsWaiting: - commonlib.panels.generic.timeSeries.base.new( - 'Waiting clients', - targets=[signals.connections.pools_client_waiting_connections.asTarget()], - description=||| - Current number of client connections waiting on a server connection. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('clients'), + signals.connections.pools_client_waiting_connections.withName('Waiting clients').withUnit('clients').asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), maxClientWaitTime: - commonlib.panels.generic.timeSeries.base.new( - 'Max client wait time', - targets=[signals.connections.pools_client_maxwait_seconds.asTarget()], - description=||| - Age of the oldest unserved client connection in seconds. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('s'), + signals.connections.pools_client_maxwait_seconds.asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), alertsPanel: alertList.new('PgBouncer alerts') + alertList.options.UnifiedAlertListOptions.withAlertInstanceLabelFilter(this.grafana.variables.queriesGroupSelectorAdvanced), topDatabaseActiveConnection: - commonlib.panels.generic.timeSeries.base.new( - 'Top databases by active connections', - targets=[signals.cluster.top_database_active_connection.asTarget()], - description=||| - Top databases by current number of active client connections. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('conn'), + signals.cluster.top_database_active_connection.asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), topDatabaseQueryPooled: - commonlib.panels.generic.timeSeries.base.new( - 'Top databases by queries processed', - targets=[signals.cluster.top_database_query_processed.asTarget()], - description=||| - Top databases by rate of SQL queries pooled by PgBouncer. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('ops'), + signals.cluster.top_database_query_processed.asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), topDatabaseQueryDuration: - commonlib.panels.generic.timeSeries.base.new( - 'Top databases by average query duration', - targets=[signals.cluster.top_database_query_duration.asTarget()], - description=||| - Top databases by average duration of queries being processed by PgBouncer. - ||| - ) - + g.panel.timeSeries.standardOptions.withUnit('s'), + signals.cluster.top_database_query_duration.asTimeSeries() + + commonlib.panels.generic.timeSeries.base.stylize(), + // Multi-signal top-database network panel keeps target-based construction. topDatabaseNetworkTraffic: commonlib.panels.generic.timeSeries.base.new( 'Top databases by network traffic', diff --git a/pgbouncer-mixin/signals/config.libsonnet b/pgbouncer-mixin/signals/config.libsonnet index a4758402c..cf2d2430d 100644 --- a/pgbouncer-mixin/signals/config.libsonnet +++ b/pgbouncer-mixin/signals/config.libsonnet @@ -19,7 +19,7 @@ function(this) name: 'Max user connections', description: 'Maximum number of server connections per user allowed.', type: 'gauge', - unit: 'short', + unit: '', sources: { prometheus: { expr: 'pgbouncer_config_max_user_connections{%(queriesSelector)s}', @@ -30,7 +30,7 @@ function(this) name: 'Max client connections', description: 'Maximum number of client connections allowed.', type: 'gauge', - unit: 'short', + unit: '', sources: { prometheus: { expr: 'pgbouncer_config_max_client_connections{%(queriesSelector)s}', diff --git a/pgbouncer-mixin/signals/connections.libsonnet b/pgbouncer-mixin/signals/connections.libsonnet index 81d7fa25c..01b30cd04 100644 --- a/pgbouncer-mixin/signals/connections.libsonnet +++ b/pgbouncer-mixin/signals/connections.libsonnet @@ -19,7 +19,9 @@ function(this) name: 'Client waiting connections', description: 'Current number of client connections waiting on a server connection.', type: 'gauge', - unit: 'short', + // unit '' matches the legacy "Client waiting connections" stat panel (no unit). + // The "Waiting clients" timeSeries panel supplies its own unit at the call site. + unit: '', sources: { prometheus: { expr: 'pgbouncer_pools_client_waiting_connections{%(queriesSelector)s}', @@ -31,7 +33,9 @@ function(this) name: 'Active client connections', description: 'Current number of active client connections.', type: 'gauge', - unit: 'short', + // unit '' matches the legacy "Active client connections" stat panel (no unit). + // The granular timeSeries panel supplies its own unit ('conn') at the call site. + unit: '', sources: { prometheus: { expr: 'pgbouncer_pools_client_active_connections{%(queriesSelector)s}', @@ -55,9 +59,9 @@ function(this) // Server connection states pools_server_active_connections: { name: 'Active server connections', - description: 'Current number of server connections linked to a client connection.', + description: 'Current number of client connections that are linked to a server connection and able to process queries.', type: 'gauge', - unit: 'short', + unit: '', sources: { prometheus: { expr: 'pgbouncer_pools_server_active_connections{%(queriesSelector)s}', @@ -116,9 +120,9 @@ function(this) // Per-database connection limit databases_max_connections: { name: 'Max database connections', - description: 'Maximum number of server connections allowed per database.', + description: 'Maximum number of allowed connections for database.', type: 'gauge', - unit: 'short', + unit: '', sources: { prometheus: { expr: 'pgbouncer_databases_max_connections{%(queriesSelector)s}', diff --git a/pgbouncer-mixin/signals/stats.libsonnet b/pgbouncer-mixin/signals/stats.libsonnet index 9e4f0f3f4..f6e6fa507 100644 --- a/pgbouncer-mixin/signals/stats.libsonnet +++ b/pgbouncer-mixin/signals/stats.libsonnet @@ -28,8 +28,8 @@ function(this) }, }, stats_query_avg_duration: { - name: 'Average query duration', - description: 'Average duration of queries processed by PgBouncer per interval.', + name: 'Queries average duration / $__interval', + description: 'Average duration of queries being processed by PgBouncer.', type: 'raw', unit: 'ms', sources: { @@ -43,7 +43,7 @@ function(this) // Transaction statistics stats_sql_transactions_pooled_total: { name: 'SQL transaction rate', - description: 'Rate of SQL transactions pooled by PgBouncer.', + description: 'Rate of SQL transactions pooled.', type: 'counter', unit: 'ops', sources: { @@ -54,8 +54,8 @@ function(this) }, }, stats_transaction_avg_duration: { - name: 'Average transaction duration', - description: 'Average duration of SQL transactions pooled by PgBouncer per interval.', + name: 'SQL average transaction duration / $__interval', + description: 'Average duration of SQL transactions pooled.', type: 'raw', unit: 'ms', sources: { From 76aeeadb42404f3b26e8de1a422cdd0b1309c5cc Mon Sep 17 00:00:00 2001 From: Greg Pattison Date: Thu, 6 Aug 2026 10:35:27 -0400 Subject: [PATCH 4/4] pgbouncer-mixin: convert raw signals to typed, move query shape into signal specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the "everything is type: raw" review feedback. Raw signals drop from 7/22 (32%) to 3/24 (12%). signals/cluster.libsonnet: four of the five "top database" signals were raw with the whole legacy expression baked in. They are now typed — top_database_active_ connection as gauge, and top_database_query_processed / _network_received / _network_sent as counter (rate is the framework default) — with the topk by-clause and $top_database_count supplied through exprWrappers. The wrapper is written out inline on each signal rather than hoisted into a local. top_database_query_duration stays raw, as do stats_query_avg_duration and stats_transaction_avg_duration: all three are a ratio of two range vectors (1000 * increase(a) / clamp_min(increase(b), 1)), which no typed signal can express. Only the topk wrapper is factored out of the cluster one. Query shape moved out of panels.libsonnet and into the signal specs. The stat panels need a bare sum() with no by-clause, which the aggregation template cannot emit, so those signals carry exprWrappers: [['sum(', ')']] and an empty legendCustomTemplate. Where one metric is shown both as a summed stat and as per-database series, each view is now its own signal (the _total suffix marks the summed one) instead of one signal mutated at two call sites. panels.libsonnet now has no call-site signal mutation at all: every single-signal panel is signals...asStat()/.asTimeSeries() + stylize(). Intentional semantic change: the three hand-written raw signals that use increase() now carry the same window the framework emits for a typed increase counter — [$__interval:] offset -$__interval, in both the numerator and the denominator of each ratio. The negative offset shifts the window forward so the newest bucket is complete rather than partial, so "Queries average duration", "SQL average transaction duration" and "Top databases by average query duration" stop under-reporting at the right-hand edge. Both halves of each ratio shift by the same amount, so the ratio stays coherent. Verified with a Python leaf-path comparator against both the branch HEAD baseline and the grafana/master pre-modernization baseline. Against master, every panel matches on title, gridPos, panel type, legend and target count, with these categories of delta and nothing else: 21 expr: trailing-comma / whitespace only 10 description: trailing newline dropped (cosmetic, markdown identical) 6 stat panel unit: None <-> '' (renders identically) 3 expr: + offset -$__interval (the intentional change above) The offset exprs were re-compared with the offset tokens stripped back out and then match master exactly, confirming it is the only semantic addition. prometheus_rules_out/ is byte-unchanged; the alerts are threshold comparisons on instant gauges with no range functions and reference no signals. make fmt, make lint and make -B all are clean and the build is reproducible. Co-Authored-By: Claude Opus 5 (1M context) --- .../dashboards_out/clusterOverview.json | 10 ++-- pgbouncer-mixin/dashboards_out/overview.json | 4 +- pgbouncer-mixin/panels.libsonnet | 25 ++++----- pgbouncer-mixin/signals/cluster.libsonnet | 32 +++++++---- pgbouncer-mixin/signals/config.libsonnet | 10 +++- pgbouncer-mixin/signals/connections.libsonnet | 56 +++++++++++++++---- pgbouncer-mixin/signals/stats.libsonnet | 4 +- 7 files changed, 92 insertions(+), 49 deletions(-) diff --git a/pgbouncer-mixin/dashboards_out/clusterOverview.json b/pgbouncer-mixin/dashboards_out/clusterOverview.json index 52198ea24..7e0502139 100644 --- a/pgbouncer-mixin/dashboards_out/clusterOverview.json +++ b/pgbouncer-mixin/dashboards_out/clusterOverview.json @@ -78,7 +78,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, pgbouncer_pools_client_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"})", + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, \n pgbouncer_pools_client_active_connections{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}\n)", "format": "time_series", "instant": false, "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}}", @@ -171,7 +171,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__rate_interval]))", + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, \n rate(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__rate_interval])\n)", "format": "time_series", "instant": false, "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}}", @@ -245,7 +245,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, 1000 * increase(pgbouncer_stats_queries_duration_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__interval:]), 1))", + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, \n 1000 * increase(pgbouncer_stats_queries_duration_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__interval:] offset -$__interval) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__interval:] offset -$__interval), 1)\n)", "format": "time_series", "instant": false, "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}}", @@ -306,7 +306,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_received_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__rate_interval]))", + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, \n rate(pgbouncer_stats_received_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__rate_interval])\n)", "format": "time_series", "instant": false, "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}} - received", @@ -317,7 +317,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_sent_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__rate_interval]))", + "expr": "topk by(database, instance, pgbouncer_cluster)($top_database_count, \n rate(pgbouncer_stats_sent_bytes_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\"}[$__rate_interval])\n)", "format": "time_series", "instant": false, "legendFormat": "{{pgbouncer_cluster}} - {{instance}} - {{database}} - sent", diff --git a/pgbouncer-mixin/dashboards_out/overview.json b/pgbouncer-mixin/dashboards_out/overview.json index d8e3bbb57..5f1a294e4 100644 --- a/pgbouncer-mixin/dashboards_out/overview.json +++ b/pgbouncer-mixin/dashboards_out/overview.json @@ -587,7 +587,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "1000 * increase(pgbouncer_stats_queries_duration_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:]), 1)", + "expr": "1000 * increase(pgbouncer_stats_queries_duration_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:] offset -$__interval) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:] offset -$__interval), 1)", "format": "time_series", "instant": false, "legendFormat": "{{database}}", @@ -833,7 +833,7 @@ "type": "prometheus", "uid": "${prometheus_datasource}" }, - "expr": "1000 * increase(pgbouncer_stats_server_in_transaction_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:]) / clamp_min(increase(pgbouncer_stats_sql_transactions_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:]), 1)", + "expr": "1000 * increase(pgbouncer_stats_server_in_transaction_seconds_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:] offset -$__interval) / clamp_min(increase(pgbouncer_stats_sql_transactions_pooled_total{job=~\"$job\",pgbouncer_cluster=~\"$pgbouncer_cluster\",instance=~\"$instance\",database=~\"$database\"}[$__interval:] offset -$__interval), 1)", "format": "time_series", "instant": false, "legendFormat": "{{database}}", diff --git a/pgbouncer-mixin/panels.libsonnet b/pgbouncer-mixin/panels.libsonnet index 58d9b5ab6..88f711abd 100644 --- a/pgbouncer-mixin/panels.libsonnet +++ b/pgbouncer-mixin/panels.libsonnet @@ -7,11 +7,10 @@ local commonlib = import 'common-lib/common/main.libsonnet'; local stat = g.panel.stat, local alertList = g.panel.alertList, - // Single-signal stat panels: signal.asStat() + generic stat stylize() reproduces - // the legacy commonlib.panels.generic.stat..new(...) styling, with title, - // unit and description sourced from the signal spec. + // Single-signal stat panels: signal.asStat() + generic stat stylize(). Title, unit, + // description and query all come from the signal spec; only styling lives here. clientsWaitingConnections: - signals.connections.pools_client_waiting_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + signals.connections.pools_client_waiting_connections_total.asStat() + commonlib.panels.generic.stat.base.stylize() + stat.options.withGraphMode('none') + stat.standardOptions.color.withMode('thresholds') @@ -24,23 +23,23 @@ local commonlib = import 'common-lib/common/main.libsonnet'; + stat.thresholdStep.withValue(20), ]), activeClientConnections: - signals.connections.pools_client_active_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + signals.connections.pools_client_active_connections_total.asStat() + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), activeServerConnections: - signals.connections.pools_server_active_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + signals.connections.pools_server_active_connections.asStat() + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), maxDatabaseConnections: - signals.connections.databases_max_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + signals.connections.databases_max_connections.asStat() + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), maxUserConnections: - signals.config.config_max_user_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + signals.config.config_max_user_connections.asStat() + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), maxClientConnections: - signals.config.config_max_client_connections.withExprWrappersMixin(['sum(', ')']).withLegendFormat('').asStat() + signals.config.config_max_client_connections.asStat() + commonlib.panels.generic.stat.info.stylize() + stat.options.withGraphMode('none'), @@ -95,16 +94,12 @@ local commonlib = import 'common-lib/common/main.libsonnet'; + g.panel.timeSeries.options.legend.withDisplayMode('table') + g.panel.timeSeries.options.legend.withPlacement('right'), - // Shares the "Active client connections" signal with the stat panel above; the - // granular timeSeries view supplies its own unit. granularActiveClientConnections: - signals.connections.pools_client_active_connections.withUnit('conn').asTimeSeries() + signals.connections.pools_client_active_connections.asTimeSeries() + commonlib.panels.generic.timeSeries.base.stylize(), - // Shares the "Client waiting connections" signal; the timeSeries view uses a - // different title ('Waiting clients') and unit ('clients'). clientsWaiting: - signals.connections.pools_client_waiting_connections.withName('Waiting clients').withUnit('clients').asTimeSeries() + signals.connections.pools_client_waiting_connections.asTimeSeries() + commonlib.panels.generic.timeSeries.base.stylize(), maxClientWaitTime: diff --git a/pgbouncer-mixin/signals/cluster.libsonnet b/pgbouncer-mixin/signals/cluster.libsonnet index c12c5ad2d..0fbe73258 100644 --- a/pgbouncer-mixin/signals/cluster.libsonnet +++ b/pgbouncer-mixin/signals/cluster.libsonnet @@ -1,7 +1,8 @@ // Cluster-overview "top database" signals for PgBouncer. -// These reproduce the legacy topk panels: the inner selector is group-only -// (job, pgbouncer_cluster via %(queriesSelectorGroupOnly)s) and the topk by-clause / -// $top_database_count parameter are baked into raw expressions. +// The inner selector is group-only (job, pgbouncer_cluster via +// %(queriesSelectorGroupOnly)s). The topk by-clause and the $top_database_count +// dashboard variable are supplied through exprWrappers so the metric expression +// itself stays a plain typed signal. function(this) { filteringSelector: this.filteringSelector, @@ -18,11 +19,12 @@ function(this) top_database_active_connection: { name: 'Top databases by active connections', description: 'Top databases by current number of active client connections.', - type: 'raw', + type: 'gauge', unit: 'conn', sources: { prometheus: { - expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, pgbouncer_pools_client_active_connections{%(queriesSelectorGroupOnly)s})', + expr: 'pgbouncer_pools_client_active_connections{%(queriesSelectorGroupOnly)s}', + exprWrappers: [['topk by(database, instance, pgbouncer_cluster)($top_database_count, ', ')']], legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}}', }, }, @@ -30,15 +32,18 @@ function(this) top_database_query_processed: { name: 'Top databases by queries processed', description: 'Top databases by rate of SQL queries pooled by PgBouncer.', - type: 'raw', + type: 'counter', unit: 'ops', sources: { prometheus: { - expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_queries_pooled_total{%(queriesSelectorGroupOnly)s}[$__rate_interval]))', + expr: 'pgbouncer_stats_queries_pooled_total{%(queriesSelectorGroupOnly)s}', + exprWrappers: [['topk by(database, instance, pgbouncer_cluster)($top_database_count, ', ')']], legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}}', }, }, }, + // Raw: ratio of two range vectors, which no typed signal can express. + // Only the topk wrapper is factored out. top_database_query_duration: { name: 'Top databases by average query duration', description: 'Top databases by average duration of queries being processed by PgBouncer.', @@ -46,7 +51,8 @@ function(this) unit: 's', sources: { prometheus: { - expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, 1000 * increase(pgbouncer_stats_queries_duration_seconds_total{%(queriesSelectorGroupOnly)s}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{%(queriesSelectorGroupOnly)s}[$__interval:]), 1))', + expr: '1000 * increase(pgbouncer_stats_queries_duration_seconds_total{%(queriesSelectorGroupOnly)s}[$__interval:] offset -$__interval) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{%(queriesSelectorGroupOnly)s}[$__interval:] offset -$__interval), 1)', + exprWrappers: [['topk by(database, instance, pgbouncer_cluster)($top_database_count, ', ')']], legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}}', }, }, @@ -54,11 +60,12 @@ function(this) top_database_network_received: { name: 'Top databases by network traffic received', description: 'Top databases by volume of network traffic received.', - type: 'raw', + type: 'counter', unit: 'Bps', sources: { prometheus: { - expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_received_bytes_total{%(queriesSelectorGroupOnly)s}[$__rate_interval]))', + expr: 'pgbouncer_stats_received_bytes_total{%(queriesSelectorGroupOnly)s}', + exprWrappers: [['topk by(database, instance, pgbouncer_cluster)($top_database_count, ', ')']], legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}} - received', }, }, @@ -66,11 +73,12 @@ function(this) top_database_network_sent: { name: 'Top databases by network traffic sent', description: 'Top databases by volume of network traffic sent.', - type: 'raw', + type: 'counter', unit: 'Bps', sources: { prometheus: { - expr: 'topk by(database, instance, pgbouncer_cluster)($top_database_count, rate(pgbouncer_stats_sent_bytes_total{%(queriesSelectorGroupOnly)s}[$__rate_interval]))', + expr: 'pgbouncer_stats_sent_bytes_total{%(queriesSelectorGroupOnly)s}', + exprWrappers: [['topk by(database, instance, pgbouncer_cluster)($top_database_count, ', ')']], legendCustomTemplate: '{{pgbouncer_cluster}} - {{instance}} - {{database}} - sent', }, }, diff --git a/pgbouncer-mixin/signals/config.libsonnet b/pgbouncer-mixin/signals/config.libsonnet index cf2d2430d..77b16d0bf 100644 --- a/pgbouncer-mixin/signals/config.libsonnet +++ b/pgbouncer-mixin/signals/config.libsonnet @@ -1,7 +1,9 @@ // Per-instance configuration limit signals for PgBouncer. // These are instance-scoped settings (not per-database), so instanceLabels is -// pureInstanceLabels (['instance']) -> selector job, pgbouncer_cluster, instance, -// reproducing the legacy instanceQueriesSelector. aggLevel 'none'; panels supply sum(). +// pureInstanceLabels (['instance']) -> selector job, pgbouncer_cluster, instance. +// Both signals feed a stat panel showing one total across the selection, so they carry +// a bare sum() (no by-clause, which the agg template cannot express) via exprWrappers +// and an empty legend. function(this) { filteringSelector: this.filteringSelector, @@ -23,6 +25,8 @@ function(this) sources: { prometheus: { expr: 'pgbouncer_config_max_user_connections{%(queriesSelector)s}', + exprWrappers: [['sum(', ')']], + legendCustomTemplate: '', }, }, }, @@ -34,6 +38,8 @@ function(this) sources: { prometheus: { expr: 'pgbouncer_config_max_client_connections{%(queriesSelector)s}', + exprWrappers: [['sum(', ')']], + legendCustomTemplate: '', }, }, }, diff --git a/pgbouncer-mixin/signals/connections.libsonnet b/pgbouncer-mixin/signals/connections.libsonnet index 01b30cd04..8dc523423 100644 --- a/pgbouncer-mixin/signals/connections.libsonnet +++ b/pgbouncer-mixin/signals/connections.libsonnet @@ -1,6 +1,12 @@ // Connection pool signals for PgBouncer (per-database group) // Selector: job, pgbouncer_cluster, instance, database. -// aggLevel is kept 'none' so panels supply aggregation explicitly (matching legacy sum/raw). +// aggLevel is 'none' because these panels plot one series per database rather than an +// aggregate. The stat panels want a single total across the selection, which is a bare +// sum() with no by-clause and so cannot come from the agg template; those signals carry +// it via exprWrappers instead. +// Where the same metric is shown both as a total (stat) and per-database (timeSeries), +// each view is its own signal (the '_total' suffix marks the summed one) so that query +// shape, title, unit and legend all live in the spec rather than at the call site. function(this) { filteringSelector: this.filteringSelector, @@ -15,17 +21,30 @@ function(this) }, signals: { // Client connection states + // Per-database series, plotted over time. pools_client_waiting_connections: { + name: 'Waiting clients', + description: 'Current number of client connections waiting on a server connection.', + type: 'gauge', + unit: 'clients', + sources: { + prometheus: { + expr: 'pgbouncer_pools_client_waiting_connections{%(queriesSelector)s}', + legendCustomTemplate: '{{database}}', + }, + }, + }, + // Single total across the selection, shown as a stat. + pools_client_waiting_connections_total: { name: 'Client waiting connections', description: 'Current number of client connections waiting on a server connection.', type: 'gauge', - // unit '' matches the legacy "Client waiting connections" stat panel (no unit). - // The "Waiting clients" timeSeries panel supplies its own unit at the call site. unit: '', sources: { prometheus: { expr: 'pgbouncer_pools_client_waiting_connections{%(queriesSelector)s}', - legendCustomTemplate: '{{database}}', + exprWrappers: [['sum(', ')']], + legendCustomTemplate: '', }, }, }, @@ -33,9 +52,7 @@ function(this) name: 'Active client connections', description: 'Current number of active client connections.', type: 'gauge', - // unit '' matches the legacy "Active client connections" stat panel (no unit). - // The granular timeSeries panel supplies its own unit ('conn') at the call site. - unit: '', + unit: 'conn', sources: { prometheus: { expr: 'pgbouncer_pools_client_active_connections{%(queriesSelector)s}', @@ -43,6 +60,19 @@ function(this) }, }, }, + pools_client_active_connections_total: { + name: 'Active client connections', + description: 'Current number of active client connections.', + type: 'gauge', + unit: '', + sources: { + prometheus: { + expr: 'pgbouncer_pools_client_active_connections{%(queriesSelector)s}', + exprWrappers: [['sum(', ')']], + legendCustomTemplate: '', + }, + }, + }, pools_client_maxwait_seconds: { name: 'Max client wait time', description: 'Age of the oldest unserved client connection in seconds.', @@ -65,6 +95,8 @@ function(this) sources: { prometheus: { expr: 'pgbouncer_pools_server_active_connections{%(queriesSelector)s}', + exprWrappers: [['sum(', ')']], + legendCustomTemplate: '', }, }, }, @@ -72,7 +104,7 @@ function(this) name: 'Server idle connections', description: 'Current number of server connections idle and ready for a client query.', type: 'gauge', - unit: 'short', + unit: 'conn', sources: { prometheus: { expr: 'pgbouncer_pools_server_idle_connections{%(queriesSelector)s}', @@ -84,7 +116,7 @@ function(this) name: 'Server used connections', description: 'Current number of server connections idle more than server_check_delay.', type: 'gauge', - unit: 'short', + unit: 'conn', sources: { prometheus: { expr: 'pgbouncer_pools_server_used_connections{%(queriesSelector)s}', @@ -96,7 +128,7 @@ function(this) name: 'Server login connections', description: 'Current number of server connections in login phase.', type: 'gauge', - unit: 'short', + unit: 'conn', sources: { prometheus: { expr: 'pgbouncer_pools_server_login_connections{%(queriesSelector)s}', @@ -108,7 +140,7 @@ function(this) name: 'Server testing connections', description: 'Current number of server connections being tested.', type: 'gauge', - unit: 'short', + unit: 'conn', sources: { prometheus: { expr: 'pgbouncer_pools_server_testing_connections{%(queriesSelector)s}', @@ -126,6 +158,8 @@ function(this) sources: { prometheus: { expr: 'pgbouncer_databases_max_connections{%(queriesSelector)s}', + exprWrappers: [['sum(', ')']], + legendCustomTemplate: '', }, }, }, diff --git a/pgbouncer-mixin/signals/stats.libsonnet b/pgbouncer-mixin/signals/stats.libsonnet index f6e6fa507..6ff5f3949 100644 --- a/pgbouncer-mixin/signals/stats.libsonnet +++ b/pgbouncer-mixin/signals/stats.libsonnet @@ -34,7 +34,7 @@ function(this) unit: 'ms', sources: { prometheus: { - expr: '1000 * increase(pgbouncer_stats_queries_duration_seconds_total{%(queriesSelector)s}[$__interval:]) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{%(queriesSelector)s}[$__interval:]), 1)', + expr: '1000 * increase(pgbouncer_stats_queries_duration_seconds_total{%(queriesSelector)s}[$__interval:] offset -$__interval) / clamp_min(increase(pgbouncer_stats_queries_pooled_total{%(queriesSelector)s}[$__interval:] offset -$__interval), 1)', legendCustomTemplate: '{{database}}', }, }, @@ -60,7 +60,7 @@ function(this) unit: 'ms', sources: { prometheus: { - expr: '1000 * increase(pgbouncer_stats_server_in_transaction_seconds_total{%(queriesSelector)s}[$__interval:]) / clamp_min(increase(pgbouncer_stats_sql_transactions_pooled_total{%(queriesSelector)s}[$__interval:]), 1)', + expr: '1000 * increase(pgbouncer_stats_server_in_transaction_seconds_total{%(queriesSelector)s}[$__interval:] offset -$__interval) / clamp_min(increase(pgbouncer_stats_sql_transactions_pooled_total{%(queriesSelector)s}[$__interval:] offset -$__interval), 1)', legendCustomTemplate: '{{database}}', }, },