Skip to main content

mz_compute/logging/
prometheus.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Logging dataflow for Prometheus metrics gathered from the metrics registry.
11
12use std::collections::BTreeMap;
13use std::rc::Rc;
14use std::time::{Duration, Instant};
15
16use mz_compute_types::dyncfgs::COMPUTE_PROMETHEUS_INTROSPECTION_SCRAPE_INTERVAL;
17use mz_dyncfg::ConfigSet;
18use mz_ore::cast::{CastFrom, CastLossy};
19use mz_ore::collections::CollectionExt;
20use mz_ore::metrics::MetricsRegistry;
21use mz_ore::soft_panic_or_log;
22use mz_repr::{Datum, Timestamp};
23use mz_row_spine::RowRowBuilder;
24use mz_timely_util::columnar::batcher;
25use mz_timely_util::columnar::builder::ColumnBuilder;
26use mz_timely_util::columnar::{Col2ValBatcher, columnar_exchange};
27use prometheus::proto::MetricType;
28use timely::dataflow::Scope;
29use timely::dataflow::channels::pact::ExchangeCore;
30use timely::dataflow::operators::generic::OutputBuilder;
31use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
32
33use crate::extensions::arrange::MzArrangeCore;
34use crate::logging::{
35    ComputeLog, LogCollection, LogVariant, PermutedRowPacker, downgrade_to_interval_boundary,
36    emit_snapshot_diff,
37};
38use crate::typedefs::RowRowSpine;
39
40/// The return type of [`construct`].
41pub(super) struct Return {
42    /// Collections to export.
43    pub collections: BTreeMap<LogVariant, LogCollection>,
44}
45
46/// Key type for the snapshot: (metric_name, sorted_label_pairs).
47type SnapshotKey = (String, Vec<(String, String)>);
48/// Value type for the snapshot: (value, metric_type, help).
49type SnapshotValue = (f64, &'static str, String);
50
51/// Constructs the logging dataflow fragment for Prometheus metrics.
52pub(super) fn construct(
53    scope: Scope<'_, Timestamp>,
54    config: &mz_compute_client::logging::LoggingConfig,
55    metrics_registry: MetricsRegistry,
56    now: Instant,
57    start_offset: Duration,
58    worker_config: Rc<ConfigSet>,
59    workers_per_process: usize,
60) -> Return {
61    let variant = LogVariant::Compute(ComputeLog::PrometheusMetrics);
62    let mut collections = BTreeMap::new();
63    let interval = config.interval;
64    let interval_ms = std::cmp::max(1, interval.as_millis());
65
66    if !config.index_logs.contains_key(&variant) {
67        return Return { collections };
68    }
69
70    let process_id = scope.index() / workers_per_process;
71    let enable = scope.index() % workers_per_process == 0;
72
73    // Build a source operator that periodically gathers Prometheus metrics
74    // and packs them directly into Row pairs.
75    let mut builder = OperatorBuilder::new("PrometheusMetrics".to_string(), scope.clone());
76    let (output, stream) = builder.new_output();
77    let mut output = OutputBuilder::<_, ColumnBuilder<_>>::from(output);
78
79    let operator_info = builder.operator_info();
80    builder.build(move |capabilities| {
81        // Metrics are per-process, so only one worker per process needs to
82        // scrape. Drop the capability for disabled workers so the frontier
83        // can advance without this operator holding it back.
84        let mut cap = enable.then_some(capabilities.into_element());
85        let activator = scope.activator_for(operator_info.address);
86
87        let mut prev_snapshot: BTreeMap<SnapshotKey, SnapshotValue> = BTreeMap::new();
88        let mut next_scrape = Instant::now();
89        let mut packer = PermutedRowPacker::new(ComputeLog::PrometheusMetrics);
90
91        move |_frontiers| {
92            let Some(cap) = &mut cap else { return };
93
94            let ts =
95                downgrade_to_interval_boundary(cap, &activator, now, start_offset, interval_ms);
96
97            // Only scrape when the scrape interval has elapsed.
98            // The operator wakes every logging interval to advance the
99            // capability, but scrapes less frequently if configured.
100            if Instant::now() < next_scrape {
101                return;
102            }
103
104            let prom_interval =
105                COMPUTE_PROMETHEUS_INTROSPECTION_SCRAPE_INTERVAL.get(&worker_config);
106            let effective_interval = prom_interval.max(interval);
107            next_scrape = Instant::now() + effective_interval;
108
109            // Gather current metrics and build new snapshot, or an empty
110            // snapshot when disabled (which retracts any existing data).
111            let new_snapshot = if !prom_interval.is_zero() {
112                let metric_families = metrics_registry.gather();
113                flatten_metrics(metric_families)
114            } else {
115                BTreeMap::new()
116            };
117
118            // Diff against previous snapshot and emit packed Row pairs.
119            let mut output = output.activate();
120            let mut session = output.session_with_builder(&cap);
121            emit_snapshot_diff(
122                &mut session,
123                &mut packer,
124                &prev_snapshot,
125                &new_snapshot,
126                ts,
127                |packer, key, value| {
128                    pack_row(
129                        packer, &key.0, value.1, &key.1, value.0, &value.2, process_id,
130                    )
131                },
132            );
133
134            prev_snapshot = new_snapshot;
135        }
136    });
137
138    // Arrange into a trace.
139    let exchange = ExchangeCore::<ColumnBuilder<_>, _>::new_core(
140        columnar_exchange::<mz_repr::Row, mz_repr::Row, Timestamp, mz_repr::Diff>,
141    );
142    let trace = stream
143        .mz_arrange_core::<
144            _,
145            Col2ValBatcher<_, _, _, _, batcher::Chunker<_>, RowRowBuilder<_, _>>,
146            RowRowSpine<_, _>,
147        >(exchange, "Arrange PrometheusMetrics")
148        .trace;
149    let token: Rc<dyn std::any::Any> = Rc::new(());
150    let collection = LogCollection { trace, token };
151    collections.insert(variant, collection);
152
153    Return { collections }
154}
155
156/// Flatten metric families into a snapshot map.
157fn flatten_metrics(
158    families: Vec<prometheus::proto::MetricFamily>,
159) -> BTreeMap<SnapshotKey, SnapshotValue> {
160    let mut snapshot = BTreeMap::new();
161
162    for family in families {
163        let base_name = family.name();
164        let help = family.help();
165        let metric_type = family.get_field_type();
166        let type_str = match metric_type {
167            MetricType::COUNTER => "counter",
168            MetricType::GAUGE => "gauge",
169            MetricType::HISTOGRAM => "histogram",
170            MetricType::SUMMARY => "summary",
171            MetricType::UNTYPED => "untyped",
172        };
173
174        for metric in family.get_metric() {
175            let base_labels: Vec<(String, String)> = metric
176                .get_label()
177                .iter()
178                .map(|l| (l.name().to_string(), l.value().to_string()))
179                .collect();
180
181            match metric_type {
182                MetricType::COUNTER => {
183                    let value = metric.get_counter().value();
184                    insert_row(
185                        &mut snapshot,
186                        base_name.to_string(),
187                        base_labels,
188                        value,
189                        type_str,
190                        help,
191                    );
192                }
193                MetricType::GAUGE => {
194                    let value = metric.get_gauge().value();
195                    insert_row(
196                        &mut snapshot,
197                        base_name.to_string(),
198                        base_labels,
199                        value,
200                        type_str,
201                        help,
202                    );
203                }
204                MetricType::HISTOGRAM => {
205                    let histogram = metric.get_histogram();
206
207                    // One row per bucket with `le` label.
208                    for bucket in histogram.get_bucket() {
209                        let mut labels = base_labels.clone();
210                        labels.push(("le".to_string(), format_f64(bucket.upper_bound())));
211                        insert_row(
212                            &mut snapshot,
213                            format!("{base_name}_bucket"),
214                            labels,
215                            f64::cast_lossy(bucket.cumulative_count()),
216                            type_str,
217                            help,
218                        );
219                    }
220
221                    // _sum row
222                    insert_row(
223                        &mut snapshot,
224                        format!("{base_name}_sum"),
225                        base_labels.clone(),
226                        histogram.get_sample_sum(),
227                        type_str,
228                        help,
229                    );
230
231                    // _count row
232                    insert_row(
233                        &mut snapshot,
234                        format!("{base_name}_count"),
235                        base_labels,
236                        f64::cast_lossy(histogram.get_sample_count()),
237                        type_str,
238                        help,
239                    );
240                }
241                MetricType::SUMMARY => {
242                    let summary = metric.get_summary();
243
244                    // One row per quantile.
245                    for quantile in summary.get_quantile() {
246                        let mut labels = base_labels.clone();
247                        labels.push(("quantile".to_string(), format_f64(quantile.quantile())));
248                        insert_row(
249                            &mut snapshot,
250                            base_name.to_string(),
251                            labels,
252                            quantile.value(),
253                            type_str,
254                            help,
255                        );
256                    }
257
258                    // _sum row
259                    insert_row(
260                        &mut snapshot,
261                        format!("{base_name}_sum"),
262                        base_labels.clone(),
263                        summary.sample_sum(),
264                        type_str,
265                        help,
266                    );
267
268                    // _count row
269                    insert_row(
270                        &mut snapshot,
271                        format!("{base_name}_count"),
272                        base_labels,
273                        f64::cast_lossy(summary.sample_count()),
274                        type_str,
275                        help,
276                    );
277                }
278                MetricType::UNTYPED => {
279                    soft_panic_or_log!("unexpected untyped metric: {base_name}");
280                }
281            }
282        }
283    }
284
285    snapshot
286}
287
288/// Format an f64 for use as a label value, matching Prometheus conventions.
289fn format_f64(v: f64) -> String {
290    if v == f64::INFINITY {
291        "+Inf".to_string()
292    } else if v == f64::NEG_INFINITY {
293        "-Inf".to_string()
294    } else {
295        v.to_string()
296    }
297}
298
299/// Insert a single metric row into the snapshot.
300fn insert_row(
301    snapshot: &mut BTreeMap<SnapshotKey, SnapshotValue>,
302    name: String,
303    mut labels: Vec<(String, String)>,
304    value: f64,
305    metric_type: &'static str,
306    help: &str,
307) {
308    labels.sort();
309
310    snapshot.insert((name, labels), (value, metric_type, help.to_string()));
311}
312
313/// Pack a metric row into key/value row pairs.
314fn pack_row<'a>(
315    packer: &'a mut PermutedRowPacker,
316    metric_name: &str,
317    metric_type: &str,
318    labels: &[(String, String)],
319    value: f64,
320    help: &str,
321    process_id: usize,
322) -> (&'a mz_repr::RowRef, &'a mz_repr::RowRef) {
323    packer.pack_by_index(|row_packer, index| match index {
324        // process_id
325        0 => row_packer.push(Datum::UInt64(u64::cast_from(process_id))),
326        // metric_name
327        1 => row_packer.push(Datum::String(metric_name)),
328        // metric_type
329        2 => row_packer.push(Datum::String(metric_type)),
330        // labels (Map)
331        3 => {
332            row_packer.push_dict(labels.iter().map(|(k, v)| (k.as_str(), Datum::String(v))));
333        }
334        // value
335        4 => row_packer.push(Datum::Float64(value.into())),
336        // help
337        5 => row_packer.push(Datum::String(help)),
338        _ => unreachable!("unexpected column index {index}"),
339    })
340}