Skip to content

Commit b5b0e9b

Browse files
committed
feat(dashboard): add query texts to pg_stat_statements table
Add query_text column to the "Detailed table view" in the Query Performance Analysis dashboard. This allows users to see the actual SQL query text alongside the query metrics. Changes: - Add PostgreSQL sink database connection to Flask backend for fetching query texts from pgss_queryid_queries table - Update process_pgss_data() to include query_text in CSV output - Add query_text column selector to Grafana dashboard with 400px width and text wrapping enabled - Update docker-compose.yml with POSTGRES_SINK_URL env var - Update Helm chart flask-deployment with sink database connection - Add psycopg2-binary dependency to requirements.txt
1 parent 2042d70 commit b5b0e9b

5 files changed

Lines changed: 156 additions & 40 deletions

File tree

config/grafana/dashboards/Dashboard_2_Aggregated_query_analysis.json

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,25 @@
163163
]
164164
}
165165
]
166+
},
167+
{
168+
"matcher": {
169+
"id": "byName",
170+
"options": "Query Text"
171+
},
172+
"properties": [
173+
{
174+
"id": "custom.width",
175+
"value": 400
176+
},
177+
{
178+
"id": "custom.cellOptions",
179+
"value": {
180+
"type": "auto",
181+
"wrapText": true
182+
}
183+
}
184+
]
166185
}
167186
]
168187
},
@@ -200,6 +219,11 @@
200219
"text": "Query ID",
201220
"type": "string"
202221
},
222+
{
223+
"selector": "query_text",
224+
"text": "Query Text",
225+
"type": "string"
226+
},
203227
{
204228
"selector": "duration_seconds",
205229
"text": "Duration (seconds)",
@@ -395,37 +419,38 @@
395419
},
396420
"includeByName": {},
397421
"indexByName": {
398-
"Block read time (ms)": 27,
399-
"Block read time/call (ms)": 29,
400-
"Block read time/sec (ms)": 28,
401-
"Block write time (ms)": 24,
402-
"Block write time/call (ms)": 26,
403-
"Block write time/sec (ms)": 25,
404-
"Calls": 1,
405-
"Calls/sec": 2,
406-
"Duration (seconds)": 30,
407-
"Exec time (ms)": 3,
408-
"Exec time/call (ms)": 5,
409-
"Exec time/sec (ms/s)": 4,
410-
"Planning time (ms)": 6,
411-
"Planning time/call (ms)": 8,
412-
"Planning time/sec (ms/s)": 7,
422+
"Block read time (ms)": 28,
423+
"Block read time/call (ms)": 30,
424+
"Block read time/sec (ms)": 29,
425+
"Block write time (ms)": 25,
426+
"Block write time/call (ms)": 27,
427+
"Block write time/sec (ms)": 26,
428+
"Calls": 2,
429+
"Calls/sec": 3,
430+
"Duration (seconds)": 31,
431+
"Exec time (ms)": 4,
432+
"Exec time/call (ms)": 6,
433+
"Exec time/sec (ms/s)": 5,
434+
"Planning time (ms)": 7,
435+
"Planning time/call (ms)": 9,
436+
"Planning time/sec (ms/s)": 8,
413437
"Query ID": 0,
414-
"Rows": 9,
415-
"Rows/call": 11,
416-
"Rows/sec": 10,
417-
"Shared blocks dirtied": 21,
418-
"Shared blocks dirtied/call": 23,
419-
"Shared blocks dirtied/sec": 22,
420-
"Shared blocks hit": 12,
421-
"Shared blocks hit/call": 14,
422-
"Shared blocks hit/sec": 13,
423-
"Shared blocks read": 15,
424-
"Shared blocks read/call": 17,
425-
"Shared blocks read/sec": 16,
426-
"Shared blocks written": 18,
427-
"Shared blocks written/call": 20,
428-
"Shared blocks written/sec": 19
438+
"Query Text": 1,
439+
"Rows": 10,
440+
"Rows/call": 12,
441+
"Rows/sec": 11,
442+
"Shared blocks dirtied": 22,
443+
"Shared blocks dirtied/call": 24,
444+
"Shared blocks dirtied/sec": 23,
445+
"Shared blocks hit": 13,
446+
"Shared blocks hit/call": 15,
447+
"Shared blocks hit/sec": 14,
448+
"Shared blocks read": 16,
449+
"Shared blocks read/call": 18,
450+
"Shared blocks read/sec": 17,
451+
"Shared blocks written": 19,
452+
"Shared blocks written/call": 21,
453+
"Shared blocks written/sec": 20
429454
},
430455
"renameByName": {}
431456
}

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,10 @@ services:
175175
environment:
176176
- FLASK_ENV=production
177177
- PROMETHEUS_URL=http://sink-prometheus:9090
178+
- POSTGRES_SINK_URL=postgresql://pgwatch@sink-postgres:5432/measurements
178179
depends_on:
179180
- sink-prometheus
181+
- sink-postgres
180182
restart: unless-stopped
181183

