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_timely_util::columnar::batcher;
24use mz_timely_util::columnar::builder::ColumnBuilder;
25use mz_timely_util::columnar::{Col2ValBatcher, columnar_exchange};
26use prometheus::proto::MetricType;
27use timely::dataflow::Scope;
28use timely::dataflow::channels::pact::ExchangeCore;
29use timely::dataflow::operators::generic::OutputBuilder;
30use timely::dataflow::operators::generic::builder_rc::OperatorBuilder;
31
32use crate::extensions::arrange::MzArrangeCore;
33use crate::logging::{
34    ComputeLog, LogCollection, LogVariant, PermutedRowPacker, downgrade_to_interval_boundary,
35    emit_snapshot_diff,
36};
37use crate::typedefs::RowRowSpine;
38use mz_row_spine::RowRowBuilder;
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            batcher::Chunker<_>,
146            Col2ValBatcher<_, _, _, _>,
147            RowRowBuilder<_, _>,
148            RowRowSpine<_, _>,
149        >(exchange, "Arrange PrometheusMetrics")
150        .trace;
151    let token: Rc<dyn std::any::Any> = Rc::new(());
152    let collection = LogCollection { trace, token };
153    collections.insert(variant, collection);
154
155    Return { collections }
156}
157
158/// Flatten metric families into a snapshot map.
159fn flatten_metrics(
160    families: Vec<prometheus::proto::MetricFamily>,
161) -> BTreeMap<SnapshotKey, SnapshotValue> {
162    let mut snapshot = BTreeMap::new();
163
164    for family in families {
165        let base_name = family.name();
166        let help = family.help();
167        let metric_type = family.get_field_type();
168        let type_str = match metric_type {
169            MetricType::COUNTER => "counter",
170            MetricType::GAUGE => "gauge",
171            MetricType::HISTOGRAM => "histogram",
172            MetricType::SUMMARY => "summary",
173            MetricType::UNTYPED => "untyped",
174        };
175
176        for metric in family.get_metric() {
177            let base_labels: Vec<(String, String)> = metric
178                .get_label()
179                .iter()
180                .map(|l| (l.name().to_string(), l.value().to_string()))
181                .collect();
182
183            match metric_type {
184                MetricType::COUNTER => {
185                    let value = metric.get_counter().value();
186                    insert_row(
187                        &mut snapshot,
188                        base_name.to_string(),
189                        base_labels,
190                        value,
191                        type_str,
192                        help,
193                    );
194                }
195                MetricType::GAUGE => {
196                    let value = metric.get_gauge().value();
197                    insert_row(
198                        &mut snapshot,
199                        base_name.to_string(),
200                        base_labels,
201                        value,
202                        type_str,
203                        help,
204                    );
205                }
206                MetricType::HISTOGRAM => {
207                    let histogram = metric.get_histogram();
208
209                    // One row per bucket with `le` label.
210                    for bucket in histogram.get_bucket() {
211                        let mut labels = base_labels.clone();
212                        labels.push(("le".to_string(), format_f64(bucket.upper_bound())));
213                        insert_row(
214                            &mut snapshot,
215                            format!("{base_name}_bucket"),
216                            labels,
217                            f64::cast_lossy(bucket.cumulative_count()),
218                            type_str,
219                            help,
220                        );
221                    }
222
223                    // _sum row
224                    insert_row(
225                        &mut snapshot,
226                        format!("{base_name}_sum"),
227                        base_labels.clone(),
228                        histogram.get_sample_sum(),
229                        type_str,
230                        help,
231                    );
232
233                    // _count row
234                    insert_row(
235                        &mut snapshot,
236                        format!("{base_name}_count"),
237                        base_labels,
238                        f64::cast_lossy(histogram.get_sample_count()),
239                        type_str,
240                        help,
241                    );
242                }
243                MetricType::SUMMARY => {
244                    let summary = metric.get_summary();
245
246                    // One row per quantile.
247                    for quantile in summary.get_quantile() {
248                        let mut labels = base_labels.clone();
249                        labels.push(("quantile".to_string(), format_f64(quantile.quantile())));
250                        insert_row(
251                            &mut snapshot,
252                            base_name.to_string(),
253                            labels,
254                            quantile.value(),
255                            type_str,
256                            help,
257                        );
258                    }
259
260                    // _sum row
261                    insert_row(
262                        &mut snapshot,
263                        format!("{base_name}_sum"),
264                        base_labels.clone(),
265                        summary.sample_sum(),
266                        type_str,
267                        help,
268                    );
269
270                    // _count row
271                    insert_row(
272                        &mut snapshot,
273                        format!("{base_name}_count"),
274                        base_labels,
275                        f64::cast_lossy(summary.sample_count()),
276                        type_str,
277                        help,
278                    );
279                }
280                MetricType::UNTYPED => {
281                    soft_panic_or_log!("unexpected untyped metric: {base_name}");
282                }
283            }
284        }
285    }
286
287    snapshot
288}
289
290/// Format an f64 for use as a label value, matching Prometheus conventions.
291fn format_f64(v: f64) -> String {
292    if v == f64::INFINITY {
293        "+Inf".to_string()
294    } else if v == f64::NEG_INFINITY {
295        "-Inf".to_string()
296    } else {
297        v.to_string()
298    }
299}
300
301/// Insert a single metric row into the snapshot.
302fn insert_row(
303    snapshot: &mut BTreeMap<SnapshotKey, SnapshotValue>,
304    name: String,
305    mut labels: Vec<(String, String)>,
306    value: f64,
307    metric_type: &'static str,
308    help: &str,
309) {
310    labels.sort();
311
312    snapshot.insert((name, labels), (value, metric_type, help.to_string()));
313}
314
315/// Pack a metric row into key/value row pairs.
316fn pack_row<'a>(
317    packer: &'a mut PermutedRowPacker,
318    metric_name: &str,
319    metric_type: &str,
320    labels: &[(String, String)],
321    value: f64,
322    help: &str,
323    process_id: usize,
324) -> (&'a mz_repr::RowRef, &'a mz_repr::RowRef) {
325    packer.pack_by_index(|row_packer, index| match index {
326        // process_id
327        0 => row_packer.push(Datum::UInt64(u64::cast_from(process_id))),
328        // metric_name
329        1 => row_packer.push(Datum::String(metric_name)),
330        // metric_type
331        2 => row_packer.push(Datum::String(metric_type)),
332        // labels (Map)
333        3 => {
334            row_packer.push_dict(labels.iter().map(|(k, v)| (k.as_str(), Datum::String(v))));
335        }
336        // value
337        4 => row_packer.push(Datum::Float64(value.into())),
338        // help
339        5 => row_packer.push(Datum::String(help)),
340        _ => unreachable!("unexpected column index {index}"),
341    })
342}