1use crate::{
2 apis::coredb_types::CoreDB,
3 configmap::{create_configmap_ifnotexist, set_configmap},
4 defaults, Context, Error,
5};
6use kube::Client;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::{collections::BTreeMap, sync::Arc};
10use tracing::debug;
11
12pub const QUERIES_YAML: &str = "queries.yaml";
13pub const EXPORTER_VOLUME: &str = "postgres-exporter";
14pub const EXPORTER_CONFIGMAP: &str = "postgres-exporter";
15
16#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema, Default)]
17#[allow(non_snake_case)]
18pub struct PostgresMetrics {
19 #[serde(default = "defaults::default_postgres_exporter_image")]
20 pub image: String,
21 #[serde(default = "defaults::default_postgres_exporter_enabled")]
22 pub enabled: bool,
23
24 #[schemars(schema_with = "preserve_arbitrary")]
25 pub queries: Option<QueryConfig>,
26}
27
28#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize, Deserialize)]
29pub struct Metric {
30 pub usage: Usage,
31 pub description: String,
32}
33
34#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize, Deserialize)]
35pub struct Metrics {
36 #[serde(flatten)]
37 pub metrics: BTreeMap<String, Metric>,
38}
39
40#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize, Deserialize)]
41pub struct QueryItem {
42 pub query: String,
43 pub master: bool,
44 pub metrics: Vec<Metrics>,
45}
46
47#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize, Deserialize)]
48pub struct QueryConfig {
49 #[serde(flatten)]
50 pub queries: BTreeMap<String, QueryItem>,
51}
52
53fn preserve_arbitrary(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
55 let mut obj = schemars::schema::SchemaObject::default();
56 obj.extensions
57 .insert("x-kubernetes-preserve-unknown-fields".into(), true.into());
58 schemars::schema::Schema::Object(obj)
59}
60
61use std::str::FromStr;
62
63#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize, PartialEq)]
64#[serde(rename_all = "UPPERCASE")]
65pub enum Usage {
66 Counter,
67 Gauge,
68 Histogram,
69 Label,
70}
71
72impl FromStr for Usage {
73 type Err = ();
74
75 fn from_str(input: &str) -> Result<Usage, Self::Err> {
76 match input {
77 "COUNTER" => Ok(Usage::Counter),
78 "GAUGE" => Ok(Usage::Gauge),
79 "HISTOGRAM" => Ok(Usage::Histogram),
80 "LABEL" => Ok(Usage::Label),
81 _ => Err(()),
82 }
83 }
84}
85
86pub async fn create_postgres_exporter_role(cdb: &CoreDB, ctx: Arc<Context>) -> Result<(), Error> {
87 let client = ctx.client.clone();
88 if !(cdb.spec.postgresExporterEnabled) {
89 return Ok(());
90 }
91 debug!(
92 "Creating postgres_exporter role for database {} in namespace {}",
93 cdb.metadata.name.clone().unwrap(),
94 cdb.metadata.namespace.clone().unwrap()
95 );
96 let _ = cdb
98 .psql(
99 "
100 CREATE OR REPLACE FUNCTION __tmp_create_user() returns void as $$
101 BEGIN
102 IF NOT EXISTS (
103 SELECT
104 FROM pg_catalog.pg_user
105 WHERE usename = 'postgres_exporter') THEN
106 CREATE USER postgres_exporter;
107 END IF;
108 END;
109 $$ language plpgsql;
110
111 SELECT __tmp_create_user();
112 DROP FUNCTION __tmp_create_user();
113
114 ALTER USER postgres_exporter SET SEARCH_PATH TO postgres_exporter,pg_catalog;
115 GRANT CONNECT ON DATABASE postgres TO postgres_exporter;
116 GRANT pg_monitor to postgres_exporter;
117 GRANT pg_read_all_stats to postgres_exporter;
118 "
119 .to_string(),
120 "postgres".to_owned(),
121 client.clone(),
122 )
123 .await?;
124 Ok(())
125}
126
127
128pub async fn reconcile_prom_configmap(cdb: &CoreDB, client: Client, ns: &str) -> Result<(), Error> {
129 create_configmap_ifnotexist(client.clone(), ns, EXPORTER_CONFIGMAP).await?;
130 match cdb.spec.metrics.clone().and_then(|m| m.queries) {
132 Some(queries) => {
133 let qdata = serde_yaml::to_string(&queries).unwrap();
134 let d: BTreeMap<String, String> = BTreeMap::from([(QUERIES_YAML.to_string(), qdata)]);
135 set_configmap(client.clone(), ns, EXPORTER_CONFIGMAP, d).await?
136 }
137 None => {
138 debug!("No queries specified in CoreDB spec");
139 }
140 }
141 Ok(())
142}
143
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use serde_yaml;
149
150 #[test]
151 fn query_deserialize_serialize() {
152 let incoming_data = serde_json::json!(
155 {
156 "pg_postmaster": {
157 "query": "SELECT pg_postmaster_start_time as start_time_seconds from pg_postmaster_start_time()",
158 "master": true,
159 "metrics": [
160 {
161 "start_time_seconds": {
162 "usage": "GAUGE",
163 "description": "Time at which postmaster started"
164 }
165 }
166 ]
167 },
168 "extensions": {
169 "query": "select count(*) as num_ext from pg_available_extensions",
170 "master": true,
171 "metrics": [
172 {
173 "num_ext": {
174 "usage": "GAUGE",
175 "description": "Num extensions"
176 }
177 }
178 ]
179 }
180 }
181 );
182
183 let query_config: QueryConfig = serde_json::from_value(incoming_data).expect("failed to deserialize");
184
185 assert!(query_config.queries.contains_key("pg_postmaster"));
186 assert!(query_config.queries.contains_key("extensions"));
187
188 let pg_postmaster = query_config.queries.get("pg_postmaster").unwrap();
189 assert_eq!(
190 pg_postmaster.query,
191 "SELECT pg_postmaster_start_time as start_time_seconds from pg_postmaster_start_time()"
192 );
193 assert!(pg_postmaster.master);
194 assert!(pg_postmaster.metrics[0]
195 .metrics
196 .contains_key("start_time_seconds"));
197
198 let start_time_seconds_metric = pg_postmaster.metrics[0]
199 .metrics
200 .get("start_time_seconds")
201 .unwrap();
202 assert_eq!(
203 start_time_seconds_metric.description,
204 "Time at which postmaster started"
205 );
206
207 let extensions = query_config
208 .queries
209 .get("extensions")
210 .expect("extensions not found");
211 assert_eq!(
212 extensions.query,
213 "select count(*) as num_ext from pg_available_extensions"
214 );
215 assert!(extensions.master);
216 assert!(extensions.metrics[0].metrics.contains_key("num_ext"));
217
218 let yaml = serde_yaml::to_string(&query_config).expect("failed to serialize to yaml");
221
222 let data = r#"extensions:
223 query: select count(*) as num_ext from pg_available_extensions
224 master: true
225 metrics:
226 - num_ext:
227 usage: GAUGE
228 description: Num extensions
229pg_postmaster:
230 query: SELECT pg_postmaster_start_time as start_time_seconds from pg_postmaster_start_time()
231 master: true
232 metrics:
233 - start_time_seconds:
234 usage: GAUGE
235 description: Time at which postmaster started
236"#;
237 assert_eq!(yaml, data);
239 }
240}