1use axum::http;
5use std::{
6 borrow::Cow,
7 collections::HashSet,
8 sync::Arc,
9 time::{Duration, Instant},
10};
11
12use prometheus::{
13 Histogram, HistogramTimer, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec,
14 Registry, register_histogram_vec_with_registry, register_histogram_with_registry,
15 register_int_counter_vec_with_registry, register_int_counter_with_registry,
16 register_int_gauge_vec_with_registry, register_int_gauge_with_registry,
17};
18use prost::Message;
19use sui_http::middleware::callback::{MakeCallbackHandler, ResponseHandler};
20
21#[derive(Clone)]
22pub struct RpcMetrics {
23 inflight_requests: IntGaugeVec,
24 num_requests: IntCounterVec,
25 request_latency: HistogramVec,
26 request_handler_latency: HistogramVec,
27 first_chunk_latency: HistogramVec,
28 simulate_client_protocol_versions: IntCounterVec,
29}
30
31const LATENCY_SEC_BUCKETS: &[f64] = &[
32 0.001, 0.005, 0.01, 0.05, 0.1, 0.25, 0.5, 1., 2.5, 5., 10., 20., 30., 60., 90.,
33];
34
35impl RpcMetrics {
36 pub fn new(registry: &Registry) -> Self {
37 Self {
38 inflight_requests: register_int_gauge_vec_with_registry!(
39 "rpc_inflight_requests",
40 "Total in-flight RPC requests per route",
41 &["path"],
42 registry,
43 )
44 .unwrap(),
45 num_requests: register_int_counter_vec_with_registry!(
46 "rpc_requests",
47 "Total RPC requests per route and their http status",
48 &["path", "status"],
49 registry,
50 )
51 .unwrap(),
52 request_latency: register_histogram_vec_with_registry!(
53 "rpc_request_latency",
54 "Latency of RPC requests per route, measured from receipt of the request \
55 until the response body finished streaming back to the client",
56 &["path"],
57 LATENCY_SEC_BUCKETS.to_vec(),
58 registry,
59 )
60 .unwrap(),
61 request_handler_latency: register_histogram_vec_with_registry!(
62 "rpc_request_handler_latency",
63 "Latency of RPC requests per route, measured from receipt of the request \
64 until the request handler produced a response, excluding the time spent \
65 streaming the response body back to the client",
66 &["path"],
67 LATENCY_SEC_BUCKETS.to_vec(),
68 registry,
69 )
70 .unwrap(),
71 first_chunk_latency: register_histogram_vec_with_registry!(
72 "rpc_first_chunk_latency",
73 "Latency of RPC requests per route, measured from receipt of the request \
74 until the first response body data chunk is produced. For streaming responses \
75 this is when the first chunk is handed to the transport, which for gRPC \
76 typically carries the first encoded message; response headers are excluded. \
77 Responses whose body never yields a data chunk are not observed.",
78 &["path"],
79 LATENCY_SEC_BUCKETS.to_vec(),
80 registry,
81 )
82 .unwrap(),
83 simulate_client_protocol_versions: register_int_counter_vec_with_registry!(
84 "rpc_simulate_transaction_client_protocol_version",
85 "SimulateTransaction requests by the client's reported x-sui-client-protocol-version; \
86 0 when the header is missing, malformed, or more than 20 above this binary's max",
87 &["client_protocol_version"],
88 registry,
89 )
90 .unwrap(),
91 }
92 }
93
94 pub(crate) fn observe_simulate_client_protocol_version(&self, version: u64) {
95 self.simulate_client_protocol_versions
96 .with_label_values(&[&version.to_string()])
97 .inc();
98 }
99}
100
101#[derive(Clone)]
102pub(crate) struct ListApiMetrics {
103 list_first_frame_seconds: HistogramVec,
104 list_response_page_bytes: HistogramVec,
105 list_watermark_frames_total: IntCounterVec,
106 list_stream_yield_wait_seconds: HistogramVec,
107 list_render_seconds: HistogramVec,
108 list_chunk_seconds: HistogramVec,
109 list_query_ends_total: IntCounterVec,
110 list_bitmap_buckets_evaluated: HistogramVec,
111}
112
113impl ListApiMetrics {
114 pub(crate) fn new(registry: &Registry) -> Self {
115 Self {
116 list_first_frame_seconds: register_histogram_vec_with_registry!(
117 "list_first_frame_seconds",
118 "Time in seconds from List handler entry to the first response frame of any kind — data, watermark-only, or terminal — the client's first actionable signal; resolution label derived only from the validated read mask.",
119 &["method", "resolution"],
120 prometheus::exponential_buckets(0.001, 2.0, 17).unwrap(),
121 registry,
122 )
123 .unwrap(),
124 list_response_page_bytes: register_histogram_vec_with_registry!(
125 "list_response_page_bytes",
126 "Protobuf encoded size in bytes of data-bearing List response frames, measured with encoded_len without serializing or copying; watermark-only and terminal-only frames are excluded and counted by list_watermark_frames_total.",
127 &["method", "resolution"],
128 prometheus::exponential_buckets(1024.0, 2.0, 17).unwrap(),
129 registry,
130 )
131 .unwrap(),
132 list_watermark_frames_total: register_int_counter_vec_with_registry!(
133 "list_watermark_frames_total",
134 "Total watermark-only and terminal-only response frames emitted by List handlers; data-bearing frames are excluded.",
135 &["method"],
136 registry,
137 )
138 .unwrap(),
139 list_stream_yield_wait_seconds: register_histogram_vec_with_registry!(
140 "list_stream_yield_wait_seconds",
141 "Time from yielding one List response frame (any kind) until the handler stream is polled again; downstream transport consumption and backpressure signal.",
142 &["method", "resolution"],
143 prometheus::exponential_buckets(0.00001, 2.0, 24).unwrap(),
144 registry,
145 )
146 .unwrap(),
147 list_render_seconds: register_histogram_vec_with_registry!(
148 "list_render_seconds",
149 "Time in seconds spent rendering one data item into a List response frame. Request setup, scan-only watermark rendering, standalone terminal rendering, and chunk-level batch reads are excluded. The resolution label is derived only from the validated read mask.",
150 &["method", "resolution"],
151 prometheus::exponential_buckets(0.00001, 2.0, 24).unwrap(),
152 registry,
153 )
154 .unwrap(),
155 list_chunk_seconds: register_histogram_vec_with_registry!(
156 "list_chunk_seconds",
157 "Time in seconds for one blocking List chunk phase. queue spans immediately before spawn_blocking through entry into its closure; work spans execution of the blocking chunk and includes chunks that return an error.",
158 &["method", "phase"],
159 prometheus::exponential_buckets(0.0001, 2.0, 20).unwrap(),
160 registry,
161 )
162 .unwrap(),
163 list_query_ends_total: register_int_counter_vec_with_registry!(
164 "list_query_ends_total",
165 "Successful List streams by effective protocol QueryEndReason. Errors, cancellation, and dropped streams are excluded.",
166 &["method", "reason"],
167 registry,
168 )
169 .unwrap(),
170 list_bitmap_buckets_evaluated: register_histogram_vec_with_registry!(
171 "list_bitmap_buckets_evaluated",
172 "Total bitmap buckets evaluated across all blocking chunks of one successfully completed filtered List request. Unfiltered requests are not observed.",
173 &["method"],
174 prometheus::exponential_buckets(1.0, 2.0, 12).unwrap(),
175 registry,
176 )
177 .unwrap(),
178 }
179 }
180
181 pub(crate) fn stream_metrics(
182 &self,
183 method: &'static str,
184 resolution: &'static str,
185 ) -> ListStreamMetrics {
186 ListStreamMetrics {
187 method,
188 first_frame: self
189 .list_first_frame_seconds
190 .with_label_values(&[method, resolution]),
191 page_bytes: self
192 .list_response_page_bytes
193 .with_label_values(&[method, resolution]),
194 watermark_frames: self
195 .list_watermark_frames_total
196 .with_label_values(&[method]),
197 yield_wait: self
198 .list_stream_yield_wait_seconds
199 .with_label_values(&[method, resolution]),
200 render: self
201 .list_render_seconds
202 .with_label_values(&[method, resolution]),
203 chunk_queue: self
204 .list_chunk_seconds
205 .with_label_values(&[method, "queue"]),
206 chunk_work: self.list_chunk_seconds.with_label_values(&[method, "work"]),
207 query_ends: self.list_query_ends_total.clone(),
208 bitmap_buckets_evaluated: self
209 .list_bitmap_buckets_evaluated
210 .with_label_values(&[method]),
211 }
212 }
213}
214
215pub type GrpcMethodAllowlist = Arc<HashSet<String>>;
225
226pub fn grpc_method_paths_from_file_descriptor_sets(
233 encoded_sets: &[&[u8]],
234) -> Result<HashSet<String>, prost::DecodeError> {
235 let mut paths = HashSet::new();
236 for bytes in encoded_sets {
237 let fds = prost_types::FileDescriptorSet::decode(*bytes)?;
238 for file in fds.file {
239 let package = file.package.unwrap_or_default();
240 for service in file.service {
241 let Some(service_name) = service.name else {
242 continue;
243 };
244 let qualified_service = if package.is_empty() {
245 service_name
246 } else {
247 format!("{}.{}", package, service_name)
248 };
249 for method in service.method {
250 let Some(method_name) = method.name else {
251 continue;
252 };
253 paths.insert(format!("/{}/{}", qualified_service, method_name));
254 }
255 }
256 }
257 }
258 Ok(paths)
259}
260
261#[derive(Clone)]
262pub struct RpcMetricsMakeCallbackHandler {
263 metrics: Arc<RpcMetrics>,
264 grpc_method_allowlist: GrpcMethodAllowlist,
265}
266
267impl RpcMetricsMakeCallbackHandler {
268 pub fn new(metrics: Arc<RpcMetrics>) -> Self {
273 Self::with_grpc_method_allowlist(metrics, Arc::new(HashSet::new()))
274 }
275
276 pub fn with_grpc_method_allowlist(
279 metrics: Arc<RpcMetrics>,
280 allowlist: GrpcMethodAllowlist,
281 ) -> Self {
282 Self {
283 metrics,
284 grpc_method_allowlist: allowlist,
285 }
286 }
287}
288
289impl MakeCallbackHandler for RpcMetricsMakeCallbackHandler {
290 type RequestHandler = ();
291 type ResponseHandler = RpcMetricsCallbackHandler;
292
293 fn make_handler(
294 &self,
295 request: &http::request::Parts,
296 ) -> (Self::RequestHandler, Self::ResponseHandler) {
297 let start = Instant::now();
298 let metrics = self.metrics.clone();
299
300 let matched_path = request
301 .extensions
302 .get::<axum::extract::MatchedPath>()
303 .map(|m| m.as_str());
304 let is_grpc = request
305 .headers
306 .get(&http::header::CONTENT_TYPE)
307 .is_some_and(is_grpc_content_type);
308
309 let path = compute_metric_label(
310 is_grpc,
311 request.uri.path(),
312 matched_path,
313 &self.grpc_method_allowlist,
314 );
315
316 metrics
317 .inflight_requests
318 .with_label_values(&[path.as_ref()])
319 .inc();
320
321 (
322 (),
323 RpcMetricsCallbackHandler {
324 metrics,
325 path,
326 start,
327 counted_response: false,
328 counted_first_chunk: false,
329 },
330 )
331 }
332}
333
334fn compute_metric_label(
342 is_grpc: bool,
343 uri_path: &str,
344 matched_path: Option<&str>,
345 grpc_method_allowlist: &HashSet<String>,
346) -> Cow<'static, str> {
347 match (is_grpc, matched_path) {
348 (true, _) if grpc_method_allowlist.contains(uri_path) => Cow::Owned(uri_path.to_owned()),
349 (true, Some(matched)) => Cow::Owned(matched.to_owned()),
350 (false, Some(matched)) => Cow::Owned(matched.to_owned()),
351 (_, None) => Cow::Borrowed("unknown"),
352 }
353}
354
355fn is_grpc_content_type(content_type: &http::HeaderValue) -> bool {
356 content_type
357 .as_bytes()
358 .starts_with(tonic::metadata::GRPC_CONTENT_TYPE.as_bytes())
359}
360
361pub struct RpcMetricsCallbackHandler {
362 metrics: Arc<RpcMetrics>,
363 path: Cow<'static, str>,
364 start: Instant,
365 counted_response: bool,
368 counted_first_chunk: bool,
369}
370
371impl ResponseHandler for RpcMetricsCallbackHandler {
372 fn on_response(&mut self, response: &http::response::Parts) {
373 const GRPC_STATUS: http::HeaderName = http::HeaderName::from_static("grpc-status");
374
375 self.metrics
379 .request_handler_latency
380 .with_label_values(&[self.path.as_ref()])
381 .observe(self.start.elapsed().as_secs_f64());
382
383 let status = if response
384 .headers
385 .get(&http::header::CONTENT_TYPE)
386 .is_some_and(is_grpc_content_type)
387 {
388 let code = response
389 .headers
390 .get(&GRPC_STATUS)
391 .map(http::HeaderValue::as_bytes)
392 .map(tonic::Code::from_bytes)
393 .unwrap_or(tonic::Code::Ok);
394
395 code_as_str(code)
396 } else {
397 response.status.as_str()
398 };
399
400 self.metrics
401 .num_requests
402 .with_label_values(&[self.path.as_ref(), status])
403 .inc();
404
405 self.counted_response = true;
406 }
407
408 fn on_body_chunk<B>(&mut self, _chunk: &B)
409 where
410 B: bytes::Buf,
411 {
412 if !self.counted_first_chunk {
413 self.metrics
414 .first_chunk_latency
415 .with_label_values(&[self.path.as_ref()])
416 .observe(self.start.elapsed().as_secs_f64());
417 self.counted_first_chunk = true;
418 }
419 }
420
421 fn on_service_error<E>(&mut self, _error: &E)
422 where
423 E: std::fmt::Display + 'static,
424 {
425 }
430}
431
432impl Drop for RpcMetricsCallbackHandler {
433 fn drop(&mut self) {
434 self.metrics
435 .inflight_requests
436 .with_label_values(&[self.path.as_ref()])
437 .dec();
438
439 let latency = self.start.elapsed().as_secs_f64();
440 self.metrics
441 .request_latency
442 .with_label_values(&[self.path.as_ref()])
443 .observe(latency);
444
445 if !self.counted_response {
446 self.metrics
447 .num_requests
448 .with_label_values(&[self.path.as_ref(), "canceled"])
449 .inc();
450 }
451 }
452}
453
454fn code_as_str(code: tonic::Code) -> &'static str {
455 match code {
456 tonic::Code::Ok => "ok",
457 tonic::Code::Cancelled => "canceled",
458 tonic::Code::Unknown => "unknown",
459 tonic::Code::InvalidArgument => "invalid-argument",
460 tonic::Code::DeadlineExceeded => "deadline-exceeded",
461 tonic::Code::NotFound => "not-found",
462 tonic::Code::AlreadyExists => "already-exists",
463 tonic::Code::PermissionDenied => "permission-denied",
464 tonic::Code::ResourceExhausted => "resource-exhausted",
465 tonic::Code::FailedPrecondition => "failed-precondition",
466 tonic::Code::Aborted => "aborted",
467 tonic::Code::OutOfRange => "out-of-range",
468 tonic::Code::Unimplemented => "unimplemented",
469 tonic::Code::Internal => "internal",
470 tonic::Code::Unavailable => "unavailable",
471 tonic::Code::DataLoss => "data-loss",
472 tonic::Code::Unauthenticated => "unauthenticated",
473 }
474}
475
476#[derive(Clone)]
477pub(crate) struct ListStreamMetrics {
478 method: &'static str,
479 first_frame: Histogram,
480 page_bytes: Histogram,
481 watermark_frames: IntCounter,
482 yield_wait: Histogram,
483 render: Histogram,
484 chunk_queue: Histogram,
485 chunk_work: Histogram,
486 query_ends: IntCounterVec,
487 bitmap_buckets_evaluated: Histogram,
488}
489
490impl ListStreamMetrics {
491 pub(crate) fn observe_render(&self, elapsed: Duration) {
492 self.render.observe(elapsed.as_secs_f64());
493 }
494
495 pub(crate) fn start_queue_timer(&self) -> HistogramTimer {
496 self.chunk_queue.start_timer()
497 }
498
499 pub(crate) fn start_work_timer(&self) -> HistogramTimer {
500 self.chunk_work.start_timer()
501 }
502}
503
504pub(crate) struct ListRequestMetrics {
505 inner: Option<ListRequestMetricsInner>,
506}
507
508struct ListRequestMetricsInner {
509 handles: ListStreamMetrics,
510 started: Instant,
511 first_frame_observed: bool,
512 success_finished: bool,
513}
514
515impl ListRequestMetrics {
516 pub(crate) fn new(handles: Option<ListStreamMetrics>, started: Instant) -> Self {
517 Self {
518 inner: handles.map(|handles| ListRequestMetricsInner {
519 handles,
520 started,
521 first_frame_observed: false,
522 success_finished: false,
523 }),
524 }
525 }
526
527 pub(crate) fn chunk_metrics(&self) -> Option<ListStreamMetrics> {
528 self.inner.as_ref().map(|inner| inner.handles.clone())
529 }
530
531 pub(crate) fn observe_frame<M: prost::Message>(&mut self, response: &M, is_data: bool) {
532 let Some(inner) = &mut self.inner else {
533 return;
534 };
535 if is_data {
536 inner
537 .handles
538 .page_bytes
539 .observe(response.encoded_len() as f64);
540 } else {
541 inner.handles.watermark_frames.inc();
542 }
543 if !inner.first_frame_observed {
544 inner
545 .handles
546 .first_frame
547 .observe(inner.started.elapsed().as_secs_f64());
548 inner.first_frame_observed = true;
549 }
550 }
551
552 pub(crate) fn yield_clock(&self) -> Option<Instant> {
553 self.inner.as_ref().map(|_| Instant::now())
554 }
555
556 pub(crate) fn observe_yield_wait(&self, yield_started: Option<Instant>) {
559 if let (Some(inner), Some(yield_started)) = (&self.inner, yield_started) {
560 inner
561 .handles
562 .yield_wait
563 .observe(yield_started.elapsed().as_secs_f64());
564 }
565 }
566
567 pub(crate) fn finish_success(
568 &mut self,
569 reason: sui_rpc::proto::sui::rpc::v2::QueryEndReason,
570 bitmap_buckets_evaluated: Option<usize>,
571 ) {
572 let Some(inner) = &mut self.inner else {
573 return;
574 };
575 if inner.success_finished {
576 return;
577 }
578 inner.success_finished = true;
579 let reason = match reason {
580 sui_rpc::proto::sui::rpc::v2::QueryEndReason::ItemLimit => "item_limit",
581 sui_rpc::proto::sui::rpc::v2::QueryEndReason::ScanLimit => "scan_limit",
582 sui_rpc::proto::sui::rpc::v2::QueryEndReason::LedgerTip => "ledger_tip",
583 sui_rpc::proto::sui::rpc::v2::QueryEndReason::CheckpointBound => "checkpoint_bound",
584 sui_rpc::proto::sui::rpc::v2::QueryEndReason::CursorBound => "cursor_bound",
585 sui_rpc::proto::sui::rpc::v2::QueryEndReason::Unknown => {
587 unreachable!("validated successful List stream has an unspecified end reason")
588 }
589 _ => unreachable!("validated successful List stream has an unsupported end reason"),
590 };
591 inner
592 .handles
593 .query_ends
594 .with_label_values(&[inner.handles.method, reason])
595 .inc();
596 if let Some(bitmap_buckets_evaluated) = bitmap_buckets_evaluated {
597 inner
598 .handles
599 .bitmap_buckets_evaluated
600 .observe(bitmap_buckets_evaluated as f64);
601 }
602 }
603}
604
605#[derive(Clone, Copy)]
606pub(crate) enum SubscriptionFrameKind {
607 Payload,
608 Watermark,
609}
610
611#[derive(Clone)]
612pub(crate) struct SubscriptionStreamMetrics {
613 pub(crate) payload_messages: IntCounter,
614 watermark_messages: IntCounter,
615 payload_bytes: Histogram,
616 yield_wait: Histogram,
617}
618
619impl SubscriptionStreamMetrics {
620 pub(crate) fn observe_frame<M: prost::Message>(
621 &self,
622 response: &M,
623 kind: SubscriptionFrameKind,
624 ) {
625 match kind {
626 SubscriptionFrameKind::Payload => {
627 self.payload_messages.inc();
628 self.payload_bytes.observe(response.encoded_len() as f64);
629 }
630 SubscriptionFrameKind::Watermark => {
631 self.watermark_messages.inc();
632 }
633 }
634 }
635
636 pub(crate) fn observe_yield_wait(&self, elapsed: Duration) {
637 self.yield_wait.observe(elapsed.as_secs_f64());
638 }
639}
640
641#[derive(Clone)]
642pub(crate) struct SubscriptionMetrics {
643 pub(crate) inflight_subscribers: IntGaugeVec,
644 pub(crate) last_recieved_checkpoint: IntGauge,
645 pub payload_messages: IntCounterVec,
646 pub(crate) watermark_messages: IntCounterVec,
647 pub(crate) payload_bytes: HistogramVec,
648 pub(crate) stream_yield_wait_seconds: HistogramVec,
649 pub(crate) terminations_total: IntCounterVec,
650 pub(crate) index_wait_seconds: Histogram,
651 pub(crate) index_wait_timeouts_total: IntCounter,
652}
653
654impl SubscriptionMetrics {
655 pub fn new(registry: &Registry) -> Self {
656 Self {
657 inflight_subscribers: register_int_gauge_vec_with_registry!(
658 "subscription_inflight_subscribers",
659 "Current admitted gRPC subscriptions by type and whether a filter is present.",
660 &["type", "filtered"],
661 registry,
662 )
663 .unwrap(),
664 last_recieved_checkpoint: register_int_gauge_with_registry!(
665 "subscription_last_recieved_checkpoint",
666 "Last recieved checkpoint by the subscription service",
667 registry,
668 )
669 .unwrap(),
670 payload_messages: register_int_counter_vec_with_registry!(
671 "subscription_payload_messages",
672 "Total number of payload messages emitted by gRPC subscriptions, by type",
673 &["type"],
674 registry,
675 )
676 .unwrap(),
677 watermark_messages: register_int_counter_vec_with_registry!(
678 "subscription_watermark_messages_total",
679 "Total progress-only response frames emitted by gRPC subscriptions, including initial recovery-boundary frames, by type.",
680 &["type"],
681 registry,
682 )
683 .unwrap(),
684 payload_bytes: register_histogram_vec_with_registry!(
685 "subscription_payload_bytes",
686 "Protobuf encoded size in bytes of payload response frames yielded by a gRPC subscription, measured with encoded_len without serializing or copying the response. Progress-only frames are excluded and counted by subscription_watermark_messages_total.",
687 &["type"],
688 prometheus::exponential_buckets(1024.0, 2.0, 17).unwrap(),
689 registry,
690 )
691 .unwrap(),
692 stream_yield_wait_seconds: register_histogram_vec_with_registry!(
693 "subscription_stream_yield_wait_seconds",
694 "Time in seconds from yielding any gRPC subscription response until the stream is polled again; this is a downstream transport consumption and backpressure signal.",
695 &["type"],
696 prometheus::exponential_buckets(0.00001, 2.0, 24).unwrap(),
697 registry,
698 )
699 .unwrap(),
700 terminations_total: register_int_counter_vec_with_registry!(
701 "subscription_terminations_total",
702 "Admitted gRPC subscriptions terminated by bounded lifecycle reason. Admission rejections are excluded.",
703 &["type", "reason"],
704 registry,
705 )
706 .unwrap(),
707 index_wait_seconds: register_histogram_with_registry!(
708 "subscription_index_wait_seconds",
709 "Time in seconds spent waiting for the subscription index to catch up before dispatching a checkpoint. Checkpoints that do not wait are excluded.",
710 LATENCY_SEC_BUCKETS.to_vec(),
711 registry,
712 )
713 .unwrap(),
714 index_wait_timeouts_total: register_int_counter_with_registry!(
715 "subscription_index_wait_timeouts_total",
716 "Total subscription index waits that reached the 10-second timeout and dispatched the checkpoint before the index caught up.",
717 registry,
718 )
719 .unwrap(),
720 }
721 }
722}
723impl SubscriptionMetrics {
724 pub(crate) fn stream_metrics(&self, type_label: &'static str) -> SubscriptionStreamMetrics {
725 SubscriptionStreamMetrics {
726 payload_messages: self.payload_messages.with_label_values(&[type_label]),
727 watermark_messages: self.watermark_messages.with_label_values(&[type_label]),
728 payload_bytes: self.payload_bytes.with_label_values(&[type_label]),
729 yield_wait: self
730 .stream_yield_wait_seconds
731 .with_label_values(&[type_label]),
732 }
733 }
734}
735
736#[cfg(test)]
737mod tests {
738 use super::*;
739 use std::collections::BTreeSet;
740
741 use prost_types::{
742 FileDescriptorProto, FileDescriptorSet, MethodDescriptorProto, ServiceDescriptorProto,
743 };
744 use sui_rpc::proto::sui::rpc::v2::{
745 ListTransactionsResponse, QueryEnd, SubscribeCheckpointsResponse, SubscribeEventsResponse,
746 SubscribeTransactionsResponse, Watermark,
747 };
748
749 fn encode(set: FileDescriptorSet) -> Vec<u8> {
750 let mut buf = Vec::with_capacity(set.encoded_len());
751 set.encode(&mut buf).unwrap();
752 buf
753 }
754
755 fn fds(package: &str, services: &[(&str, &[&str])]) -> Vec<u8> {
756 encode(FileDescriptorSet {
757 file: vec![FileDescriptorProto {
758 package: Some(package.to_owned()),
759 service: services
760 .iter()
761 .map(|(name, methods)| ServiceDescriptorProto {
762 name: Some((*name).to_owned()),
763 method: methods
764 .iter()
765 .map(|m| MethodDescriptorProto {
766 name: Some((*m).to_owned()),
767 ..Default::default()
768 })
769 .collect(),
770 ..Default::default()
771 })
772 .collect(),
773 ..Default::default()
774 }],
775 })
776 }
777
778 #[test]
779 fn parses_method_paths_from_file_descriptor_sets() {
780 let v2 = fds(
781 "sui.rpc.v2",
782 &[("LedgerService", &["GetCheckpoint", "GetTransaction"])],
783 );
784 let v2alpha = fds(
785 "sui.rpc.v2alpha",
786 &[("ProofService", &["GetCheckpointObjectProof"])],
787 );
788
789 let paths = grpc_method_paths_from_file_descriptor_sets(&[&v2, &v2alpha]).unwrap();
790
791 assert_eq!(paths.len(), 3);
792 assert!(paths.contains("/sui.rpc.v2.LedgerService/GetCheckpoint"));
793 assert!(paths.contains("/sui.rpc.v2.LedgerService/GetTransaction"));
794 assert!(paths.contains("/sui.rpc.v2alpha.ProofService/GetCheckpointObjectProof"));
795 }
796
797 #[test]
798 fn parser_handles_files_without_a_package() {
799 let bare = fds("", &[("BareService", &["Ping"])]);
800 let paths = grpc_method_paths_from_file_descriptor_sets(&[&bare]).unwrap();
801 assert!(paths.contains("/BareService/Ping"));
802 }
803
804 #[test]
805 fn known_grpc_method_uses_uri_path_label() {
806 let mut allowlist = HashSet::new();
807 allowlist.insert("/sui.rpc.v2.LedgerService/GetCheckpoint".to_owned());
808
809 let label = compute_metric_label(
810 true,
811 "/sui.rpc.v2.LedgerService/GetCheckpoint",
812 Some("/sui.rpc.v2.LedgerService/{*rest}"),
813 &allowlist,
814 );
815 assert_eq!(label, "/sui.rpc.v2.LedgerService/GetCheckpoint");
816 }
817
818 #[test]
819 fn known_grpc_method_without_matched_path_uses_uri_path_label() {
820 let mut allowlist = HashSet::new();
821 allowlist.insert("/sui.rpc.v2.LedgerService/ListTransactions".to_owned());
822
823 let label = compute_metric_label(
824 true,
825 "/sui.rpc.v2.LedgerService/ListTransactions",
826 None,
827 &allowlist,
828 );
829 assert_eq!(label, "/sui.rpc.v2.LedgerService/ListTransactions");
830 }
831
832 #[test]
833 fn unknown_grpc_method_falls_back_to_route_pattern() {
834 let allowlist = HashSet::new();
839 let label = compute_metric_label(
840 true,
841 "/sui.rpc.v2.LedgerService/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
842 Some("/sui.rpc.v2.LedgerService/{*rest}"),
843 &allowlist,
844 );
845 assert_eq!(label, "/sui.rpc.v2.LedgerService/{*rest}");
846 }
847
848 #[test]
849 fn non_grpc_request_uses_matched_path() {
850 let allowlist = HashSet::new();
851 let label = compute_metric_label(false, "/health", Some("/health"), &allowlist);
852 assert_eq!(label, "/health");
853 }
854
855 #[test]
856 fn request_without_matched_path_is_labelled_unknown() {
857 let allowlist = HashSet::new();
858 let label = compute_metric_label(true, "/no/match", None, &allowlist);
859 assert_eq!(label, "unknown");
860 }
861
862 #[test]
863 fn grpc_content_type_accepts_codec_suffixes() {
864 assert!(is_grpc_content_type(&http::HeaderValue::from_static(
865 "application/grpc"
866 )));
867 assert!(is_grpc_content_type(&http::HeaderValue::from_static(
868 "application/grpc+proto"
869 )));
870 assert!(!is_grpc_content_type(&http::HeaderValue::from_static(
871 "application/json"
872 )));
873 }
874
875 fn make_test_handler(metrics: &Arc<RpcMetrics>) -> RpcMetricsCallbackHandler {
878 let make = RpcMetricsMakeCallbackHandler::new(metrics.clone());
879 let (parts, _) = http::Request::new(()).into_parts();
880 let ((), handler) = make.make_handler(&parts);
881 handler
882 }
883
884 #[test]
888 fn handler_latency_observed_on_response_and_total_latency_on_drop() {
889 let metrics = Arc::new(RpcMetrics::new(&Registry::new()));
890 let mut handler = make_test_handler(&metrics);
891
892 let handler_latency = metrics
893 .request_handler_latency
894 .with_label_values(&["unknown"]);
895 let total_latency = metrics.request_latency.with_label_values(&["unknown"]);
896
897 assert_eq!(handler_latency.get_sample_count(), 0);
898
899 let (parts, _) = http::Response::new(()).into_parts();
900 handler.on_response(&parts);
901
902 assert_eq!(handler_latency.get_sample_count(), 1);
903 assert_eq!(total_latency.get_sample_count(), 0);
904
905 drop(handler);
906
907 assert_eq!(handler_latency.get_sample_count(), 1);
908 assert_eq!(total_latency.get_sample_count(), 1);
909 }
910
911 #[test]
912 fn first_chunk_latency_observed_once_on_first_body_chunk() {
913 let metrics = Arc::new(RpcMetrics::new(&Registry::new()));
914 let mut handler = make_test_handler(&metrics);
915 let first_chunk_latency = metrics.first_chunk_latency.with_label_values(&["unknown"]);
916
917 let (parts, _) = http::Response::new(()).into_parts();
918 handler.on_response(&parts);
919 handler.on_body_chunk(&bytes::Bytes::from_static(b"first"));
920 handler.on_body_chunk(&bytes::Bytes::from_static(b"second"));
921
922 assert_eq!(first_chunk_latency.get_sample_count(), 1);
923
924 drop(handler);
925
926 assert_eq!(first_chunk_latency.get_sample_count(), 1);
927 }
928
929 #[test]
932 fn handler_latency_not_observed_for_canceled_requests() {
933 let metrics = Arc::new(RpcMetrics::new(&Registry::new()));
934 let handler = make_test_handler(&metrics);
935
936 drop(handler);
937
938 assert_eq!(
939 metrics
940 .request_handler_latency
941 .with_label_values(&["unknown"])
942 .get_sample_count(),
943 0
944 );
945 assert_eq!(
946 metrics
947 .first_chunk_latency
948 .with_label_values(&["unknown"])
949 .get_sample_count(),
950 0
951 );
952 assert_eq!(
953 metrics
954 .request_latency
955 .with_label_values(&["unknown"])
956 .get_sample_count(),
957 1
958 );
959 assert_eq!(
960 metrics
961 .num_requests
962 .with_label_values(&["unknown", "canceled"])
963 .get(),
964 1
965 );
966 }
967 fn metric_label_sets(
968 family: &prometheus::proto::MetricFamily,
969 ) -> BTreeSet<Vec<(String, String)>> {
970 family
971 .get_metric()
972 .iter()
973 .map(|metric| {
974 let mut labels = metric
975 .get_label()
976 .iter()
977 .map(|label| (label.name().to_owned(), label.value().to_owned()))
978 .collect::<Vec<_>>();
979 labels.sort();
980 labels
981 })
982 .collect()
983 }
984
985 fn expected_label_sets(rows: Vec<Vec<(&str, &str)>>) -> BTreeSet<Vec<(String, String)>> {
986 rows.into_iter()
987 .map(|row| {
988 let mut labels = row
989 .into_iter()
990 .map(|(name, value)| (name.to_owned(), value.to_owned()))
991 .collect::<Vec<_>>();
992 labels.sort();
993 labels
994 })
995 .collect()
996 }
997
998 fn assert_metric_family(
999 families: &[prometheus::proto::MetricFamily],
1000 name: &str,
1001 expected_labels: BTreeSet<Vec<(String, String)>>,
1002 ) {
1003 let family = families
1004 .iter()
1005 .find(|family| family.name() == name)
1006 .unwrap_or_else(|| panic!("missing metric family {name}"));
1007 assert_eq!(metric_label_sets(family), expected_labels, "{name}");
1008 }
1009
1010 #[test]
1011 fn focused_metric_families_use_exact_bounded_labels() {
1012 let registry = Registry::new();
1013 let list_metrics = ListApiMetrics::new(®istry);
1014 let method_resolutions = [
1015 ("list_checkpoints", "summary"),
1016 ("list_checkpoints", "transactions"),
1017 ("list_checkpoints", "objects"),
1018 ("list_transactions", "digest"),
1019 ("list_transactions", "full"),
1020 ("list_transactions", "full_objects"),
1021 ("list_events", "no_json"),
1022 ("list_events", "json"),
1023 ];
1024 for (method, resolution) in method_resolutions {
1025 list_metrics.stream_metrics(method, resolution);
1026 }
1027 let methods = ["list_checkpoints", "list_transactions", "list_events"];
1028 let reasons = [
1029 "item_limit",
1030 "scan_limit",
1031 "ledger_tip",
1032 "checkpoint_bound",
1033 "cursor_bound",
1034 ];
1035 for method in methods {
1036 for reason in reasons {
1037 list_metrics
1038 .list_query_ends_total
1039 .with_label_values(&[method, reason]);
1040 }
1041 }
1042
1043 let subscription_metrics = SubscriptionMetrics::new(®istry);
1044 let types = ["checkpoint", "transaction", "event"];
1045 for type_label in types {
1046 subscription_metrics.stream_metrics(type_label);
1047 for filtered in ["true", "false"] {
1048 subscription_metrics
1049 .inflight_subscribers
1050 .with_label_values(&[type_label, filtered]);
1051 }
1052 for reason in [
1053 "client_closed",
1054 "slow_consumer",
1055 "source_lag",
1056 "service_shutdown",
1057 ] {
1058 subscription_metrics
1059 .terminations_total
1060 .with_label_values(&[type_label, reason]);
1061 }
1062 }
1063
1064 let families = registry.gather();
1065 let method_resolution_labels = expected_label_sets(
1066 method_resolutions
1067 .into_iter()
1068 .map(|(method, resolution)| vec![("method", method), ("resolution", resolution)])
1069 .collect(),
1070 );
1071 for name in [
1072 "list_first_frame_seconds",
1073 "list_response_page_bytes",
1074 "list_stream_yield_wait_seconds",
1075 "list_render_seconds",
1076 ] {
1077 assert_metric_family(&families, name, method_resolution_labels.clone());
1078 }
1079 assert_metric_family(
1080 &families,
1081 "list_watermark_frames_total",
1082 expected_label_sets(
1083 methods
1084 .into_iter()
1085 .map(|method| vec![("method", method)])
1086 .collect(),
1087 ),
1088 );
1089 assert_metric_family(
1090 &families,
1091 "list_chunk_seconds",
1092 expected_label_sets(
1093 methods
1094 .into_iter()
1095 .flat_map(|method| {
1096 ["queue", "work"]
1097 .into_iter()
1098 .map(move |phase| vec![("method", method), ("phase", phase)])
1099 })
1100 .collect(),
1101 ),
1102 );
1103 assert_metric_family(
1104 &families,
1105 "list_query_ends_total",
1106 expected_label_sets(
1107 methods
1108 .into_iter()
1109 .flat_map(|method| {
1110 reasons
1111 .into_iter()
1112 .map(move |reason| vec![("method", method), ("reason", reason)])
1113 })
1114 .collect(),
1115 ),
1116 );
1117 assert_metric_family(
1118 &families,
1119 "list_bitmap_buckets_evaluated",
1120 expected_label_sets(
1121 methods
1122 .into_iter()
1123 .map(|method| vec![("method", method)])
1124 .collect(),
1125 ),
1126 );
1127
1128 let type_labels = expected_label_sets(
1129 types
1130 .into_iter()
1131 .map(|type_label| vec![("type", type_label)])
1132 .collect(),
1133 );
1134 assert_metric_family(
1135 &families,
1136 "subscription_payload_messages",
1137 type_labels.clone(),
1138 );
1139 assert_metric_family(
1140 &families,
1141 "subscription_watermark_messages_total",
1142 type_labels.clone(),
1143 );
1144 assert_metric_family(
1145 &families,
1146 "subscription_stream_yield_wait_seconds",
1147 type_labels.clone(),
1148 );
1149 assert_metric_family(&families, "subscription_payload_bytes", type_labels);
1150 assert_metric_family(
1151 &families,
1152 "subscription_inflight_subscribers",
1153 expected_label_sets(
1154 types
1155 .into_iter()
1156 .flat_map(|type_label| {
1157 ["true", "false"]
1158 .into_iter()
1159 .map(move |filtered| vec![("type", type_label), ("filtered", filtered)])
1160 })
1161 .collect(),
1162 ),
1163 );
1164 assert_metric_family(
1165 &families,
1166 "subscription_terminations_total",
1167 expected_label_sets(
1168 types
1169 .into_iter()
1170 .flat_map(|type_label| {
1171 [
1172 "client_closed",
1173 "slow_consumer",
1174 "source_lag",
1175 "service_shutdown",
1176 ]
1177 .into_iter()
1178 .map(move |reason| vec![("type", type_label), ("reason", reason)])
1179 })
1180 .collect(),
1181 ),
1182 );
1183 assert_metric_family(
1184 &families,
1185 "subscription_index_wait_seconds",
1186 expected_label_sets(vec![vec![]]),
1187 );
1188 assert_metric_family(
1189 &families,
1190 "subscription_index_wait_timeouts_total",
1191 expected_label_sets(vec![vec![]]),
1192 );
1193 }
1194
1195 #[test]
1196 fn list_page_and_watermark_metrics_cover_all_frame_kinds() {
1197 let registry = Registry::new();
1198 let metrics = ListApiMetrics::new(®istry);
1199 let handles = metrics.stream_metrics("list_transactions", "full");
1200 let mut request_metrics = ListRequestMetrics::new(Some(handles.clone()), Instant::now());
1201
1202 let mut data = ListTransactionsResponse::default();
1203 data.transaction = Some(Default::default());
1204 let mut watermark_only = ListTransactionsResponse::default();
1205 watermark_only.watermark = Some(Watermark::default());
1206 let mut terminal = ListTransactionsResponse::default();
1207 terminal.end = Some(QueryEnd::default());
1208
1209 request_metrics.observe_frame(&watermark_only, false);
1210 assert_eq!(handles.first_frame.get_sample_count(), 1);
1211 let yield_started = request_metrics.yield_clock();
1212 request_metrics.observe_yield_wait(yield_started);
1213 request_metrics.observe_frame(&data, true);
1214 let yield_started = request_metrics.yield_clock();
1215 request_metrics.observe_yield_wait(yield_started);
1216 request_metrics.observe_frame(&terminal, false);
1217 let yield_started = request_metrics.yield_clock();
1218 request_metrics.observe_yield_wait(yield_started);
1219 handles.observe_render(Duration::from_millis(1));
1220
1221 assert_eq!(handles.page_bytes.get_sample_count(), 1);
1222 assert_eq!(
1223 handles.page_bytes.get_sample_sum(),
1224 data.encoded_len() as f64
1225 );
1226 assert_eq!(handles.watermark_frames.get(), 2);
1227 assert_eq!(handles.first_frame.get_sample_count(), 1);
1228 assert_eq!(handles.yield_wait.get_sample_count(), 3);
1229 assert_eq!(handles.render.get_sample_count(), 1);
1230
1231 let terminal_registry = Registry::new();
1232 let terminal_metrics = ListApiMetrics::new(&terminal_registry);
1233 let terminal_handles = terminal_metrics.stream_metrics("list_transactions", "digest");
1234 let mut terminal_request =
1235 ListRequestMetrics::new(Some(terminal_handles.clone()), Instant::now());
1236 terminal_request.observe_frame(&terminal, false);
1237
1238 assert_eq!(terminal_handles.page_bytes.get_sample_count(), 0);
1239 assert_eq!(terminal_handles.page_bytes.get_sample_sum(), 0.0);
1240 assert_eq!(terminal_handles.watermark_frames.get(), 1);
1241 assert_eq!(terminal_handles.first_frame.get_sample_count(), 1);
1242 }
1243
1244 fn assert_subscription_response_metrics<M: Message>(
1245 metrics: &SubscriptionMetrics,
1246 type_label: &'static str,
1247 payload: &M,
1248 watermark: &M,
1249 ) {
1250 let stream_metrics = metrics.stream_metrics(type_label);
1251 stream_metrics.observe_frame(payload, SubscriptionFrameKind::Payload);
1252 stream_metrics.observe_yield_wait(Duration::from_millis(1));
1253 stream_metrics.observe_frame(watermark, SubscriptionFrameKind::Watermark);
1254 stream_metrics.observe_yield_wait(Duration::from_millis(2));
1255
1256 assert_eq!(stream_metrics.payload_messages.get(), 1);
1257 assert_eq!(stream_metrics.watermark_messages.get(), 1);
1258 assert_eq!(stream_metrics.payload_bytes.get_sample_count(), 1);
1259 assert_eq!(
1260 stream_metrics.payload_bytes.get_sample_sum(),
1261 payload.encoded_len() as f64
1262 );
1263 assert_eq!(stream_metrics.yield_wait.get_sample_count(), 2);
1264 }
1265
1266 #[test]
1267 fn subscription_response_metrics_split_payload_and_watermark_frames() {
1268 let registry = Registry::new();
1269 let metrics = SubscriptionMetrics::new(®istry);
1270
1271 let mut checkpoint_payload = SubscribeCheckpointsResponse::default();
1272 checkpoint_payload.cursor = Some(7);
1273 checkpoint_payload.checkpoint = Some(Default::default());
1274 let mut checkpoint_watermark = SubscribeCheckpointsResponse::default();
1275 checkpoint_watermark.cursor = Some(8);
1276 assert_subscription_response_metrics(
1277 &metrics,
1278 "checkpoint",
1279 &checkpoint_payload,
1280 &checkpoint_watermark,
1281 );
1282
1283 let mut transaction_payload = SubscribeTransactionsResponse::default();
1284 transaction_payload.transaction = Some(Default::default());
1285 transaction_payload.watermark = Some(Watermark::default());
1286 let mut transaction_watermark = SubscribeTransactionsResponse::default();
1287 transaction_watermark.watermark = Some(Watermark::default());
1288 assert_subscription_response_metrics(
1289 &metrics,
1290 "transaction",
1291 &transaction_payload,
1292 &transaction_watermark,
1293 );
1294
1295 let mut event_payload = SubscribeEventsResponse::default();
1296 event_payload.event = Some(Default::default());
1297 event_payload.watermark = Some(Watermark::default());
1298 let mut event_watermark = SubscribeEventsResponse::default();
1299 event_watermark.watermark = Some(Watermark::default());
1300 assert_subscription_response_metrics(&metrics, "event", &event_payload, &event_watermark);
1301 }
1302}