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#[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 #[serde(skip_serializing_if = "crate::utils::serde_utils::IsDefault::is_default")]
45 pub log_format: LogFormat,
46 #[serde(skip_serializing_if = "crate::utils::serde_utils::is_false")]
48 pub short_timestamp: bool,
49 #[serde(skip_serializing_if = "crate::utils::serde_utils::is_false")]
51 pub full_line_source: bool,
52 #[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 Off,
115 Error,
117 Warn,
119 #[default]
121 Info,
122 Debug,
124 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
169static 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
279pub trait StatusRenderable {
281 fn status_pretty(&self) -> Cow<'_, str>;
283
284 fn status_json(&self) -> Value;
286}
287
288impl<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}