Skip to main content

miden_node_utils/tracing/
grpc.rs

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