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