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