Skip to main content

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}