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