controller/postgres_exporter.rs
1use crate::{apis::coredb_types::CoreDB, defaults, Error};
2use kube::Client;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6use tracing::debug;
7
8pub const QUERIES: &str = "tembo-queries";
9pub const EXPORTER_VOLUME: &str = "postgres-exporter";
10pub const EXPORTER_CONFIGMAP_PREFIX: &str = "metrics-";
11
12/// PostgresExporter is the configuration for the postgres-exporter to expose
13/// custom metrics from the database.
14///
15/// **Example:** This example exposes specific metrics from a query to a
16/// [pgmq](https://github.com/tembo-io/pgmq) queue enabled database.
17///
18/// ```yaml
19/// apiVersion: coredb.io/v1alpha1
20/// kind: CoreDB
21/// metadata:
22/// name: test-db
23/// spec:
24/// metrics:
25/// enabled: true
26/// image: quay.io/prometheuscommunity/postgres-exporter:v0.12.0
27/// queries:
28/// pgmq:
29/// query: select queue_name, queue_length, oldest_msg_age_sec, newest_msg_age_sec, total_messages from public.pgmq_metrics_all()
30/// master: true
31/// metrics:
32/// - queue_name:
33/// description: Name of the queue
34/// usage: LABEL
35/// - queue_length:
36/// description: Number of messages in the queue
37/// usage: GAUGE
38/// - oldest_msg_age_sec:
39/// description: Age of the oldest message in the queue, in seconds.
40/// usage: GAUGE
41/// - newest_msg_age_sec:
42/// description: Age of the newest message in the queue, in seconds.
43/// usage: GAUGE
44/// - total_messages:
45/// description: Total number of messages that have passed into the queue.
46/// usage: GAUGE
47/// ````
48#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema, Default)]
49#[allow(non_snake_case)]
50pub struct PostgresMetrics {
51 /// The image to use for the postgres-exporter container.
52 ///
53 /// **Default:** `quay.io/prometheuscommunity/postgres-exporter:v0.12.0`
54 #[serde(default = "defaults::default_postgres_exporter_image")]
55 pub image: String,
56
57 /// To enable or disable the metric.
58 ///
59 /// **Default:** `true`
60 #[serde(default = "defaults::default_postgres_exporter_enabled")]
61 pub enabled: bool,
62
63 /// The SQL query to run.
64 ///
65 /// **Example:** `select queue_name, queue_length, oldest_msg_age_sec, newest_msg_age_sec, total_messages from public.pgmq_metrics_all()`
66 ///
67 /// **Default**: `None`
68 #[schemars(schema_with = "preserve_arbitrary")]
69 pub queries: Option<QueryConfig>,
70}
71
72#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize, Deserialize)]
73pub struct Metric {
74 pub usage: Usage,
75 pub description: String,
76}
77
78#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize, Deserialize)]
79pub struct Metrics {
80 #[serde(flatten)]
81 pub metrics: BTreeMap<String, Metric>,
82}
83
84/// **Example**: This example exposes specific metrics from a query to a
85/// [pgmq](https://github.com/tembo-io/pgmq) queue enabled database.
86///
87/// ```yaml
88/// metrics:
89/// enabled: true
90/// image: quay.io/prometheuscommunity/postgres-exporter:v0.12.0
91/// queries:
92/// pgmq:
93/// query: select queue_name, queue_length, oldest_msg_age_sec, newest_msg_age_sec, total_messages from pgmq.metrics_all()
94/// primary: true
95/// metrics:
96/// - queue_name:
97/// description: Name of the queue
98/// usage: LABEL
99/// - queue_length:
100/// description: Number of messages in the queue
101/// usage: GAUGE
102/// - oldest_msg_age_sec:
103/// description: Age of the oldest message in the queue, in seconds.
104/// usage: GAUGE
105/// - newest_msg_age_sec:
106/// description: Age of the newest message in the queue, in seconds.
107/// usage: GAUGE
108/// - total_messages:
109/// description: Total number of messages that have passed into the queue.
110/// usage: GAUGE
111/// target_databases:
112/// - "postgres"
113/// ```
114#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize, Deserialize)]
115pub struct QueryItem {
116 /// the SQL query to run on the target database to generate the metrics
117 pub query: String,
118
119 // We need to support this at some point going forward since master
120 // is now deprecated.
121 // whether to run the query only on the primary instance
122 //pub primary: Option<bool>,
123
124 // same as primary (for compatibility with the Prometheus PostgreSQL
125 // exporter's syntax - **deprecated**)
126 /// whether to run the query only on the master instance
127 /// See [https://cloudnative-pg.io/documentation/1.20/monitoring/#structure-of-a-user-defined-metric](https://cloudnative-pg.io/documentation/1.20/monitoring/#structure-of-a-user-defined-metric)
128 pub master: bool,
129
130 /// the name of the column returned by the query
131 ///
132 /// usage: one of the values described below
133 /// description: the metric's description
134 /// metrics_mapping: the optional column mapping when usage is set to MAPPEDMETRIC
135 pub metrics: Vec<Metrics>,
136
137 /// The default database can always be overridden for a given user-defined
138 /// metric, by specifying a list of one or more databases in the target_databases
139 /// option.
140 ///
141 /// See: [https://cloudnative-pg.io/documentation/1.20/monitoring/#example-of-a-user-defined-metric-running-on-multiple-databases](https://cloudnative-pg.io/documentation/1.20/monitoring/#example-of-a-user-defined-metric-running-on-multiple-databases)
142 ///
143 /// **Default:** `["postgres"]`
144 #[serde(default = "defaults::default_postgres_exporter_target_databases")]
145 pub target_databases: Vec<String>,
146}
147
148#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize, Deserialize)]
149pub struct QueryConfig {
150 #[serde(flatten)]
151 pub queries: BTreeMap<String, QueryItem>,
152}
153
154// source: https://github.com/kube-rs/kube/issues/844
155fn preserve_arbitrary(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
156 let mut obj = schemars::schema::SchemaObject::default();
157 obj.extensions
158 .insert("x-kubernetes-preserve-unknown-fields".into(), true.into());
159 schemars::schema::Schema::Object(obj)
160}
161
162use crate::configmap::apply_configmap;
163
164use std::str::FromStr;
165
166#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize, PartialEq)]
167#[serde(rename_all = "UPPERCASE")]
168pub enum Usage {
169 Counter,
170 Gauge,
171 Histogram,
172 Label,
173}
174
175impl FromStr for Usage {
176 type Err = ();
177
178 fn from_str(input: &str) -> Result<Usage, Self::Err> {
179 match input {
180 "COUNTER" => Ok(Usage::Counter),
181 "GAUGE" => Ok(Usage::Gauge),
182 "HISTOGRAM" => Ok(Usage::Histogram),
183 "LABEL" => Ok(Usage::Label),
184 _ => Err(()),
185 }
186 }
187}
188
189pub async fn reconcile_metrics_configmap(
190 cdb: &CoreDB,
191 client: Client,
192 ns: &str,
193) -> Result<(), Error> {
194 // set custom pg-prom metrics in configmap values if they are specified
195 let coredb_name = cdb
196 .metadata
197 .name
198 .clone()
199 .expect("instance should always have a name");
200 // Make sure we always check for queries in the spec, incase someone calls this function
201 // directly and not through the reconcile function.
202 match cdb.spec.metrics.clone().and_then(|m| m.queries) {
203 Some(queries) => {
204 let qdata = serde_yaml::to_string(&queries)?;
205 let d: BTreeMap<String, String> = BTreeMap::from([(QUERIES.to_string(), qdata)]);
206 apply_configmap(
207 client.clone(),
208 ns,
209 &format!("{}{}", EXPORTER_CONFIGMAP_PREFIX, coredb_name),
210 d,
211 )
212 .await?
213 }
214 None => {
215 debug!("No queries specified in CoreDB spec {}", coredb_name);
216 }
217 }
218 Ok(())
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use serde_yaml;
225
226 #[test]
227 fn query_deserialize_serialize() {
228 // query data received as json. map to struct.
229 // serialize struct to yaml
230 let incoming_data = serde_json::json!(
231 {
232 "pg_postmaster": {
233 "query": "SELECT pg_postmaster_start_time as start_time_seconds from pg_postmaster_start_time()",
234 "master": true,
235 "metrics": [
236 {
237 "start_time_seconds": {
238 "usage": "GAUGE",
239 "description": "Time at which postmaster started"
240 }
241 }
242 ],
243 "target_databases": ["postgres"]
244 },
245 "extensions": {
246 "query": "select count(*) as num_ext from pg_available_extensions",
247 "master": true,
248 "metrics": [
249 {
250 "num_ext": {
251 "usage": "GAUGE",
252 "description": "Num extensions"
253 }
254 }
255 ],
256 "target_databases": ["postgres"]
257 }
258 }
259 );
260
261 let query_config: QueryConfig =
262 serde_json::from_value(incoming_data).expect("failed to deserialize");
263
264 assert!(query_config.queries.contains_key("pg_postmaster"));
265 assert!(query_config.queries.contains_key("extensions"));
266
267 let pg_postmaster = query_config.queries.get("pg_postmaster").unwrap();
268 assert_eq!(
269 pg_postmaster.query,
270 "SELECT pg_postmaster_start_time as start_time_seconds from pg_postmaster_start_time()"
271 );
272 assert!(pg_postmaster.master);
273 assert!(pg_postmaster.metrics[0]
274 .metrics
275 .contains_key("start_time_seconds"));
276
277 let start_time_seconds_metric = pg_postmaster.metrics[0]
278 .metrics
279 .get("start_time_seconds")
280 .unwrap();
281 assert_eq!(
282 start_time_seconds_metric.description,
283 "Time at which postmaster started"
284 );
285
286 let extensions = query_config
287 .queries
288 .get("extensions")
289 .expect("extensions not found");
290 assert_eq!(
291 extensions.query,
292 "select count(*) as num_ext from pg_available_extensions"
293 );
294 assert!(extensions.master);
295 assert!(extensions.metrics[0].metrics.contains_key("num_ext"));
296
297 // yaml to yaml
298
299 let yaml = serde_yaml::to_string(&query_config).expect("failed to serialize to yaml");
300
301 let data = r#"extensions:
302 query: select count(*) as num_ext from pg_available_extensions
303 master: true
304 metrics:
305 - num_ext:
306 usage: GAUGE
307 description: Num extensions
308 target_databases:
309 - postgres
310pg_postmaster:
311 query: SELECT pg_postmaster_start_time as start_time_seconds from pg_postmaster_start_time()
312 master: true
313 metrics:
314 - start_time_seconds:
315 usage: GAUGE
316 description: Time at which postmaster started
317 target_databases:
318 - postgres
319"#;
320 // formmatted correctly as yaml (for configmap)
321 assert_eq!(yaml, data);
322 }
323}