mysten_network/request_log/layer.rs
1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use prost_reflect::DescriptorPool;
5use tower::Layer;
6
7use super::GrpcRequestLog;
8
9/// Large enough for any Sui transaction payload (the protocol caps transactions well below this),
10/// and deliberately smaller than tonic's 4 MiB decode limit: the captured payload is re-emitted as
11/// a single JSON log line, so this also bounds log-line size and the memory buffered per request.
12/// Larger frames are skipped rather than buffered.
13const DEFAULT_MAX_CAPTURED_MESSAGE_SIZE: usize = 512 * 1024;
14
15/// Layer that applies [`GrpcRequestLog`] to a service.
16///
17/// See the [module docs](super) for how captures are enabled and collected.
18#[derive(Clone)]
19pub struct GrpcRequestLogLayer {
20 pool: DescriptorPool,
21 max_captured_message_size: usize,
22}
23
24impl GrpcRequestLogLayer {
25 pub fn new(pool: DescriptorPool) -> Self {
26 Self {
27 pool,
28 max_captured_message_size: DEFAULT_MAX_CAPTURED_MESSAGE_SIZE,
29 }
30 }
31
32 /// Build the descriptor pool from the same encoded `FileDescriptorSet`s the server registers
33 /// for gRPC reflection. Sets are decoded one at a time: files already added by an earlier set
34 /// are skipped (so well-known types appearing in several sets built with `--include_imports`
35 /// are fine), while duplicates *within* one set are malformed and error. A set may reference
36 /// files from the sets before it, so dependencies must come first.
37 pub fn from_encoded_file_descriptor_sets<'a>(
38 sets: impl IntoIterator<Item = &'a [u8]>,
39 ) -> Result<Self, prost_reflect::DescriptorError> {
40 // `DescriptorPool` indexes its symbol table with plain `std::collections::HashMap`s, seeded
41 // from `getrandom()`. Under `msim`, that syscall is only reliably intercepted (made
42 // deterministic) on macOS; on Linux it leaks real entropy into the simulated run, which
43 // desyncs the two-iteration determinism check the moment this pool is built at server
44 // startup. `nondeterministic!` moves the build onto an unintercepted thread so the leak is
45 // invisible to msim's tracked RNG log, the same fix used for `tempfile` and RocksDB
46 // elsewhere in this repo. Collect first so the closure moved onto that thread doesn't need
47 // a `Send` bound added to the public `impl IntoIterator` parameter.
48 let sets: Vec<&'a [u8]> = sets.into_iter().collect();
49 sui_macros::nondeterministic!({
50 let mut pool = DescriptorPool::new();
51 for set in sets {
52 pool.decode_file_descriptor_set(set)?;
53 }
54 Ok(Self::new(pool))
55 })
56 }
57
58 pub fn with_max_captured_message_size(mut self, bytes: usize) -> Self {
59 self.max_captured_message_size = bytes;
60 self
61 }
62}
63
64impl<S> Layer<S> for GrpcRequestLogLayer {
65 type Service = GrpcRequestLog<S>;
66
67 fn layer(&self, inner: S) -> Self::Service {
68 GrpcRequestLog::new(inner, self.pool.clone(), self.max_captured_message_size)
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 /// The same file appearing in more than one set must be skipped, not error — servers register
77 /// well-known types alongside sets built with `--include_imports` that embed them again.
78 #[test]
79 fn duplicate_files_across_sets_are_skipped() {
80 GrpcRequestLogLayer::from_encoded_file_descriptor_sets([
81 tonic_health::pb::FILE_DESCRIPTOR_SET,
82 tonic_health::pb::FILE_DESCRIPTOR_SET,
83 ])
84 .unwrap();
85 }
86}