mysten_network/request_log/
mod.rs1mod body;
31mod layer;
32mod service;
33
34pub use self::body::RequestLogBody;
35pub use self::layer::GrpcRequestLogLayer;
36pub use self::service::GrpcRequestLog;
37
38pub const TARGET: &str = "grpc_request";
40
41#[cfg(test)]
42mod tests {
43 use std::collections::BTreeMap;
44 use std::convert::Infallible;
45 use std::sync::Arc;
46 use std::sync::Mutex;
47
48 use base64::Engine as _;
49 use bytes::Bytes;
50 use http_body::Frame;
51 use http_body_util::BodyExt;
52 use http_body_util::Empty;
53 use http_body_util::Full;
54 use http_body_util::StreamBody;
55 use prost::Message;
56 use tower::Layer;
57 use tower::ServiceExt;
58 use tracing::field::Field;
59 use tracing::field::Visit;
60 use tracing_subscriber::EnvFilter;
61 use tracing_subscriber::Layer as SubscriberLayer;
62 use tracing_subscriber::layer::Context;
63 use tracing_subscriber::layer::SubscriberExt;
64
65 use super::*;
66
67 type Event = BTreeMap<String, String>;
69
70 #[derive(Clone, Default)]
73 struct CaptureLayer {
74 events: Arc<Mutex<Vec<Event>>>,
75 }
76
77 impl<S: tracing::Subscriber> SubscriberLayer<S> for CaptureLayer {
78 fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
79 let mut visitor = FieldVisitor(Event::new());
80 event.record(&mut visitor);
81 self.events.lock().unwrap().push(visitor.0);
82 }
83 }
84
85 struct FieldVisitor(Event);
86
87 impl Visit for FieldVisitor {
88 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
89 self.0.insert(field.name().to_owned(), format!("{value:?}"));
90 }
91
92 fn record_str(&mut self, field: &Field, value: &str) {
93 self.0.insert(field.name().to_owned(), value.to_owned());
94 }
95 }
96
97 fn frame(flag: u8, message: &[u8]) -> Vec<u8> {
99 let mut framed = vec![flag];
100 framed.extend_from_slice(&(message.len() as u32).to_be_bytes());
101 framed.extend_from_slice(message);
102 framed
103 }
104
105 fn health_check_bytes(service: &str) -> Vec<u8> {
107 tonic_health::pb::HealthCheckRequest {
108 service: service.to_owned(),
109 }
110 .encode_to_vec()
111 }
112
113 fn framed_health_check(service: &str) -> Vec<u8> {
115 frame(0, &health_check_bytes(service))
116 }
117
118 fn base64_health_check(service: &str) -> String {
120 base64::engine::general_purpose::STANDARD.encode(health_check_bytes(service))
121 }
122
123 fn health_layer() -> GrpcRequestLogLayer {
124 GrpcRequestLogLayer::from_encoded_file_descriptor_sets([
125 tonic_health::pb::FILE_DESCRIPTOR_SET,
126 ])
127 .unwrap()
128 }
129
130 struct SpanLevelCap;
134
135 impl<S: tracing::Subscriber> SubscriberLayer<S> for SpanLevelCap {
136 fn register_callsite(
137 &self,
138 metadata: &'static tracing::Metadata<'static>,
139 ) -> tracing::subscriber::Interest {
140 use tracing::level_filters::LevelFilter;
141 if metadata.is_span() && LevelFilter::from_level(*metadata.level()) > LevelFilter::INFO
142 {
143 tracing::subscriber::Interest::never()
144 } else {
145 tracing::subscriber::Interest::sometimes()
146 }
147 }
148 }
149
150 async fn send_body<S, B>(
152 subscriber: S,
153 layer: GrpcRequestLogLayer,
154 path: &str,
155 content_type: &str,
156 body: B,
157 ) where
158 S: tracing::Subscriber + Send + Sync + 'static,
159 B: http_body::Body<Data = Bytes> + Send + 'static,
160 B::Error: std::fmt::Debug,
161 {
162 let _guard = tracing::subscriber::set_default(subscriber);
163
164 let service = layer.layer(tower::service_fn(
165 |request: http::Request<RequestLogBody<B>>| async move {
166 request.into_body().collect().await.unwrap();
168 Ok::<_, Infallible>(http::Response::new(Empty::<Bytes>::new()))
169 },
170 ));
171
172 let request = http::Request::builder()
173 .method(http::Method::POST)
174 .uri(path)
175 .header(http::header::CONTENT_TYPE, content_type)
176 .body(body)
177 .unwrap();
178
179 service.oneshot(request).await.unwrap();
180 }
181
182 async fn capture_events<B>(
185 directive: &str,
186 layer: GrpcRequestLogLayer,
187 path: &str,
188 content_type: &str,
189 body: B,
190 ) -> Vec<Event>
191 where
192 B: http_body::Body<Data = Bytes> + Send + 'static,
193 B::Error: std::fmt::Debug,
194 {
195 let capture_layer = CaptureLayer::default();
196 let subscriber = tracing_subscriber::registry()
197 .with(EnvFilter::new(directive))
198 .with(capture_layer.clone());
199 send_body(subscriber, layer, path, content_type, body).await;
200
201 let events = capture_layer.events.lock().unwrap();
202 events.clone()
203 }
204
205 async fn capture_with_filter(directive: &str, path: &str, content_type: &str) -> Vec<String> {
207 capture_events(
208 directive,
209 health_layer(),
210 path,
211 content_type,
212 Full::new(Bytes::from(framed_health_check("x"))),
213 )
214 .await
215 .into_iter()
216 .filter_map(|mut event| event.remove("payload"))
217 .collect()
218 }
219
220 #[tokio::test]
221 async fn target_directive_captures_canonical_json() {
222 let payloads = capture_with_filter(
223 "grpc_request=trace",
224 "/grpc.health.v1.Health/Check",
225 "application/grpc",
226 )
227 .await;
228
229 assert_eq!(payloads, vec![base64_health_check("x")]);
230 }
231
232 #[tokio::test]
236 async fn target_directive_captures_under_global_span_level_cap() {
237 let capture_layer = CaptureLayer::default();
238 let subscriber = tracing_subscriber::registry()
239 .with(EnvFilter::new("grpc_request=trace"))
240 .with(capture_layer.clone())
241 .with(SpanLevelCap);
242 send_body(
243 subscriber,
244 health_layer(),
245 "/grpc.health.v1.Health/Check",
246 "application/grpc",
247 Full::new(Bytes::from(framed_health_check("x"))),
248 )
249 .await;
250
251 let events = capture_layer.events.lock().unwrap();
252 assert_eq!(events.len(), 1);
253 assert_eq!(events[0]["payload"], base64_health_check("x"));
254 }
255
256 #[tokio::test]
257 async fn disabled_target_captures_nothing() {
258 let payloads =
259 capture_with_filter("info", "/grpc.health.v1.Health/Check", "application/grpc").await;
260
261 assert_eq!(payloads, Vec::<String>::new());
262 }
263
264 #[tokio::test]
265 async fn service_span_field_directive_captures_matching_service() {
266 let payloads = capture_with_filter(
267 "grpc_request[{service=grpc.health.v1.Health}]=trace",
268 "/grpc.health.v1.Health/Check",
269 "application/grpc",
270 )
271 .await;
272
273 assert_eq!(payloads, vec![base64_health_check("x")]);
274 }
275
276 #[tokio::test]
277 async fn service_span_field_directive_skips_other_services() {
278 let payloads = capture_with_filter(
279 "grpc_request[{service=some.other.Service}]=trace",
280 "/grpc.health.v1.Health/Check",
281 "application/grpc",
282 )
283 .await;
284
285 assert_eq!(payloads, Vec::<String>::new());
286 }
287
288 #[tokio::test]
289 async fn method_span_field_directive_captures_matching_method() {
290 let payloads = capture_with_filter(
291 "grpc_request[{method=Check}]=trace",
292 "/grpc.health.v1.Health/Check",
293 "application/grpc",
294 )
295 .await;
296
297 assert_eq!(payloads, vec![base64_health_check("x")]);
298 }
299
300 #[tokio::test]
301 async fn unknown_path_captures_nothing() {
302 let payloads = capture_with_filter(
303 "grpc_request=trace",
304 "/unknown.Service/Method",
305 "application/grpc",
306 )
307 .await;
308
309 assert_eq!(payloads, Vec::<String>::new());
310 }
311
312 #[tokio::test]
313 async fn non_grpc_content_type_captures_nothing() {
314 let payloads = capture_with_filter(
315 "grpc_request=trace",
316 "/grpc.health.v1.Health/Check",
317 "application/json",
318 )
319 .await;
320
321 assert_eq!(payloads, Vec::<String>::new());
322 }
323
324 #[tokio::test]
328 async fn chunked_body_captures_first_message_and_caps_the_rest() {
329 let mut bytes = framed_health_check("a");
330 bytes.extend_from_slice(&framed_health_check("b"));
331 let chunks: Vec<Result<Frame<Bytes>, Infallible>> = bytes
332 .chunks(3)
333 .map(|chunk| Ok(Frame::data(Bytes::copy_from_slice(chunk))))
334 .collect();
335
336 let events = capture_events(
337 "grpc_request=trace",
338 health_layer(),
339 "/grpc.health.v1.Health/Check",
340 "application/grpc",
341 StreamBody::new(futures::stream::iter(chunks)),
342 )
343 .await;
344
345 assert_eq!(events.len(), 2);
346 assert_eq!(events[0]["payload"], base64_health_check("a"));
347 assert_eq!(events[0]["message_index"], "0");
348 assert!(events[1]["message"].contains("too many messages"));
349 }
350
351 #[tokio::test]
354 async fn oversized_message_emits_payloadless_event() {
355 let bytes = frame(0, &[0x20; 32]); let events = capture_events(
358 "grpc_request=trace",
359 health_layer().with_max_captured_message_size(16),
360 "/grpc.health.v1.Health/Check",
361 "application/grpc",
362 Full::new(Bytes::from(bytes)),
363 )
364 .await;
365
366 assert_eq!(events.len(), 1);
367 assert_eq!(events[0]["skipped"], "message too large");
368 assert_eq!(events[0]["message_len"], "32");
369 assert!(!events[0].contains_key("payload"));
370 }
371
372 #[tokio::test]
375 async fn compressed_message_emits_payloadless_event() {
376 let bytes = frame(1, b"zz");
377
378 let events = capture_events(
379 "grpc_request=trace",
380 health_layer(),
381 "/grpc.health.v1.Health/Check",
382 "application/grpc",
383 Full::new(Bytes::from(bytes)),
384 )
385 .await;
386
387 assert_eq!(events.len(), 1);
388 assert_eq!(events[0]["skipped"], "compressed message");
389 }
390
391 #[tokio::test]
394 async fn invalid_protobuf_message_is_still_captured() {
395 let garbage = [0xFF]; let bytes = frame(0, &garbage);
397
398 let events = capture_events(
399 "grpc_request=trace",
400 health_layer(),
401 "/grpc.health.v1.Health/Check",
402 "application/grpc",
403 Full::new(Bytes::from(bytes)),
404 )
405 .await;
406
407 assert_eq!(events.len(), 1);
408 assert_eq!(
409 events[0]["payload"],
410 base64::engine::general_purpose::STANDARD.encode(garbage)
411 );
412 }
413
414 #[tokio::test]
417 async fn capture_stops_after_max_messages() {
418 let events = capture_events(
420 "grpc_request=trace",
421 health_layer(),
422 "/grpc.health.v1.Health/Check",
423 "application/grpc",
424 Full::new(Bytes::from(vec![0u8; 5 * 70])),
425 )
426 .await;
427
428 assert_eq!(events.len(), 2);
429 assert_eq!(events[0]["payload"], "");
430 assert!(events[1]["message"].contains("too many messages"));
431 assert_eq!(events[1]["message_count"], "1");
432 }
433}