zebra_rpc/lightwalletd/
tracing.rs1use std::{
11 task::{Context, Poll},
12 time::Instant,
13};
14
15use futures::future::BoxFuture;
16use tonic::codegen::http;
17use tower::{Layer, Service};
18use tracing::{debug, info_span, Instrument};
19
20#[derive(Clone, Copy, Debug, Default)]
22pub struct GrpcTracingLayer;
23
24impl<S> Layer<S> for GrpcTracingLayer {
25 type Service = GrpcTracing<S>;
26
27 fn layer(&self, service: S) -> Self::Service {
28 GrpcTracing { service }
29 }
30}
31
32#[derive(Clone, Debug)]
34pub struct GrpcTracing<S> {
35 service: S,
36}
37
38impl<S, RequestBody, ResponseBody> Service<http::Request<RequestBody>> for GrpcTracing<S>
39where
40 S: Service<http::Request<RequestBody>, Response = http::Response<ResponseBody>>
41 + Clone
42 + Send
43 + 'static,
44 S::Future: Send + 'static,
45 RequestBody: Send + 'static,
46{
47 type Response = S::Response;
48 type Error = S::Error;
49 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
50
51 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
52 self.service.poll_ready(cx)
53 }
54
55 fn call(&mut self, request: http::Request<RequestBody>) -> Self::Future {
56 let method = grpc_method(request.uri().path()).to_string();
57
58 let span = info_span!(
61 "lightwalletd_grpc_request",
62 otel.kind = "server",
63 rpc.method = %method,
64 rpc.system = "grpc",
65 rpc.grpc.status_code = tracing::field::Empty,
66 );
67 let span_for_record = span.clone();
68
69 let clone = self.service.clone();
72 let mut service = std::mem::replace(&mut self.service, clone);
73
74 Box::pin(
75 async move {
76 debug!("handling lightwalletd gRPC request");
77
78 let start = Instant::now();
79 let response = service.call(request).await;
80 let elapsed_ms = start.elapsed().as_millis();
81
82 match &response {
83 Ok(response) => {
84 let status = response
89 .headers()
90 .get("grpc-status")
91 .and_then(|status| status.to_str().ok());
92
93 match status {
94 Some(status) => {
95 span_for_record.record("rpc.grpc.status_code", status);
96 debug!(
97 grpc_status = status,
98 elapsed_ms, "rejected lightwalletd gRPC request",
99 );
100 }
101 None => debug!(elapsed_ms, "responded to lightwalletd gRPC request"),
105 }
106 }
107 Err(_) => debug!(
108 elapsed_ms,
109 "lightwalletd gRPC request failed before a response was produced",
110 ),
111 }
112
113 response
114 }
115 .instrument(span),
116 )
117 }
118}
119
120fn grpc_method(path: &str) -> &str {
124 path.rsplit('/').next().unwrap_or(path)
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn grpc_method_is_the_last_path_segment() {
133 assert_eq!(
134 grpc_method("/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock"),
135 "GetBlock",
136 );
137 assert_eq!(
139 grpc_method("/grpc.reflection.v1.ServerReflection/ServerReflectionInfo"),
140 "ServerReflectionInfo",
141 );
142 assert_eq!(grpc_method("nonsense"), "nonsense");
144 }
145
146 #[tokio::test]
151 async fn requests_pass_through_unchanged() {
152 use tower::ServiceExt;
153
154 let inner = tower::service_fn(|_request: http::Request<()>| async {
155 Ok::<_, std::convert::Infallible>(
156 http::Response::builder()
157 .status(200)
158 .body(())
159 .expect("a 200 response is valid"),
160 )
161 });
162
163 let mut service = GrpcTracingLayer.layer(inner);
164
165 for _ in 0..2 {
166 let request = http::Request::builder()
167 .uri("/cash.z.wallet.sdk.rpc.CompactTxStreamer/GetBlock")
168 .body(())
169 .expect("the request is valid");
170
171 let response = service
172 .ready()
173 .await
174 .expect("the service is always ready")
175 .call(request)
176 .await
177 .expect("the inner service always succeeds");
178
179 assert_eq!(response.status(), 200);
180 }
181 }
182}