182184
# PostgreSQL Reports Generator - Runs reports after 30 minutes

monitoring_flask_backend/app.py

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
from datetime import datetime, timezone, timedelta
66
import logging
77
import os
8+
import psycopg2
9+
import psycopg2.extras
810

911
# Configure logging
1012
logging.basicConfig(level=logging.INFO)
1113
logger = logging.getLogger(__name__)
1214

1315
app = Flask(__name__)
1416

17+
# PostgreSQL sink connection for query text lookups
18+
POSTGRES_SINK_URL = os.environ.get('POSTGRES_SINK_URL', 'postgresql://pgwatch@sink-postgres:5432/measurements')
19+
1520
# Prometheus connection - use environment variable with fallback
1621
PROMETHEUS_URL = os.environ.get('PROMETHEUS_URL', 'http://localhost:8428')
1722

@@ -37,6 +42,64 @@ def get_prometheus_client():
3742
logger.error(f"Failed to connect to Prometheus: {e}")
3843
raise
3944

45+
46+
def get_query_texts_from_sink(db_name: str = None) -> dict:
47+
"""
48+
Fetch queryid-to-query text mappings from the PostgreSQL sink database.
49+
50+
Args:
51+
db_name: Optional database name to filter results
52+
53+
Returns:
54+
Dictionary mapping queryid to query text
55+
"""
56+
query_texts = {}
57+
58+
try:
59+
conn = psycopg2.connect(POSTGRES_SINK_URL)
60+
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
61+
if db_name:
62+
query = """
63+
SELECT DISTINCT ON (data->>'queryid')
64+
data->>'queryid' as queryid,
65+
data->>'query' as query
66+
FROM public.pgss_queryid_queries
67+
WHERE
68+
dbname = %s
69+
AND data->>'queryid' IS NOT NULL
70+
AND data->>'query' IS NOT NULL
71+
ORDER BY data->>'queryid', time DESC
72+
"""
73+
cursor.execute(query, (db_name,))
74+
else:
75+
query = """
76+
SELECT DISTINCT ON (data->>'queryid')
77+
data->>'queryid' as queryid,
78+
data->>'query' as query
79+
FROM public.pgss_queryid_queries
80+
WHERE
81+
data->>'queryid' IS NOT NULL
82+
AND data->>'query' IS NOT NULL
83+
ORDER BY data->>'queryid', time DESC
84+
"""
85+
cursor.execute(query)
86+
87+
for row in cursor:
88+
queryid = row['queryid']
89+
query_text = row['query']
90+
if queryid:
91+
# Truncate very long queries for display
92+
if query_text and len(query_text) > 500:
93+
query_text = query_text[:500] + '...'
94+
query_texts[queryid] = query_text or ''
95+
96+
conn.close()
97+
except Exception as e:
98+
logger.warning(f"Failed to fetch query texts from sink database: {e}")
99+
100+
return query_texts
101+
102+
40103
def read_version_file(filepath, default='unknown'):
41104
"""Read version information from file"""
42105
try:
@@ -182,29 +245,33 @@ def get_pgss_metrics_csv():
182245
logger.warning(f"Failed to query metric {metric}: {e}")
183246
continue
184247

248+
# Fetch query texts from sink database
249+
query_texts = get_query_texts_from_sink(db_name)
250+
logger.info(f"Fetched {len(query_texts)} query texts from sink database")
251+
185252
# Process the data to calculate differences
186-
csv_data = process_pgss_data(start_data, end_data, start_dt, end_dt)
253+
csv_data = process_pgss_data(start_data, end_data, start_dt, end_dt, query_texts)
187254

