Skip to main content

gammalooprs/utils/
tracing.rs

1use bincode_trait_derive::{Decode, Encode};
2use clap::ValueEnum;
3use gammaloop_tracing_filter::{GammaDisplayFormat, GammaLogFilter};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use std::{path::PathBuf, sync::OnceLock};
7use tracing::level_filters::LevelFilter;
8use tracing_appender::non_blocking::WorkerGuard;
9use tracing_subscriber::{fmt, prelude::*};
10#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
11/// Amount of prefix and source detail shown by the terminal and logfile sinks.
12#[repr(usize)]
13#[derive(
14    Clone,
15    Copy,
16    PartialEq,
17    Eq,
18    PartialOrd,
19    Ord,
20    Debug,
21    Hash,
22    Encode,
23    Decode,
24    JsonSchema,
25    ValueEnum,
26    Serialize,
27    Deserialize,
28    Default,
29)]
30pub enum LogFormat {
31    #[default]
32    Long,
33    Full,
34    Short,
35    Min,
36    None,
37}
38
39#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
40#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, JsonSchema, PartialEq, Default)]
41#[serde(default, deny_unknown_fields)]
42pub struct LogStyle {
43    /// Amount of timestamp, target, level, and source context included in each log line.
44    #[serde(skip_serializing_if = "crate::utils::serde_utils::IsDefault::is_default")]
45    pub log_format: LogFormat,
46    /// Use compact timestamps instead of full date-and-time values.
47    #[serde(skip_serializing_if = "crate::utils::serde_utils::is_false")]
48    pub short_timestamp: bool,
49    /// Include the complete source file path and line number for each event.
50    #[serde(skip_serializing_if = "crate::utils::serde_utils::is_false")]
51    pub full_line_source: bool,
52    /// Render structured tracing fields in addition to the formatted event message.
53    #[serde(skip_serializing_if = "crate::utils::serde_utils::is_false")]
54    pub include_fields: bool,
55}
56
57impl From<LogFormat> for gammaloop_tracing_filter::LogFormat {
58    fn from(value: LogFormat) -> Self {
59        match value {
60            LogFormat::Long => Self::Long,
61            LogFormat::Full => Self::Full,
62            LogFormat::Short => Self::Short,
63            LogFormat::Min => Self::Min,
64            LogFormat::None => Self::None,
65        }
66    }
67}
68
69impl LogStyle {
70    pub fn to_runtime(&self) -> gammaloop_tracing_filter::LogStyle {
71        gammaloop_tracing_filter::LogStyle {
72            log_format: self.log_format.into(),
73            short_timestamp: self.short_timestamp,
74            full_line_source: self.full_line_source,
75            include_fields: self.include_fields,
76        }
77    }
78}
79
80impl From<&LogStyle> for gammaloop_tracing_filter::LogStyle {
81    fn from(value: &LogStyle) -> Self {
82        value.to_runtime()
83    }
84}
85
86impl From<LogStyle> for gammaloop_tracing_filter::LogStyle {
87    fn from(value: LogStyle) -> Self {
88        value.to_runtime()
89    }
90}
91
92#[repr(usize)]
93#[derive(
94    Clone,
95    Copy,
96    PartialEq,
97    Eq,
98    PartialOrd,
99    Ord,
100    Debug,
101    Hash,
102    ValueEnum,
103    Serialize,
104    Deserialize,
105    Encode,
106    Decode,
107    JsonSchema,
108)]
109#[cfg_attr(feature = "python-stubs", pyo3_stub_gen::derive::gen_stub_pyclass_enum)]
110#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
111#[derive(Default)]
112pub enum LogLevel {
113    /// A level lower than all log levels.
114    Off,
115    /// Corresponds to the `Error` log level.
116    Error,
117    /// Corresponds to the `Warn` log level.
118    Warn,
119    /// Corresponds to the `Info` log level.
120    #[default]
121    Info,
122    /// Corresponds to the `Debug` log level.
123    Debug,
124    /// Corresponds to the `Trace` log level.
125    Trace,
126}
127
128impl From<LogLevel> for LevelFilter {
129    fn from(value: LogLevel) -> Self {
130        match value {
131            LogLevel::Debug => LevelFilter::DEBUG,
132            LogLevel::Trace => LevelFilter::TRACE,
133            LogLevel::Warn => LevelFilter::WARN,
134            LogLevel::Error => LevelFilter::ERROR,
135            LogLevel::Off => LevelFilter::OFF,
136            LogLevel::Info => LevelFilter::INFO,
137        }
138    }
139}
140
141impl LogLevel {
142    pub fn to_env_spec(self) -> &'static str {
143        match self {
144            LogLevel::Off => "gammalooprs=off",
145            LogLevel::Error => "gammalooprs=error",
146            LogLevel::Warn => "gammalooprs=warn",
147            LogLevel::Info => "gammalooprs=info",
148            LogLevel::Debug => "gammalooprs=debug",
149            LogLevel::Trace => "gammalooprs=trace",
150        }
151    }
152
153    pub fn to_cli_display_directive_spec(self) -> &'static str {
154        match self {
155            LogLevel::Off => "gammaloop_api=off,gammalooprs=off",
156            LogLevel::Error => "gammaloop_api=error,gammalooprs=error",
157            LogLevel::Warn => "gammaloop_api=warn,gammalooprs=warn",
158            LogLevel::Info => "gammaloop_api=info,gammalooprs=info",
159            LogLevel::Debug => "gammaloop_api=debug,gammalooprs=debug",
160            LogLevel::Trace => "gammaloop_api=trace,gammalooprs=trace",
161        }
162    }
163
164    pub fn to_cli_logfile_directive_spec(self) -> &'static str {
165        self.to_cli_display_directive_spec()
166    }
167}
168
169// Global one-time slots.
170static TEST_TRACING_INITIALISED: OnceLock<()> = OnceLock::new();
171pub static LOG_GUARD: OnceLock<WorkerGuard> = OnceLock::new();
172
173const ENV_FILE_LOG_FILTER: &str = "GL_LOGFILE_FILTER";
174const ENV_DISPLAY_LOG_FILTER: &str = "GL_DISPLAY_FILTER";
175const ENV_ALL_LOG_FILTER: &str = "GL_ALL_LOG_FILTER";
176const ENV_TEST_LOG_DIR: &str = "GL_TEST_LOG_DIR";
177
178pub fn init_test_tracing() {
179    TEST_TRACING_INITIALISED.get_or_init(|| {
180        init_test_tracing_with_defaults("info", "off");
181    });
182}
183
184pub fn init_bench_tracing() {
185    TEST_TRACING_INITIALISED.get_or_init(|| {
186        init_test_tracing_with_defaults("warn", "off");
187    });
188}
189
190fn init_test_tracing_with_defaults(display_default: &str, file_default: &str) {
191    let display_spec = log_filter_env_override(ENV_DISPLAY_LOG_FILTER)
192        .unwrap_or_else(|| display_default.to_string());
193    let file_spec =
194        log_filter_env_override(ENV_FILE_LOG_FILTER).unwrap_or_else(|| file_default.to_string());
195
196    let (_display_spec, display_filter) =
197        parse_log_filter_or_default(&display_spec, display_default, "display");
198    let (file_spec, file_filter) = parse_log_filter_or_default(&file_spec, file_default, "file");
199
200    let display_layer = fmt::layer()
201        .event_format(GammaDisplayFormat::new(
202            LogStyle {
203                log_format: LogFormat::Full,
204                short_timestamp: true,
205                full_line_source: true,
206                include_fields: true,
207            }
208            .to_runtime(),
209        ))
210        .with_writer(std::io::stderr)
211        .with_filter(display_filter);
212
213    let subscriber = tracing_subscriber::registry().with(display_layer);
214    if GammaLogFilter::is_effectively_off(&file_spec) {
215        _ = subscriber.try_init();
216    } else {
217        let file_appender = tracing_appender::rolling::never(
218            test_log_dir(),
219            format!("gammaloop-test-{}.jsonl", std::process::id()),
220        );
221        let (file_writer, file_guard) = tracing_appender::non_blocking(file_appender);
222        let _ = LOG_GUARD.set(file_guard);
223
224        let file_layer = fmt::layer()
225            .json()
226            .with_writer(file_writer)
227            .with_filter(file_filter);
228
229        _ = subscriber.with(file_layer).try_init();
230    }
231}
232
233fn log_filter_env_override(specific_env: &str) -> Option<String> {
234    if let Ok(all) = std::env::var(ENV_ALL_LOG_FILTER) {
235        Some(all)
236    } else {
237        std::env::var(specific_env).ok()
238    }
239}
240
241fn parse_log_filter_or_default(
242    spec: &str,
243    default_spec: &str,
244    sink: &str,
245) -> (String, GammaLogFilter) {
246    let spec = if spec.trim().is_empty() {
247        default_spec
248    } else {
249        spec
250    };
251
252    match GammaLogFilter::parse(spec) {
253        Ok(filter) => (spec.to_string(), filter),
254        Err(err) => {
255            eprintln!(
256                "Invalid test {sink} log filter '{spec}': {err}. Falling back to {default_spec}."
257            );
258            (
259                default_spec.to_string(),
260                GammaLogFilter::parse(default_spec).expect("test log default filter must parse"),
261            )
262        }
263    }
264}
265
266fn test_log_dir() -> PathBuf {
267    std::env::var(ENV_TEST_LOG_DIR)
268        .map(PathBuf::from)
269        .unwrap_or_else(|_| {
270            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
271                .join("../..")
272                .join("target/test-logs")
273        })
274}
275
276use serde_json::Value;
277use std::borrow::Cow;
278
279/// Anything you want to show in status + attach as structured JSON.
280pub trait StatusRenderable {
281    /// Human-friendly, possibly multi-line (e.g. a table).
282    fn status_pretty(&self) -> Cow<'_, str>;
283
284    /// Machine-readable payload (stable shape for analysis).
285    fn status_json(&self) -> Value;
286}
287
288// If you already implement Display + Serialize on a type,
289// you get a decent default for free.
290impl<T> StatusRenderable for T
291where
292    T: Serialize + std::fmt::Display,
293{
294    fn status_pretty(&self) -> Cow<'_, str> {
295        Cow::Owned(self.to_string())
296    }
297    fn status_json(&self) -> Value {
298        serde_json::to_value(self).unwrap_or(Value::Null)
299    }
300}
301
302use std::io::IsTerminal;
303pub fn stderr_is_tty() -> bool {
304    std::io::stderr().is_terminal()
305}