Skip to main content

systemprompt_logging/services/output/
mod.rs

1//! CLI output mode flags and the pre-init notice buffer, owned by the
2//! installed subscriber.
3//!
4//! [`OutputState`] is created once at the CLI composition root, handed to the
5//! subscriber installer, and recovered by display sinks through
6//! [`OutputState::current`], which downcasts the active dispatcher the same
7//! way the database layer is found. With no subscriber carrying one, `current`
8//! returns a detached default.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{Arc, Mutex};
15
16use systemprompt_manifest::profile::LogFormat;
17use tracing::Subscriber;
18use tracing_subscriber::Layer;
19
20#[derive(Debug, Clone)]
21pub struct BufferedNotice {
22    pub level: String,
23    pub text: String,
24}
25
26#[derive(Debug)]
27struct Inner {
28    startup: AtomicBool,
29    structured: AtomicBool,
30    structured_emitted: AtomicBool,
31    json_console: AtomicBool,
32    console_armed: AtomicBool,
33    notices: Mutex<Vec<BufferedNotice>>,
34}
35
36/// Output mode and notice buffer for one process; clones share the same state.
37#[derive(Debug, Clone)]
38pub struct OutputState {
39    inner: Arc<Inner>,
40}
41
42impl Default for OutputState {
43    fn default() -> Self {
44        Self {
45            inner: Arc::new(Inner {
46                startup: AtomicBool::new(true),
47                structured: AtomicBool::new(false),
48                structured_emitted: AtomicBool::new(false),
49                json_console: AtomicBool::new(false),
50                console_armed: AtomicBool::new(true),
51                notices: Mutex::new(Vec::new()),
52            }),
53        }
54    }
55}
56
57impl<S: Subscriber> Layer<S> for OutputState {}
58
59impl OutputState {
60    #[must_use]
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    #[must_use]
66    pub fn current() -> Self {
67        tracing::dispatcher::get_default(|dispatch| dispatch.downcast_ref::<Self>().cloned())
68            .unwrap_or_default()
69    }
70
71    pub fn set_startup_mode(&self, enabled: bool) {
72        self.inner.startup.store(enabled, Ordering::SeqCst);
73    }
74
75    #[must_use]
76    pub fn is_startup_mode(&self) -> bool {
77        self.inner.startup.load(Ordering::SeqCst)
78    }
79
80    pub fn set_structured_output(&self, enabled: bool) {
81        self.inner.structured.store(enabled, Ordering::SeqCst);
82    }
83
84    #[must_use]
85    pub fn is_structured_output(&self) -> bool {
86        self.inner.structured.load(Ordering::SeqCst)
87    }
88
89    pub fn mark_structured_emitted(&self) {
90        self.inner.structured_emitted.store(true, Ordering::SeqCst);
91    }
92
93    pub fn reset_structured_emitted(&self) {
94        self.inner.structured_emitted.store(false, Ordering::SeqCst);
95    }
96
97    #[must_use]
98    pub fn structured_was_emitted(&self) -> bool {
99        self.inner.structured_emitted.load(Ordering::SeqCst)
100    }
101
102    pub fn set_console_format(&self, format: LogFormat) {
103        self.inner
104            .json_console
105            .store(matches!(format, LogFormat::Json), Ordering::Relaxed);
106    }
107
108    #[must_use]
109    pub fn console_format(&self) -> LogFormat {
110        if self.is_json_console() {
111            LogFormat::Json
112        } else {
113            LogFormat::Text
114        }
115    }
116
117    pub(crate) fn is_json_console(&self) -> bool {
118        self.inner.json_console.load(Ordering::Relaxed)
119    }
120
121    pub(crate) fn set_console_armed(&self, armed: bool) {
122        self.inner.console_armed.store(armed, Ordering::SeqCst);
123    }
124
125    pub(crate) fn is_console_armed(&self) -> bool {
126        self.inner.console_armed.load(Ordering::SeqCst)
127    }
128
129    pub fn buffer_notice(&self, level: &str, text: &str) {
130        if let Ok(mut buf) = self.inner.notices.lock() {
131            buf.push(BufferedNotice {
132                level: level.to_owned(),
133                text: text.to_owned(),
134            });
135        }
136    }
137
138    #[must_use]
139    pub fn drain_notices(&self) -> Vec<BufferedNotice> {
140        self.inner
141            .notices
142            .lock()
143            .map(|mut buf| std::mem::take(&mut *buf))
144            .unwrap_or_default()
145    }
146}