1use anyhow::{Context, Result};
19use async_trait::async_trait;
20use chrono::{DateTime, Utc};
21use micromegas_analytics::lakehouse::lakehouse_context::LakehouseContext;
22use micromegas_tracing::intern_string::intern_string;
23use micromegas_tracing::prelude::*;
24use micromegas_tracing::property_set::{Property, PropertySet};
25use sqlx::Row;
26use sqlx::postgres::PgRow;
27use std::sync::Arc;
28
29use super::cron_task::TaskCallback;
30
31fn get_counter(row: &PgRow, col: &str) -> Result<u64> {
37 let value: Option<i64> = row.try_get(col).with_context(|| col.to_string())?;
38 Ok(value.unwrap_or(0) as u64)
39}
40
41fn table_tags(table: &str) -> &'static PropertySet {
46 PropertySet::find_or_create(vec![Property::new("table", intern_string(table))])
47}
48
49fn index_tags(table: &str, index: &str) -> &'static PropertySet {
51 PropertySet::find_or_create(vec![
52 Property::new("table", intern_string(table)),
53 Property::new("index", intern_string(index)),
54 ])
55}
56
57fn state_tags(state: &str) -> &'static PropertySet {
59 PropertySet::find_or_create(vec![Property::new("state", intern_string(state))])
60}
61
62async fn collect_index_stats(pool: &sqlx::PgPool) -> Result<()> {
65 let rows = instrument_named!(
66 sqlx::query(
67 "SELECT relname AS table_name,
68 indexrelname AS index_name,
69 idx_scan,
70 idx_tup_read,
71 idx_tup_fetch,
72 pg_relation_size(indexrelid) AS index_size_bytes
73 FROM pg_stat_user_indexes",
74 )
75 .fetch_all(pool),
76 "sql_select_pg_stat_user_indexes"
77 )
78 .await
79 .with_context(|| "querying pg_stat_user_indexes")?;
80
81 for row in rows {
82 let table: String = row.try_get("table_name").with_context(|| "table_name")?;
83 let index: String = row.try_get("index_name").with_context(|| "index_name")?;
84 let tags = index_tags(&table, &index);
85
86 imetric!(
87 "pg_index_scans",
88 "count",
89 tags,
90 get_counter(&row, "idx_scan")?
91 );
92 imetric!(
93 "pg_index_tuples_read",
94 "count",
95 tags,
96 get_counter(&row, "idx_tup_read")?
97 );
98 imetric!(
99 "pg_index_tuples_fetched",
100 "count",
101 tags,
102 get_counter(&row, "idx_tup_fetch")?
103 );
104 imetric!(
105 "pg_index_size_bytes",
106 "bytes",
107 tags,
108 get_counter(&row, "index_size_bytes")?
109 );
110 }
111 Ok(())
112}
113
114async fn collect_table_stats(pool: &sqlx::PgPool) -> Result<()> {
116 let rows = instrument_named!(
117 sqlx::query(
118 "SELECT relname AS table_name,
119 seq_scan,
120 idx_scan,
121 n_live_tup,
122 n_dead_tup,
123 n_tup_ins,
124 n_tup_upd,
125 n_tup_del,
126 extract(epoch from now() - last_autovacuum)::float8 AS seconds_since_autovacuum
127 FROM pg_stat_user_tables",
128 )
129 .fetch_all(pool),
130 "sql_select_pg_stat_user_tables"
131 )
132 .await
133 .with_context(|| "querying pg_stat_user_tables")?;
134
135 for row in rows {
136 let table: String = row.try_get("table_name").with_context(|| "table_name")?;
137 let tags = table_tags(&table);
138
139 imetric!(
140 "pg_table_seq_scans",
141 "count",
142 tags,
143 get_counter(&row, "seq_scan")?
144 );
145 imetric!(
146 "pg_table_idx_scans",
147 "count",
148 tags,
149 get_counter(&row, "idx_scan")?
150 );
151 imetric!(
152 "pg_table_live_tuples",
153 "count",
154 tags,
155 get_counter(&row, "n_live_tup")?
156 );
157 imetric!(
158 "pg_table_dead_tuples",
159 "count",
160 tags,
161 get_counter(&row, "n_dead_tup")?
162 );
163 imetric!(
164 "pg_table_tuples_inserted",
165 "count",
166 tags,
167 get_counter(&row, "n_tup_ins")?
168 );
169 imetric!(
170 "pg_table_tuples_updated",
171 "count",
172 tags,
173 get_counter(&row, "n_tup_upd")?
174 );
175 imetric!(
176 "pg_table_tuples_deleted",
177 "count",
178 tags,
179 get_counter(&row, "n_tup_del")?
180 );
181
182 let seconds_since_autovacuum: Option<f64> = row
183 .try_get("seconds_since_autovacuum")
184 .with_context(|| "seconds_since_autovacuum")?;
185 if let Some(age) = seconds_since_autovacuum {
186 fmetric!("pg_table_seconds_since_autovacuum", "seconds", tags, age);
187 }
188 }
189 Ok(())
190}
191
192async fn collect_database_stats(pool: &sqlx::PgPool) -> Result<()> {
195 let row = instrument_named!(
196 sqlx::query(
197 "SELECT blks_hit,
198 blks_read,
199 xact_commit,
200 xact_rollback,
201 deadlocks,
202 temp_bytes,
203 extract(epoch from stats_reset)::float8 AS stats_reset_epoch
204 FROM pg_stat_database
205 WHERE datname = current_database()",
206 )
207 .fetch_one(pool),
208 "sql_select_pg_stat_database"
209 )
210 .await
211 .with_context(|| "querying pg_stat_database")?;
212
213 imetric!("pg_db_blocks_hit", "count", get_counter(&row, "blks_hit")?);
214 imetric!(
215 "pg_db_blocks_read",
216 "count",
217 get_counter(&row, "blks_read")?
218 );
219 imetric!(
220 "pg_db_xact_commit",
221 "count",
222 get_counter(&row, "xact_commit")?
223 );
224 imetric!(
225 "pg_db_xact_rollback",
226 "count",
227 get_counter(&row, "xact_rollback")?
228 );
229 imetric!("pg_db_deadlocks", "count", get_counter(&row, "deadlocks")?);
230 imetric!(
231 "pg_db_temp_bytes",
232 "bytes",
233 get_counter(&row, "temp_bytes")?
234 );
235
236 let stats_reset_epoch: Option<f64> = row
237 .try_get("stats_reset_epoch")
238 .with_context(|| "stats_reset_epoch")?;
239 if let Some(epoch) = stats_reset_epoch {
240 fmetric!("pg_db_stats_reset_timestamp", "seconds", epoch);
241 }
242 Ok(())
243}
244
245async fn collect_activity_stats(pool: &sqlx::PgPool) -> Result<()> {
249 let state_rows = instrument_named!(
250 sqlx::query(
251 "SELECT coalesce(state, 'unknown') AS state, count(*) AS connections
252 FROM pg_stat_activity
253 WHERE datname = current_database()
254 GROUP BY 1",
255 )
256 .fetch_all(pool),
257 "sql_select_pg_stat_activity_by_state"
258 )
259 .await
260 .with_context(|| "querying pg_stat_activity (by state)")?;
261
262 for row in state_rows {
263 let state: String = row.try_get("state").with_context(|| "state")?;
264 imetric!(
265 "pg_activity_connections",
266 "count",
267 state_tags(&state),
268 get_counter(&row, "connections")?
269 );
270 }
271
272 let oldest_xact_row = instrument_named!(
273 sqlx::query(
274 "SELECT extract(epoch from now() - min(xact_start))::float8 AS oldest_xact_age_seconds
275 FROM pg_stat_activity
276 WHERE datname = current_database()",
277 )
278 .fetch_one(pool),
279 "sql_select_pg_stat_activity_oldest_xact"
280 )
281 .await
282 .with_context(|| "querying pg_stat_activity (oldest xact)")?;
283
284 let oldest_xact_age_seconds: Option<f64> = oldest_xact_row
285 .try_get("oldest_xact_age_seconds")
286 .with_context(|| "oldest_xact_age_seconds")?;
287 if let Some(age) = oldest_xact_age_seconds {
288 fmetric!("pg_activity_oldest_xact_age_seconds", "seconds", age);
289 }
290 Ok(())
291}
292
293fn collect_pool_stats(pool: &sqlx::PgPool) {
296 imetric!("pg_pool_size", "count", pool.size() as u64);
297 imetric!("pg_pool_idle", "count", pool.num_idle() as u64);
298}
299
300pub async fn collect_pg_stats(pool: &sqlx::PgPool) -> Result<()> {
307 let mut first_err: Option<anyhow::Error> = None;
308
309 if let Err(e) = collect_index_stats(pool).await {
310 error!("pg_stats: {e:?}");
311 first_err.get_or_insert(e);
312 }
313 if let Err(e) = collect_table_stats(pool).await {
314 error!("pg_stats: {e:?}");
315 first_err.get_or_insert(e);
316 }
317 if let Err(e) = collect_database_stats(pool).await {
318 error!("pg_stats: {e:?}");
319 first_err.get_or_insert(e);
320 }
321 if let Err(e) = collect_activity_stats(pool).await {
322 error!("pg_stats: {e:?}");
323 first_err.get_or_insert(e);
324 }
325 collect_pool_stats(pool);
326
327 match first_err {
328 Some(e) => Err(e),
329 None => Ok(()),
330 }
331}
332
333pub struct PgStatsTask {
336 pub lakehouse: Arc<LakehouseContext>,
337}
338
339#[async_trait]
340impl TaskCallback for PgStatsTask {
341 #[span_fn]
342 async fn run(&self, _task_scheduled_time: DateTime<Utc>) -> Result<()> {
343 collect_pg_stats(&self.lakehouse.lake().db_pool).await
344 }
345}