Skip to main content

mysten_network/request_log/
service.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::task::{Context, Poll};
5
6use http::Request;
7use prost_reflect::DescriptorPool;
8use tower::Service;
9use tracing::trace_span;
10
11use super::TARGET;
12use super::body::{CaptureState, RequestLogBody};
13
14/// Middleware that captures decoded gRPC request messages on the [`TARGET`] tracing target.
15///
16/// See the [module docs](super) for how captures are enabled and collected.
17#[derive(Clone)]
18pub struct GrpcRequestLog<S> {
19    inner: S,
20    pool: DescriptorPool,
21    max_captured_message_size: usize,
22}
23
24impl<S> GrpcRequestLog<S> {
25    pub(crate) fn new(inner: S, pool: DescriptorPool, max_captured_message_size: usize) -> Self {
26        Self {
27            inner,
28            pool,
29            max_captured_message_size,
30        }
31    }
32
33    /// Decide whether to capture `request`, and if so set up the state the body wrapper needs:
34    /// the span carrying the `service`/`method` fields that `EnvFilter` span-field directives
35    /// filter on. Returns `None` (pass the body through untouched) when the target is disabled,
36    /// the request is not gRPC, or its path does not resolve to a method in the descriptor pool
37    /// (e.g. a service whose `FileDescriptorSet` was not registered).
38    fn capture_state<B>(&self, request: &Request<B>) -> Option<CaptureState> {
39        // Two probes, because they fail in complementary ways:
40        //
41        // - The event probe covers plain `grpc_request=trace` directives, and keeps working under
42        //   `telemetry-subscribers`, whose global level filter rejects every *span* callsite above
43        //   `TOKIO_SPAN_LEVEL` (default `info`) — trace events still flow, trace spans do not.
44        // - The span probe covers field-scoped directives like
45        //   `grpc_request[{service=...}]=trace`, for which `EnvFilter` disables events outside a
46        //   matching span, so the event probe — before any span exists — fails. It declares the
47        //   same field names as the real span below because `EnvFilter` only considers a
48        //   field-scoped directive for callsites that have those fields. Under
49        //   `telemetry-subscribers` this additionally requires `TOKIO_SPAN_LEVEL=trace` so the
50        //   span survives its global level filter.
51        //
52        // When no `grpc_request` directive is set at all, both probes short-circuit on cached
53        // callsite interest.
54        //
55        // Both probes are deliberately evaluated (no `||` short-circuit): under the full
56        // `telemetry-subscribers` stack, skipping the span probe when the event probe already
57        // passed was empirically observed to break field-scoped capture — the value-matched
58        // events stopped reaching any layer. Evaluating the span probe registers its
59        // fields-bearing callsite with every filter before the first `capture` span is created.
60        let event_enabled = tracing::event_enabled!(target: TARGET, tracing::Level::TRACE);
61        let span_enabled =
62            tracing::span_enabled!(target: TARGET, tracing::Level::TRACE, service, method);
63        if !(event_enabled || span_enabled) {
64            return None;
65        }
66
67        // Only gRPC requests carry the length-prefixed message framing the body wrapper parses.
68        // This also skips non-gRPC routes merged into the same router.
69        if !request
70            .headers()
71            .get(http::header::CONTENT_TYPE)?
72            .to_str()
73            .ok()?
74            .starts_with("application/grpc")
75        {
76            return None;
77        }
78
79        let path = request.uri().path();
80        let (service, method) = path.strip_prefix('/')?.split_once('/')?;
81        let service = self.pool.get_service_by_name(service)?;
82        let method = service.methods().find(|m| m.name() == method)?;
83
84        let span = trace_span!(
85            target: TARGET,
86            "capture",
87            service = %service.full_name(),
88            method = %method.name(),
89        );
90
91        // The probes above are callsite-level, so under a field-scoped directive they pass for
92        // *every* request regardless of its service/method values. Re-check with this request's
93        // span entered, so a global `EnvFilter` — which consults the span's recorded field values
94        // — prunes requests whose events would all be dropped anyway before any buffering or
95        // decoding happens. Per-layer filtered stacks (e.g. `telemetry-subscribers`) answer this
96        // aggregate check permissively and filter at dispatch instead; there the per-request
97        // message-count and message-size caps bound the wasted work.
98        if !span.in_scope(|| tracing::event_enabled!(target: TARGET, tracing::Level::TRACE)) {
99            return None;
100        }
101
102        Some(CaptureState::new(
103            span,
104            path.to_owned(),
105            self.max_captured_message_size,
106        ))
107    }
108}
109
110impl<S, ReqBody> Service<Request<ReqBody>> for GrpcRequestLog<S>
111where
112    S: Service<Request<RequestLogBody<ReqBody>>>,
113{
114    type Response = S::Response;
115    type Error = S::Error;
116    type Future = S::Future;
117
118    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
119        self.inner.poll_ready(cx)
120    }
121
122    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
123        let capture = self.capture_state(&request);
124        self.inner
125            .call(request.map(|body| RequestLogBody::new(body, capture)))
126    }
127}