Skip to main content

risingwave_connector/source/monitor/
metrics.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::sync::{Arc, LazyLock};
16
17use prometheus::{
18    IntCounterVec, Registry, exponential_buckets, histogram_opts,
19    register_int_counter_vec_with_registry,
20};
21use risingwave_common::config::MetricLevel;
22use risingwave_common::metrics::{
23    GaugeAggregation, IntGaugeVecRelabelExt, LabelGuardedHistogramVec, LabelGuardedIntCounterVec,
24    LabelGuardedIntGaugeVec, MetricVecRelabelExt, RelabeledAggregatedIntGaugeVec,
25    RelabeledGuardedIntCounterVec,
26};
27use risingwave_common::monitor::GLOBAL_METRICS_REGISTRY;
28use risingwave_common::{
29    register_guarded_histogram_vec_with_registry, register_guarded_int_counter_vec_with_registry,
30    register_guarded_int_gauge_vec_with_registry,
31};
32
33use crate::connector_metrics_level;
34use crate::source::kafka::stats::RdKafkaStats;
35
36/// Low-cardinality connector ack failure categories.
37///
38/// Keep this list bounded. Do not add raw connector error messages, topics,
39/// partitions, or split identifiers as metric label values.
40#[derive(Debug, Clone, Copy)]
41pub enum ConnectorAckFailureType {
42    Error,
43    Timeout,
44    EmptyMessageId,
45    ChannelMissing,
46    ChannelSendError,
47    DecodeError,
48    BrokerError,
49}
50
51impl ConnectorAckFailureType {
52    pub fn as_str(self) -> &'static str {
53        match self {
54            Self::Error => "error",
55            Self::Timeout => "timeout",
56            Self::EmptyMessageId => "empty_message_id",
57            Self::ChannelMissing => "channel_missing",
58            Self::ChannelSendError => "channel_send_error",
59            Self::DecodeError => "decode_error",
60            Self::BrokerError => "broker_error",
61        }
62    }
63}
64
65#[derive(Debug, Clone)]
66pub struct EnumeratorMetrics {
67    pub high_watermark: LabelGuardedIntGaugeVec,
68    /// Kafka consumer group delete attempts that failed during source enumerator cleanup.
69    ///
70    /// The `consumer_group` label is fragment-derived and emitted only on cleanup failures.
71    pub kafka_consumer_group_delete_failure_count: IntCounterVec,
72    /// PostgreSQL CDC confirmed flush LSN monitoring
73    pub pg_cdc_confirmed_flush_lsn: LabelGuardedIntGaugeVec,
74    /// PostgreSQL CDC upstream max LSN monitoring
75    pub pg_cdc_upstream_max_lsn: LabelGuardedIntGaugeVec,
76    /// MySQL CDC binlog file sequence number (min)
77    pub mysql_cdc_binlog_file_seq_min: LabelGuardedIntGaugeVec,
78    /// MySQL CDC binlog file sequence number (max)
79    pub mysql_cdc_binlog_file_seq_max: LabelGuardedIntGaugeVec,
80    /// `MariaDB` CDC binlog file sequence number (min)
81    pub mariadb_cdc_binlog_file_seq_min: LabelGuardedIntGaugeVec,
82    /// `MariaDB` CDC binlog file sequence number (max)
83    pub mariadb_cdc_binlog_file_seq_max: LabelGuardedIntGaugeVec,
84    /// SQL Server CDC upstream minimum LSN
85    pub sqlserver_cdc_upstream_min_lsn: LabelGuardedIntGaugeVec,
86    /// SQL Server CDC upstream maximum LSN
87    pub sqlserver_cdc_upstream_max_lsn: LabelGuardedIntGaugeVec,
88}
89
90pub static GLOBAL_ENUMERATOR_METRICS: LazyLock<EnumeratorMetrics> =
91    LazyLock::new(|| EnumeratorMetrics::new(&GLOBAL_METRICS_REGISTRY));
92
93impl EnumeratorMetrics {
94    fn new(registry: &Registry) -> Self {
95        let high_watermark = register_guarded_int_gauge_vec_with_registry!(
96            "source_kafka_high_watermark",
97            "High watermark for a exec per partition",
98            &["source_id", "partition"],
99            registry,
100        )
101        .unwrap();
102
103        let kafka_consumer_group_delete_failure_count = register_int_counter_vec_with_registry!(
104            "source_kafka_consumer_group_delete_failure_count",
105            "Total number of Kafka consumer group delete attempts that failed during source enumerator cleanup",
106            &["source_id", "consumer_group"],
107            registry,
108        )
109        .unwrap();
110
111        let pg_cdc_confirmed_flush_lsn = register_guarded_int_gauge_vec_with_registry!(
112            "pg_cdc_confirmed_flush_lsn",
113            "PostgreSQL CDC confirmed flush LSN",
114            &["source_id", "slot_name"],
115            registry,
116        )
117        .unwrap();
118
119        let pg_cdc_upstream_max_lsn = register_guarded_int_gauge_vec_with_registry!(
120            "pg_cdc_upstream_max_lsn",
121            "PostgreSQL CDC upstream max LSN (pg_current_wal_lsn)",
122            &["source_id", "slot_name"],
123            registry,
124        )
125        .unwrap();
126
127        let mysql_cdc_binlog_file_seq_min = register_guarded_int_gauge_vec_with_registry!(
128            "mysql_cdc_binlog_file_seq_min",
129            "MySQL CDC upstream binlog file sequence number (minimum/oldest)",
130            &["source_id", "hostname", "port"],
131            registry,
132        )
133        .unwrap();
134
135        let mysql_cdc_binlog_file_seq_max = register_guarded_int_gauge_vec_with_registry!(
136            "mysql_cdc_binlog_file_seq_max",
137            "MySQL CDC upstream binlog file sequence number (maximum/newest)",
138            &["source_id", "hostname", "port"],
139            registry,
140        )
141        .unwrap();
142
143        let mariadb_cdc_binlog_file_seq_min = register_guarded_int_gauge_vec_with_registry!(
144            "mariadb_cdc_binlog_file_seq_min",
145            "MariaDB CDC upstream binlog file sequence number (minimum/oldest)",
146            &["source_id", "hostname", "port"],
147            registry,
148        )
149        .unwrap();
150
151        let mariadb_cdc_binlog_file_seq_max = register_guarded_int_gauge_vec_with_registry!(
152            "mariadb_cdc_binlog_file_seq_max",
153            "MariaDB CDC upstream binlog file sequence number (maximum/newest)",
154            &["source_id", "hostname", "port"],
155            registry,
156        )
157        .unwrap();
158
159        let sqlserver_cdc_upstream_min_lsn = register_guarded_int_gauge_vec_with_registry!(
160            "sqlserver_cdc_upstream_min_lsn",
161            "SQL Server CDC upstream minimum LSN",
162            &["source_id"],
163            registry,
164        )
165        .unwrap();
166
167        let sqlserver_cdc_upstream_max_lsn = register_guarded_int_gauge_vec_with_registry!(
168            "sqlserver_cdc_upstream_max_lsn",
169            "SQL Server CDC upstream maximum LSN",
170            &["source_id"],
171            registry,
172        )
173        .unwrap();
174
175        EnumeratorMetrics {
176            high_watermark,
177            kafka_consumer_group_delete_failure_count,
178            pg_cdc_confirmed_flush_lsn,
179            pg_cdc_upstream_max_lsn,
180            mysql_cdc_binlog_file_seq_min,
181            mysql_cdc_binlog_file_seq_max,
182            mariadb_cdc_binlog_file_seq_min,
183            mariadb_cdc_binlog_file_seq_max,
184            sqlserver_cdc_upstream_min_lsn,
185            sqlserver_cdc_upstream_max_lsn,
186        }
187    }
188
189    pub fn unused() -> Self {
190        Default::default()
191    }
192}
193
194impl Default for EnumeratorMetrics {
195    fn default() -> Self {
196        GLOBAL_ENUMERATOR_METRICS.clone()
197    }
198}
199
200#[derive(Debug, Clone)]
201pub struct SourceMetrics {
202    pub partition_input_count: RelabeledGuardedIntCounterVec,
203
204    // **Note**: for normal messages, the metric is the message's payload size.
205    // For messages from load generator, the metric is the size of stream chunk.
206    pub partition_input_bytes: RelabeledGuardedIntCounterVec,
207    /// Report latest message id
208    pub latest_message_id: RelabeledAggregatedIntGaugeVec,
209    pub partition_eof_count: LabelGuardedIntCounterVec,
210    pub partition_eof_offset: LabelGuardedIntGaugeVec,
211    pub rdkafka_native_metric: Arc<RdKafkaStats>,
212
213    pub direct_cdc_event_lag_latency: LabelGuardedHistogramVec,
214
215    pub parquet_source_skip_row_count: RelabeledGuardedIntCounterVec,
216    pub file_source_input_row_count: RelabeledGuardedIntCounterVec,
217    pub file_source_dirty_split_count: RelabeledAggregatedIntGaugeVec,
218    pub file_source_failed_split_count: RelabeledGuardedIntCounterVec,
219
220    // kinesis source
221    pub kinesis_throughput_exceeded_count: LabelGuardedIntCounterVec,
222    pub kinesis_timeout_count: LabelGuardedIntCounterVec,
223    pub kinesis_rebuild_shard_iter_count: LabelGuardedIntCounterVec,
224    pub kinesis_early_terminate_shard_count: LabelGuardedIntCounterVec,
225    pub kinesis_lag_latency_ms: LabelGuardedHistogramVec,
226
227    /// Total connector ack failures after checkpoint commit by bounded failure category.
228    connector_ack_failure_count: IntCounterVec,
229    /// Total successful connector acks after checkpoint commit.
230    connector_ack_success_count: IntCounterVec,
231}
232
233pub static GLOBAL_SOURCE_METRICS: LazyLock<SourceMetrics> =
234    LazyLock::new(|| SourceMetrics::new(&GLOBAL_METRICS_REGISTRY, connector_metrics_level()));
235
236impl SourceMetrics {
237    pub fn inc_connector_ack_failure_count(
238        &self,
239        source_name: &str,
240        connector_type: &'static str,
241        failure_type: ConnectorAckFailureType,
242    ) {
243        self.connector_ack_failure_count
244            .with_label_values(&[source_name, connector_type, failure_type.as_str()])
245            .inc();
246    }
247
248    pub fn inc_connector_ack_success_count(&self, source_name: &str, connector_type: &'static str) {
249        self.connector_ack_success_count
250            .with_label_values(&[source_name, connector_type])
251            .inc();
252    }
253
254    fn new(registry: &Registry, metric_level: MetricLevel) -> Self {
255        let partition_input_count = register_guarded_int_counter_vec_with_registry!(
256            "source_partition_input_count",
257            "Total number of rows that have been input from specific partition",
258            &[
259                "actor_id",
260                "source_id",
261                "partition",
262                "source_name",
263                "fragment_id"
264            ],
265            registry
266        )
267        .unwrap()
268        .relabel_debug_1(metric_level);
269        let partition_input_bytes = register_guarded_int_counter_vec_with_registry!(
270            "source_partition_input_bytes",
271            "Total bytes that have been input from specific partition",
272            &[
273                "actor_id",
274                "source_id",
275                "partition",
276                "source_name",
277                "fragment_id"
278            ],
279            registry
280        )
281        .unwrap()
282        .relabel_debug_1(metric_level);
283        // Preserve the frontend's max-by-source/partition query semantics when live and
284        // backfill readers share a worker and their actor labels are relabeled.
285        let latest_message_id = register_guarded_int_gauge_vec_with_registry!(
286            "source_latest_message_id",
287            "Latest message id for a exec per partition",
288            &["actor_id", "source_id", "partition"],
289            registry,
290        )
291        .unwrap()
292        .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Max);
293        let partition_eof_count = register_guarded_int_counter_vec_with_registry!(
294            "source_partition_eof_count",
295            "Total number of EOF events received from specific partition",
296            &["source_id", "partition", "source_name", "fragment_id"],
297            registry
298        )
299        .unwrap();
300        let partition_eof_offset = register_guarded_int_gauge_vec_with_registry!(
301            "source_partition_eof_offset",
302            "Latest resolved EOF offset for specific partition",
303            &["source_id", "partition", "source_name", "fragment_id"],
304            registry
305        )
306        .unwrap();
307
308        let opts = histogram_opts!(
309            "source_cdc_event_lag_duration_milliseconds",
310            "source_cdc_lag_latency",
311            exponential_buckets(1.0, 2.0, 21).unwrap(), // max 1048s
312        );
313
314        let parquet_source_skip_row_count = register_guarded_int_counter_vec_with_registry!(
315            "parquet_source_skip_row_count",
316            "Total number of rows that have been set to null in parquet source",
317            &["actor_id", "source_id", "source_name", "fragment_id"],
318            registry
319        )
320        .unwrap()
321        .relabel_debug_1(metric_level);
322
323        let direct_cdc_event_lag_latency =
324            register_guarded_histogram_vec_with_registry!(opts, &["table_name"], registry).unwrap();
325
326        let rdkafka_native_metric = Arc::new(RdKafkaStats::new(registry.clone()));
327
328        let file_source_input_row_count = register_guarded_int_counter_vec_with_registry!(
329            "file_source_input_row_count",
330            "Total number of rows that have been read in file source",
331            &["actor_id", "source_id", "source_name", "fragment_id"],
332            registry
333        )
334        .unwrap()
335        .relabel_debug_1(metric_level);
336        let file_source_dirty_split_count = register_guarded_int_gauge_vec_with_registry!(
337            "file_source_dirty_split_count",
338            "Current number of dirty file splits in file source",
339            &["actor_id", "source_id", "source_name", "fragment_id"],
340            registry
341        )
342        .unwrap()
343        .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Sum);
344        let file_source_failed_split_count = register_guarded_int_counter_vec_with_registry!(
345            "file_source_failed_split_count",
346            "Total number of file splits marked dirty in file source",
347            &["actor_id", "source_id", "source_name", "fragment_id"],
348            registry
349        )
350        .unwrap()
351        .relabel_debug_1(metric_level);
352
353        let kinesis_throughput_exceeded_count = register_guarded_int_counter_vec_with_registry!(
354            "kinesis_throughput_exceeded_count",
355            "Total number of times throughput exceeded in kinesis source",
356            &["source_id", "source_name", "fragment_id", "shard_id"],
357            registry
358        )
359        .unwrap();
360
361        let kinesis_timeout_count = register_guarded_int_counter_vec_with_registry!(
362            "kinesis_timeout_count",
363            "Total number of times timeout in kinesis source",
364            &["source_id", "source_name", "fragment_id", "shard_id"],
365            registry
366        )
367        .unwrap();
368
369        let kinesis_rebuild_shard_iter_count = register_guarded_int_counter_vec_with_registry!(
370            "kinesis_rebuild_shard_iter_count",
371            "Total number of times rebuild shard iter in kinesis source",
372            &["source_id", "source_name", "fragment_id", "shard_id"],
373            registry
374        )
375        .unwrap();
376
377        let kinesis_early_terminate_shard_count = register_guarded_int_counter_vec_with_registry!(
378            "kinesis_early_terminate_shard_count",
379            "Total number of times early terminate shard in kinesis source",
380            &["source_id", "source_name", "fragment_id", "shard_id"],
381            registry
382        )
383        .unwrap();
384
385        let kinesis_lag_latency_ms = register_guarded_histogram_vec_with_registry!(
386            "kinesis_lag_latency_ms",
387            "Lag latency in kinesis source",
388            &["source_id", "source_name", "fragment_id", "shard_id"],
389            registry
390        )
391        .unwrap();
392
393        let connector_ack_failure_count = register_int_counter_vec_with_registry!(
394            "source_connector_ack_failure_count",
395            "Total number of connector ack failures after checkpoint commit by bounded failure category",
396            &["source_name", "connector_type", "error_type"],
397            registry
398        )
399        .unwrap();
400        let connector_ack_success_count = register_int_counter_vec_with_registry!(
401            "source_connector_ack_success_count",
402            "Total number of successful connector acks after checkpoint commit",
403            &["source_name", "connector_type"],
404            registry
405        )
406        .unwrap();
407
408        SourceMetrics {
409            partition_input_count,
410            partition_input_bytes,
411            latest_message_id,
412            partition_eof_count,
413            partition_eof_offset,
414            rdkafka_native_metric,
415            direct_cdc_event_lag_latency,
416            parquet_source_skip_row_count,
417            file_source_input_row_count,
418            file_source_dirty_split_count,
419            file_source_failed_split_count,
420
421            kinesis_throughput_exceeded_count,
422            kinesis_timeout_count,
423            kinesis_rebuild_shard_iter_count,
424            kinesis_early_terminate_shard_count,
425            kinesis_lag_latency_ms,
426
427            connector_ack_failure_count,
428            connector_ack_success_count,
429        }
430    }
431}
432
433impl Default for SourceMetrics {
434    fn default() -> Self {
435        GLOBAL_SOURCE_METRICS.clone()
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    fn assert_aggregation(
444        gauge_vec: &RelabeledAggregatedIntGaugeVec,
445        first_labels: &[&str],
446        second_labels: &[&str],
447        expected: i64,
448    ) {
449        let first = gauge_vec.with_guarded_label_values(first_labels);
450        let second = gauge_vec.with_guarded_label_values(second_labels);
451        first.set(5);
452        second.set(7);
453        assert_eq!(first.get(), expected);
454        assert_eq!(second.get(), expected);
455    }
456
457    #[test]
458    fn actor_relabeled_gauges_use_the_assigned_reducers() {
459        for level in [MetricLevel::Critical, MetricLevel::Info] {
460            let registry = Registry::new();
461            let metrics = SourceMetrics::new(&registry, level);
462
463            assert_aggregation(
464                &metrics.latest_message_id,
465                &["1", "source", "partition"],
466                &["2", "source", "partition"],
467                7,
468            );
469            assert_aggregation(
470                &metrics.file_source_dirty_split_count,
471                &["1", "source", "name", "fragment"],
472                &["2", "source", "name", "fragment"],
473                12,
474            );
475        }
476    }
477
478    #[test]
479    fn latest_message_id_max_is_independent_of_reader_placement() {
480        for level in [MetricLevel::Critical, MetricLevel::Info, MetricLevel::Debug] {
481            for same_worker in [true, false] {
482                let live_registry = Registry::new();
483                let backfill_registry = Registry::new();
484                let live_metrics = SourceMetrics::new(&live_registry, level);
485                let backfill_metrics = if same_worker {
486                    live_metrics.clone()
487                } else {
488                    SourceMetrics::new(&backfill_registry, level)
489                };
490                let live = live_metrics.latest_message_id.with_guarded_label_values(&[
491                    "1",
492                    "source",
493                    "partition",
494                ]);
495                let backfill = backfill_metrics
496                    .latest_message_id
497                    .with_guarded_label_values(&["2", "source", "partition"]);
498                live.set(1_000);
499                backfill.set(100);
500
501                // Match the frontend's max(source_latest_message_id) across workers.
502                let mut offsets: Vec<_> = [&live_registry, &backfill_registry]
503                    .into_iter()
504                    .flat_map(Registry::gather)
505                    .filter(|family| family.name() == "source_latest_message_id")
506                    .flat_map(|family| {
507                        family
508                            .get_metric()
509                            .iter()
510                            .map(|metric| metric.get_gauge().value() as i64)
511                            .collect::<Vec<_>>()
512                    })
513                    .collect();
514                assert_eq!(offsets.iter().copied().max(), Some(1_000));
515                offsets.sort_unstable();
516                if same_worker && level != MetricLevel::Debug {
517                    assert_eq!(offsets, [1_000]);
518                } else {
519                    assert_eq!(offsets, [100, 1_000]);
520                }
521            }
522        }
523    }
524}