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}