Skip to main content

protocol_droid/application/server/
log_gate.rs

1//! Which `notifications/message` records a session has asked to receive.
2//!
3//! # Two revisions, two owners of the threshold
4//!
5//! Through `2025-11-25` the threshold belongs to the **session**:
6//! `logging/setLevel` sets it once and every later record is filtered against
7//! it. `2026-07-28` moved it onto the **request**: SEP-2575 replaced the RPC
8//! with the `io.modelcontextprotocol/logLevel` key in `_meta`, and a request
9//! that omits the key receives no log records at all — "the server MUST NOT
10//! send any `notifications/message` notifications for this request".
11//!
12//! One type serves both because the question a handler asks is the same:
13//! "will this record be read?". The gate is shared by every clone of a peer,
14//! so a stateful session's `setLevel` reaches the requests that follow it, and
15//! a stateless request's gate is built fresh from its own `_meta`, so no
16//! request can inherit another's.
17
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU8, Ordering};
20
21use crate::domain::ir::LogLevel;
22
23/// Every record passes.
24const OPEN: u8 = 0;
25/// No record passes.
26const CLOSED: u8 = u8::MAX;
27
28/// A session's or a request's log threshold.
29///
30/// Cloning shares the threshold, which is the point: a stateful session's
31/// peer is cloned into every request it serves.
32#[derive(Debug, Clone)]
33pub struct LogGate {
34    threshold: Arc<AtomicU8>,
35}
36
37impl LogGate {
38    /// A gate every record passes: a stateful session before any `setLevel`.
39    #[must_use]
40    pub fn open() -> Self {
41        Self::stored(OPEN)
42    }
43
44    /// A gate no record passes: a `2026-07-28` request with no `logLevel`.
45    #[must_use]
46    pub fn closed() -> Self {
47        Self::stored(CLOSED)
48    }
49
50    /// A gate passing `level` and everything more severe.
51    #[must_use]
52    pub fn at(level: LogLevel) -> Self {
53        Self::stored(encode(level))
54    }
55
56    /// The gate a `2026-07-28` request's `logLevel` key describes.
57    #[must_use]
58    pub fn for_request(level: Option<LogLevel>) -> Self {
59        level.map_or_else(Self::closed, Self::at)
60    }
61
62    /// Moves the threshold, for `logging/setLevel`.
63    pub fn set(&self, level: LogLevel) {
64        self.threshold.store(encode(level), Ordering::Relaxed);
65    }
66
67    /// Whether a record at `level` should be sent.
68    #[must_use]
69    pub fn admits(&self, level: LogLevel) -> bool {
70        match self.threshold.load(Ordering::Relaxed) {
71            OPEN => true,
72            CLOSED => false,
73            threshold => encode(level) >= threshold,
74        }
75    }
76
77    fn stored(value: u8) -> Self {
78        Self {
79            threshold: Arc::new(AtomicU8::new(value)),
80        }
81    }
82}
83
84impl Default for LogGate {
85    fn default() -> Self {
86        Self::open()
87    }
88}
89
90/// `1..=8`, so `0` and `u8::MAX` stay free for the open and closed states.
91fn encode(level: LogLevel) -> u8 {
92    let index = LogLevel::ALL
93        .iter()
94        .position(|candidate| *candidate == level)
95        .unwrap_or_default();
96    u8::try_from(index).map_or(1, |index| index.saturating_add(1))
97}
98
99#[cfg(test)]
100mod tests {
101    use super::LogGate;
102    use crate::domain::ir::LogLevel;
103
104    #[test]
105    fn an_open_gate_admits_everything() {
106        assert!(LogGate::open().admits(LogLevel::Debug));
107    }
108
109    #[test]
110    fn a_closed_gate_admits_nothing() {
111        assert!(!LogGate::closed().admits(LogLevel::Emergency));
112    }
113
114    #[test]
115    fn a_threshold_admits_its_level_and_above() {
116        let gate = LogGate::at(LogLevel::Warning);
117        assert!(!gate.admits(LogLevel::Info));
118        assert!(gate.admits(LogLevel::Warning));
119        assert!(gate.admits(LogLevel::Error));
120    }
121
122    #[test]
123    fn a_request_without_a_level_is_closed() {
124        assert!(!LogGate::for_request(None).admits(LogLevel::Emergency));
125        assert!(LogGate::for_request(Some(LogLevel::Debug)).admits(LogLevel::Debug));
126    }
127
128    #[test]
129    fn set_reaches_every_clone() {
130        let gate = LogGate::open();
131        let clone = gate.clone();
132        gate.set(LogLevel::Error);
133        assert!(!clone.admits(LogLevel::Warning));
134        assert!(clone.admits(LogLevel::Error));
135    }
136
137    #[test]
138    fn the_stored_order_agrees_with_the_level_order() {
139        for (lower, higher) in LogLevel::ALL.iter().zip(LogLevel::ALL.iter().skip(1)) {
140            assert!(lower < higher, "{lower} sorts below {higher}");
141            assert!(super::encode(*lower) < super::encode(*higher));
142        }
143    }
144}