Skip to main content

miden_node_utils/tracing/
grpc.rs

1use http::header::HeaderName;
2use tracing::field;
3
4use crate::tracing::OpenTelemetrySpanExt;
5
6/// Returns a [`trace_fn`](tonic::transport::server::Server) implementation for gRPC requests
7/// which adds open-telemetry information to the span.
8///
9/// Creates an `info` span following the open-telemetry standard: `{service}/{method}`.
10/// The span name is dynamically set using the HTTP path via the `otel.name` field.
11/// Additionally also pulls in remote tracing context which allows the server trace to be connected
12/// to the client's origin trace.
13#[track_caller]
14pub fn grpc_trace_fn<T>(request: &http::Request<T>) -> tracing::Span {
15    // A gRPC request's path ends with `../<service>/<method>`.
16    let mut path_segments = request.uri().path().rsplit('/');
17
18    let method = path_segments.next().unwrap_or_default();
19    let service = path_segments.next().unwrap_or_default();
20
21    // Create a span with a generic, static name. Fields to be recorded after needs to be
22    // initialized as empty since otherwise the assignment will have no effect.
23    let span = match method {
24        "SyncState" | "SyncNullifiers" => tracing::debug_span!(
25            "rpc",
26            otel.name = field::Empty,
27            rpc.service = service,
28            rpc.method = method
29        ),
30        _ => tracing::info_span!(
31            "rpc",
32            otel.name = field::Empty,
33            rpc.service = service,
34            rpc.method = method
35        ),
36    };
37
38    // Set the span name via otel.name
39    let otel_name = format!("{service}/{method}");
40    span.record("otel.name", otel_name);
41
42    // Pull the open-telemetry parent context using the HTTP extractor
43    let otel_ctx = opentelemetry::global::get_text_map_propagator(|propagator| {
44        propagator.extract(&MetadataExtractor(&tonic::metadata::MetadataMap::from_headers(
45            request.headers().clone(),
46        )))
47    });
48    let _ = tracing_opentelemetry::OpenTelemetrySpanExt::set_parent(&span, otel_ctx);
49
50    // Adds various network attributes to the span, including remote address and port.
51    //
52    // See [server attributes](https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#server-attributes).
53
54    // Set HTTP attributes.
55    span.set_attribute("rpc.system", "grpc");
56    if let Some(host) = request.uri().host() {
57        span.set_attribute("server.address", host);
58    }
59    if let Some(host_port) = request.uri().port() {
60        span.set_attribute("server.port", host_port.as_u16());
61    }
62    let remote_addr = request
63        .extensions()
64        .get::<tonic::transport::server::TcpConnectInfo>()
65        .and_then(tonic::transport::server::TcpConnectInfo::remote_addr);
66    if let Some(addr) = remote_addr {
67        span.set_attribute("client.address", addr.ip());
68        span.set_attribute("client.port", addr.port());
69        span.set_attribute("network.peer.address", addr.ip());
70        span.set_attribute("network.peer.port", addr.port());
71        span.set_attribute("network.transport", "tcp");
72        match addr.ip() {
73            std::net::IpAddr::V4(_) => span.set_attribute("network.type", "ipv4"),
74            std::net::IpAddr::V6(_) => span.set_attribute("network.type", "ipv6"),
75        }
76    }
77
78    for header in [
79        http::header::ACCEPT,
80        http::header::ORIGIN,
81        http::header::USER_AGENT,
82        http::header::FORWARDED,
83        HeaderName::from_static("x-forwarded-for"),
84        HeaderName::from_static("x-real-ip"),
85    ] {
86        if let Some(value) = request.headers().get(&header) {
87            if let Ok(value) = value.to_str() {
88                span.set_attribute(format!("http.request.header.{header}"), value);
89            }
90        }
91    }
92
93    span
94}
95
96/// Injects open-telemetry remote context into traces.
97#[derive(Copy, Clone)]
98pub struct OtelInterceptor;
99
100impl tonic::service::Interceptor for OtelInterceptor {
101    fn call(
102        &mut self,
103        mut request: tonic::Request<()>,
104    ) -> Result<tonic::Request<()>, tonic::Status> {
105        use tracing_opentelemetry::OpenTelemetrySpanExt;
106        let ctx = tracing::Span::current().context();
107        opentelemetry::global::get_text_map_propagator(|propagator| {
108            propagator.inject_context(&ctx, &mut MetadataInjector(request.metadata_mut()));
109        });
110
111        Ok(request)
112    }
113}
114
115struct MetadataExtractor<'a>(&'a tonic::metadata::MetadataMap);
116impl opentelemetry::propagation::Extractor for MetadataExtractor<'_> {
117    /// Get a value for a key from the `MetadataMap`.  If the value can't be converted to &str,
118    /// returns None
119    fn get(&self, key: &str) -> Option<&str> {
120        self.0.get(key).and_then(|metadata| metadata.to_str().ok())
121    }
122
123    /// Collect all the keys from the `MetadataMap`.
124    fn keys(&self) -> Vec<&str> {
125        self.0
126            .keys()
127            .map(|key| match key {
128                tonic::metadata::KeyRef::Ascii(v) => v.as_str(),
129                tonic::metadata::KeyRef::Binary(v) => v.as_str(),
130            })
131            .collect::<Vec<_>>()
132    }
133}
134
135struct MetadataInjector<'a>(&'a mut tonic::metadata::MetadataMap);
136impl opentelemetry::propagation::Injector for MetadataInjector<'_> {
137    /// Set a key and value in the `MetadataMap`.  Does nothing if the key or value are not valid
138    /// inputs
139    fn set(&mut self, key: &str, value: String) {
140        if let Ok(key) = tonic::metadata::MetadataKey::from_bytes(key.as_bytes())
141            && let Ok(val) = tonic::metadata::MetadataValue::try_from(&value)
142        {
143            self.0.insert(key, val);
144        }
145    }
146}