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    /// SQL Server CDC upstream minimum LSN
81    pub sqlserver_cdc_upstream_min_lsn: LabelGuardedIntGaugeVec,
82    /// SQL Server CDC upstream maximum LSN
83    pub sqlserver_cdc_upstream_max_lsn: LabelGuardedIntGaugeVec,
84}
85
86pub static GLOBAL_ENUMERATOR_METRICS: LazyLock<EnumeratorMetrics> =
87    LazyLock::new(|| EnumeratorMetrics::new(&GLOBAL_METRICS_REGISTRY));
88
89impl EnumeratorMetrics {
90    fn new(registry: &Registry) -> Self {
91        let high_watermark = register_guarded_int_gauge_vec_with_registry!(
92            "source_kafka_high_watermark",
93            "High watermark for a exec per partition",
94            &["source_id", "partition"],
95            registry,
96        )
97        .unwrap();
98
99        let kafka_consumer_group_delete_failure_count = register_int_counter_vec_with_registry!(
100            "source_kafka_consumer_group_delete_failure_count",
101            "Total number of Kafka consumer group delete attempts that failed during source enumerator cleanup",
102            &["source_id", "consumer_group"],
103            registry,
104        )
105        .unwrap();
106
107        let pg_cdc_confirmed_flush_lsn = register_guarded_int_gauge_vec_with_registry!(
108            "pg_cdc_confirmed_flush_lsn",
109            "PostgreSQL CDC confirmed flush LSN",
110            &["source_id", "slot_name"],
111            registry,
112        )
113        .unwrap();
114
115        let pg_cdc_upstream_max_lsn = register_guarded_int_gauge_vec_with_registry!(
116            "pg_cdc_upstream_max_lsn",
117            "PostgreSQL CDC upstream max LSN (pg_current_wal_lsn)",
118            &["source_id", "slot_name"],
119            registry,
120        )
121        .unwrap();
122
123        let mysql_cdc_binlog_file_seq_min = register_guarded_int_gauge_vec_with_registry!(
124            "mysql_cdc_binlog_file_seq_min",
125            "MySQL CDC upstream binlog file sequence number (minimum/oldest)",
126            &["source_id", "hostname", "port"],
127            registry,
128        )
129        .unwrap();
130
131        let mysql_cdc_binlog_file_seq_max = register_guarded_int_gauge_vec_with_registry!(
132            "mysql_cdc_binlog_file_seq_max",
133            "MySQL CDC upstream binlog file sequence number (maximum/newest)",
134            &["source_id", "hostname", "port"],
135            registry,
136        )
137        .unwrap();
138
139        let sqlserver_cdc_upstream_min_lsn = register_guarded_int_gauge_vec_with_registry!(
140            "sqlserver_cdc_upstream_min_lsn",
141            "SQL Server CDC upstream minimum LSN",
142            &["source_id"],
143            registry,
144        )
145        .unwrap();
146
147        let sqlserver_cdc_upstream_max_lsn = register_guarded_int_gauge_vec_with_registry!(
148            "sqlserver_cdc_upstream_max_lsn",
149            "SQL Server CDC upstream maximum LSN",
150            &["source_id"],
151            registry,
152        )
153        .unwrap();
154
155        EnumeratorMetrics {
156            high_watermark,
157            kafka_consumer_group_delete_failure_count,
158            pg_cdc_confirmed_flush_lsn,
159            pg_cdc_upstream_max_lsn,
160            mysql_cdc_binlog_file_seq_min,
161            mysql_cdc_binlog_file_seq_max,
162            sqlserver_cdc_upstream_min_lsn,
163            sqlserver_cdc_upstream_max_lsn,
164        }
165    }
166
167    pub fn unused() -> Self {
168        Default::default()
169    }
170}
171
172impl Default for EnumeratorMetrics {
173    fn default() -> Self {
174        GLOBAL_ENUMERATOR_METRICS.clone()
175    }
176}
177
178#[derive(Debug, Clone)]
179pub struct SourceMetrics {
180    pub partition_input_count: RelabeledGuardedIntCounterVec,
181
182    // **Note**: for normal messages, the metric is the message's payload size.
183    // For messages from load generator, the metric is the size of stream chunk.
184    pub partition_input_bytes: RelabeledGuardedIntCounterVec,
185    /// Report latest message id
186    pub latest_message_id: RelabeledAggregatedIntGaugeVec,
187    pub partition_eof_count: LabelGuardedIntCounterVec,
188    pub partition_eof_offset: LabelGuardedIntGaugeVec,
189    pub rdkafka_native_metric: Arc<RdKafkaStats>,
190
191    pub direct_cdc_event_lag_latency: LabelGuardedHistogramVec,
192
193    pub parquet_source_skip_row_count: RelabeledGuardedIntCounterVec,
194    pub file_source_input_row_count: RelabeledGuardedIntCounterVec,
195    pub file_source_dirty_split_count: RelabeledAggregatedIntGaugeVec,
196    pub file_source_failed_split_count: RelabeledGuardedIntCounterVec,
197
198    // kinesis source
199    pub kinesis_throughput_exceeded_count: LabelGuardedIntCounterVec,
200    pub kinesis_timeout_count: LabelGuardedIntCounterVec,
201    pub kinesis_rebuild_shard_iter_count: LabelGuardedIntCounterVec,
202    pub kinesis_early_terminate_shard_count: LabelGuardedIntCounterVec,
203    pub kinesis_lag_latency_ms: LabelGuardedHistogramVec,
204
205    /// Total connector ack failures after checkpoint commit by bounded failure category.
206    connector_ack_failure_count: IntCounterVec,
207    /// Total successful connector acks after checkpoint commit.
208    connector_ack_success_count: IntCounterVec,
209}
210
211pub static GLOBAL_SOURCE_METRICS: LazyLock<SourceMetrics> =
212    LazyLock::new(|| SourceMetrics::new(&GLOBAL_METRICS_REGISTRY, connector_metrics_level()));
213
214impl SourceMetrics {
215    pub fn inc_connector_ack_failure_count(
216        &self,
217        source_name: &str,
218        connector_type: &'static str,
219        failure_type: ConnectorAckFailureType,
220    ) {
221        self.connector_ack_failure_count
222            .with_label_values(&[source_name, connector_type, failure_type.as_str()])
223            .inc();
224    }
225
226    pub fn inc_connector_ack_success_count(&self, source_name: &str, connector_type: &'static str) {
227        self.connector_ack_success_count
228            .with_label_values(&[source_name, connector_type])
229            .inc();
230    }
231
232    fn new(registry: &Registry, metric_level: MetricLevel) -> Self {
233        let partition_input_count = register_guarded_int_counter_vec_with_registry!(
234            "source_partition_input_count",
235            "Total number of rows that have been input from specific partition",
236            &[
237                "actor_id",
238                "source_id",
239                "partition",
240                "source_name",
241                "fragment_id"
242            ],
243            registry
244        )
245        .unwrap()
246        .relabel_debug_1(metric_level);
247        let partition_input_bytes = register_guarded_int_counter_vec_with_registry!(
248            "source_partition_input_bytes",
249            "Total bytes that have been input from specific partition",
250            &[
251                "actor_id",
252                "source_id",
253                "partition",
254                "source_name",
255                "fragment_id"
256            ],
257            registry
258        )
259        .unwrap()
260        .relabel_debug_1(metric_level);
261        // Preserve the frontend's max-by-source/partition query semantics when live and
262        // backfill readers share a worker and their actor labels are relabeled.
263        let latest_message_id = register_guarded_int_gauge_vec_with_registry!(
264            "source_latest_message_id",
265            "Latest message id for a exec per partition",
266            &["actor_id", "source_id", "partition"],
267            registry,
268        )
269        .unwrap()
270        .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Max);
271        let partition_eof_count = register_guarded_int_counter_vec_with_registry!(
272            "source_partition_eof_count",
273            "Total number of EOF events received from specific partition",
274            &["source_id", "partition", "source_name", "fragment_id"],
275            registry
276        )
277        .unwrap();
278        let partition_eof_offset = register_guarded_int_gauge_vec_with_registry!(
279            "source_partition_eof_offset",
280            "Latest resolved EOF offset for specific partition",
281            &["source_id", "partition", "source_name", "fragment_id"],
282            registry
283        )
284        .unwrap();
285
286        let opts = histogram_opts!(
287            "source_cdc_event_lag_duration_milliseconds",
288            "source_cdc_lag_latency",
289            exponential_buckets(1.0, 2.0, 21).unwrap(), // max 1048s
290        );
291
292        let parquet_source_skip_row_count = register_guarded_int_counter_vec_with_registry!(
293            "parquet_source_skip_row_count",
294            "Total number of rows that have been set to null in parquet source",
295            &["actor_id", "source_id", "source_name", "fragment_id"],
296            registry
297        )
298        .unwrap()
299        .relabel_debug_1(metric_level);
300
301        let direct_cdc_event_lag_latency =
302            register_guarded_histogram_vec_with_registry!(opts, &["table_name"], registry).unwrap();
303
304        let rdkafka_native_metric = Arc::new(RdKafkaStats::new(registry.clone()));
305
306        let file_source_input_row_count = register_guarded_int_counter_vec_with_registry!(
307            "file_source_input_row_count",
308            "Total number of rows that have been read in file source",
309            &["actor_id", "source_id", "source_name", "fragment_id"],
310            registry
311        )
312        .unwrap()
313        .relabel_debug_1(metric_level);
314        let file_source_dirty_split_count = register_guarded_int_gauge_vec_with_registry!(
315            "file_source_dirty_split_count",
316            "Current number of dirty file splits in file source",
317            &["actor_id", "source_id", "source_name", "fragment_id"],
318            registry
319        )
320        .unwrap()
321        .relabel_debug_1_with_aggregation(metric_level, GaugeAggregation::Sum);
322        let file_source_failed_split_count = register_guarded_int_counter_vec_with_registry!(
323            "file_source_failed_split_count",
324            "Total number of file splits marked dirty in file source",
325            &["actor_id", "source_id", "source_name", "fragment_id"],
326            registry
327        )
328        .unwrap()
329        .relabel_debug_1(metric_level);
330
331        let kinesis_throughput_exceeded_count = register_guarded_int_counter_vec_with_registry!(
332            "kinesis_throughput_exceeded_count",
333            "Total number of times throughput exceeded in kinesis source",
334            &["source_id", "source_name", "fragment_id", "shard_id"],
335            registry
336        )
337        .unwrap();
338
339        let kinesis_timeout_count = register_guarded_int_counter_vec_with_registry!(
340            "kinesis_timeout_count",
341            "Total number of times timeout in kinesis source",
342            &["source_id", "source_name", "fragment_id", "shard_id"],
343            registry
344        )
345        .unwrap();
346
347        let kinesis_rebuild_shard_iter_count = register_guarded_int_counter_vec_with_registry!(
348            "kinesis_rebuild_shard_iter_count",
349            "Total number of times rebuild shard iter in kinesis source",
350            &["source_id", "source_name", "fragment_id", "shard_id"],
351            registry
352        )
353        .unwrap();
354
355        let kinesis_early_terminate_shard_count = register_guarded_int_counter_vec_with_registry!(
356            "kinesis_early_terminate_shard_count",
357            "Total number of times early terminate shard in kinesis source",
358            &["source_id", "source_name", "fragment_id", "shard_id"],
359            registry
360        )
361        .unwrap();
362
363        let kinesis_lag_latency_ms = register_guarded_histogram_vec_with_registry!(
364            "kinesis_lag_latency_ms",
365            "Lag latency in kinesis source",
366            &["source_id", "source_name", "fragment_id", "shard_id"],
367            registry
368        )
369        .unwrap();
370
371        let connector_ack_failure_count = register_int_counter_vec_with_registry!(
372            "source_connector_ack_failure_count",
373            "Total number of connector ack failures after checkpoint commit by bounded failure category",
374            &["source_name", "connector_type", "error_type"],
375            registry
376        )
377        .unwrap();
378        let connector_ack_success_count = register_int_counter_vec_with_registry!(
379            "source_connector_ack_success_count",
380            "Total number of successful connector acks after checkpoint commit",
381            &["source_name", "connector_type"],
382            registry
383        )
384        .unwrap();
385
386        SourceMetrics {
387            partition_input_count,
388            partition_input_bytes,
389            latest_message_id,
390            partition_eof_count,
391            partition_eof_offset,
392            rdkafka_native_metric,
393            direct_cdc_event_lag_latency,
394            parquet_source_skip_row_count,
395            file_source_input_row_count,
396            file_source_dirty_split_count,
397            file_source_failed_split_count,
398
399            kinesis_throughput_exceeded_count,
400            kinesis_timeout_count,
401            kinesis_rebuild_shard_iter_count,
402            kinesis_early_terminate_shard_count,
403            kinesis_lag_latency_ms,
404
405            connector_ack_failure_count,
406            connector_ack_success_count,
407        }
408    }
409}
410
411impl Default for SourceMetrics {
412    fn default() -> Self {
413        GLOBAL_SOURCE_METRICS.clone()
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    fn assert_aggregation(
422        gauge_vec: &RelabeledAggregatedIntGaugeVec,
423        first_labels: &[&str],
424        second_labels: &[&str],
425        expected: i64,
426    ) {
427        let first = gauge_vec.with_guarded_label_values(first_labels);
428        let second = gauge_vec.with_guarded_label_values(second_labels);
429        first.set(5);
430        second.set(7);
431        assert_eq!(first.get(), expected);
432        assert_eq!(second.get(), expected);
433    }
434
435    #[test]
436    fn actor_relabeled_gauges_use_the_assigned_reducers() {
437        for level in [MetricLevel::Critical, MetricLevel::Info] {
438            let registry = Registry::new();
439            let metrics = SourceMetrics::new(&registry, level);
440
441            assert_aggregation(
442                &metrics.latest_message_id,
443                &["1", "source", "partition"],
444                &["2", "source", "partition"],
445                7,
446            );
447            assert_aggregation(
448                &metrics.file_source_dirty_split_count,
449                &["1", "source", "name", "fragment"],
450                &["2", "source", "name", "fragment"],
451                12,
452            );
453        }
454    }
455
456    #[test]
457    fn latest_message_id_max_is_independent_of_reader_placement() {
458        for level in [MetricLevel::Critical, MetricLevel::Info, MetricLevel::Debug] {
459            for same_worker in [true, false] {
460                let live_registry = Registry::new();
461                let backfill_registry = Registry::new();
462                let live_metrics = SourceMetrics::new(&live_registry, level);
463                let backfill_metrics = if same_worker {
464                    live_metrics.clone()
465                } else {
466                    SourceMetrics::new(&backfill_registry, level)
467                };
468                let live = live_metrics.latest_message_id.with_guarded_label_values(&[
469                    "1",
470                    "source",
471                    "partition",
472                ]);
473                let backfill = backfill_metrics
474                    .latest_message_id
475                    .with_guarded_label_values(&["2", "source", "partition"]);
476                live.set(1_000);
477                backfill.set(100);
478
479                // Match the frontend's max(source_latest_message_id) across workers.
480                let mut offsets: Vec<_> = [&live_registry, &backfill_registry]
481                    .into_iter()
482                    .flat_map(Registry::gather)
483                    .filter(|family| family.name() == "source_latest_message_id")
484                    .flat_map(|family| {
485                        family
486                            .get_metric()
487                            .iter()
488                            .map(|metric| metric.get_gauge().value() as i64)
489                            .collect::<Vec<_>>()
490                    })
491                    .collect();
492                assert_eq!(offsets.iter().copied().max(), Some(1_000));
493                offsets.sort_unstable();
494                if same_worker && level != MetricLevel::Debug {
495                    assert_eq!(offsets, [1_000]);
496                } else {
497                    assert_eq!(offsets, [100, 1_000]);
498                }
499            }
500        }
501    }
502}