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