zebra_rpc/server/rpc_tracing.rs
1//! RPC tracing middleware for OpenTelemetry SERVER spans.
2//!
3//! This middleware enables Jaeger Service Performance Monitoring (SPM) by marking
4//! JSON-RPC endpoints with `SPAN_KIND_SERVER`. SPM displays RED metrics (Rate, Errors,
5//! Duration) for server-side handlers.
6//!
7//! # Background
8//!
9//! Jaeger SPM filters for `SPAN_KIND_SERVER` spans by default. Without this middleware,
10//! all Zebra spans are `SPAN_KIND_INTERNAL`, making them invisible in SPM's Monitor tab.
11//!
12//! This follows OpenTelemetry best practices used by other blockchain clients:
13//! - Lighthouse (Ethereum consensus client)
14//! - Hyperledger Besu (Ethereum execution client)
15//! - Hyperledger Fabric
16
17use jsonrpsee::{
18 server::middleware::rpc::{layer::ResponseFuture, RpcServiceT},
19 MethodResponse,
20};
21use tracing::{info_span, Instrument};
22
23/// Middleware that creates SERVER spans for each RPC request.
24///
25/// This enables Jaeger SPM by marking RPC endpoints as server-side handlers.
26/// Also captures error codes and messages for debugging.
27///
28/// # OpenTelemetry Attributes
29///
30/// Each span includes:
31/// - `otel.kind = "server"` - Marks this as a server span for SPM
32/// - `rpc.method` - The JSON-RPC method name (e.g., "getinfo", "getblock")
33/// - `rpc.system = "jsonrpc"` - The RPC protocol
34/// - `otel.status_code` - "ERROR" on failure (empty on success)
35/// - `rpc.error_code` - JSON-RPC error code on failure
36#[derive(Clone)]
37pub struct RpcTracingMiddleware<S> {
38 service: S,
39}
40
41impl<S> RpcTracingMiddleware<S> {
42 /// Create a new `RpcTracingMiddleware` with the given `service`.
43 pub fn new(service: S) -> Self {
44 Self { service }
45 }
46}
47
48impl<'a, S> RpcServiceT<'a> for RpcTracingMiddleware<S>
49where
50 S: RpcServiceT<'a> + Send + Sync + Clone + 'static,
51{
52 type Future = ResponseFuture<futures::future::BoxFuture<'a, MethodResponse>>;
53
54 fn call(&self, request: jsonrpsee::types::Request<'a>) -> Self::Future {
55 let service = self.service.clone();
56 let method = request.method_name().to_owned();
57
58 // Create span OUTSIDE the async block so it's properly registered
59 // with the tracing subscriber before the future starts.
60 let span = info_span!(
61 "rpc_request",
62 otel.kind = "server",
63 rpc.method = %method,
64 rpc.system = "jsonrpc",
65 otel.status_code = tracing::field::Empty,
66 rpc.error_code = tracing::field::Empty,
67 );
68
69 // Clone span for recording after response
70 let span_for_record = span.clone();
71
72 // Instrument the ENTIRE future with the span
73 ResponseFuture::future(Box::pin(
74 async move {
75 let response = service.call(request).await;
76
77 // Record error details if the response is an error
78 if response.is_error() {
79 span_for_record.record("otel.status_code", "ERROR");
80 if let Some(error_code) = response.as_error_code() {
81 span_for_record.record("rpc.error_code", error_code);
82 }
83 }
84
85 response
86 }
87 .instrument(span),
88 ))
89 }
90}