188255
# Create CSV response
189256
output = io.StringIO()
190257
if csv_data:
191-
# Define explicit field order with queryid first, then duration, then metrics with their rates
192-
base_fields = ['queryid', 'duration_seconds']
258+
# Define explicit field order with queryid first, query_text second, then duration, then metrics with their rates
259+
base_fields = ['queryid', 'query_text', 'duration_seconds']
193260
all_metric_fields = []
194-
261+
195262
# Get metric fields from the mapping in specific order with their rates
196263
desired_order = [
197-
'calls', 'exec_time', 'plan_time', 'rows', 'shared_blks_hit',
264+
'calls', 'exec_time', 'plan_time', 'rows', 'shared_blks_hit',
198265
'shared_blks_read', 'shared_blks_dirtied', 'shared_blks_written',
199266
'blk_read_time', 'blk_write_time'
200267
]
201-
268+
202269
for display_name in desired_order:
203270
if display_name in METRIC_NAME_MAPPING.values():
204271
all_metric_fields.append(display_name)
205272
all_metric_fields.append(f'{display_name}_per_sec')
206273
all_metric_fields.append(f'{display_name}_per_call')
207-
274+
208275
# Combine all fields in desired order
209276
all_fields = base_fields + all_metric_fields
210277

@@ -226,10 +293,20 @@ def get_pgss_metrics_csv():
226293
logger.error(f"Error processing request: {e}")
227294
return jsonify({"error": str(e)}), 500
228295

229-
def process_pgss_data(start_data, end_data, start_time, end_time):
296+
def process_pgss_data(start_data, end_data, start_time, end_time, query_texts=None):
230297
"""
231298
Process pg_stat_statements data and calculate differences between start and end times
299+
300+
Args:
301+
start_data: Prometheus data at start time
302+
end_data: Prometheus data at end time
303+
start_time: Start datetime
304+
end_time: End datetime
305+
query_texts: Optional dictionary mapping queryid to query text
232306
"""
307+
if query_texts is None:
308+
query_texts = {}
309+
233310
# Convert Prometheus data to dictionaries
234311
start_metrics = prometheus_to_dict(start_data, start_time)
235312
end_metrics = prometheus_to_dict(end_data, end_time)
@@ -264,9 +341,10 @@ def process_pgss_data(start_data, end_data, start_time, end_time):
264341
# Fallback to query parameter duration if timestamps are missing
265342
actual_duration = (end_time - start_time).total_seconds()
266343

267-
# Create result row
344+
# Create result row with query text
268345
row = {
269346
'queryid': query_id,
347+
'query_text': query_texts.get(query_id, ''),
270348
'duration_seconds': actual_duration
271349
}
272350

monitoring_flask_backend/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ prometheus-api-client==0.5.4
33
python-dateutil==2.8.2
44
gunicorn==23.0.0
55
requests==2.32.3
6-
pytest==8.3.4
6+
psycopg2-binary==2.9.9
7+
pytest==8.3.4

postgres_ai_helm/templates/flask-deployment.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ spec:
2727
- name: wait-for-victoriametrics
2828
image: busybox:1.36
2929
command: ['sh', '-c', 'until nc -z {{ include "postgres-ai-monitoring.fullname" . }}-victoriametrics {{ .Values.victoriaMetrics.service.port }}; do echo waiting for victoriametrics; sleep 2; done']
30+
- name: wait-for-sink-postgres
31+
image: busybox:1.36
32+
command: ['sh', '-c', 'until nc -z {{ include "postgres-ai-monitoring.fullname" . }}-sink-postgres 5432; do echo waiting for sink-postgres; sleep 2; done']
3033
containers:
3134
- name: flask
3235
image: {{ .Values.flask.image }}
@@ -36,6 +39,13 @@ spec:
3639
value: "production"
3740
- name: PROMETHEUS_URL
3841
value: "http://{{ include "postgres-ai-monitoring.fullname" . }}-victoriametrics:{{ .Values.victoriaMetrics.service.port }}"
42+
- name: POSTGRES_PASSWORD
43+
valueFrom:
44+
secretKeyRef:
45+
name: {{ include "postgres-ai-monitoring.secretName" . }}
46+
key: postgres-password
47+
- name: POSTGRES_SINK_URL
48+
value: "postgresql://{{ .Values.sinkPostgres.user }}:$(POSTGRES_PASSWORD)@{{ include "postgres-ai-monitoring.fullname" . }}-sink-postgres:5432/{{ .Values.sinkPostgres.database }}"
3949
{{- range $key, $value := .Values.flask.env }}
4050
- name: {{ $key }}
4151
value: {{ $value | quote }}

0 commit comments

Comments
 (0)