Skip to main content

tiny_tracing/
config.rs

1use std::str::FromStr;
2
3use crate::{errors::LoggerError, time::LocalTimer};
4use tracing::Level;
5use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
6
7/// Output format for log lines.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum LogFormat {
10    /// Human-readable single-line text output.
11    #[default]
12    Text,
13    /// One JSON object per log line.
14    Json,
15}
16
17impl LogFormat {
18    fn as_str(self) -> &'static str {
19        match self {
20            Self::Text => "text",
21            Self::Json => "json",
22        }
23    }
24}
25
26impl std::fmt::Display for LogFormat {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.write_str(self.as_str())
29    }
30}
31
32impl FromStr for LogFormat {
33    type Err = LoggerError;
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        match s.to_lowercase().as_str() {
37            "text" => Ok(Self::Text),
38            "json" => Ok(Self::Json),
39            other => Err(LoggerError::InvalidFormat(other.to_string())),
40        }
41    }
42}
43
44/// Builder for initializing the global tracing subscriber.
45///
46/// Uses a fluent builder pattern to configure log level, output format,
47/// and optional environment-based filtering before calling [`init`](Logger::init).
48///
49/// # Examples
50///
51/// Minimal setup (text output at INFO level):
52///
53/// ```rust
54/// use tiny_tracing::Logger;
55///
56/// Logger::new().init().unwrap();
57/// tiny_tracing::info!("Ready");
58/// ```
59///
60/// JSON output with environment filter:
61///
62/// ```rust
63/// use tiny_tracing::{Logger, LogFormat, Level};
64///
65/// Logger::new()
66///     .with_level(Level::DEBUG)
67///     .with_format(LogFormat::Json)
68///     .with_env_filter("info,my_crate=trace")
69///     .init()
70///     .unwrap();
71/// ```
72pub struct Logger {
73    level: Level,
74    format: LogFormat,
75    env_filter: Option<String>,
76    with_file: bool,
77    with_target: bool,
78}
79
80impl Logger {
81    /// Creates a new [`Logger`] with default values.
82    ///
83    /// Defaults: [`Level::INFO`], [`LogFormat::Text`], no env filter,
84    /// file location off, target on.
85    #[must_use]
86    pub fn new() -> Self {
87        Self {
88            level: Level::INFO,
89            format: LogFormat::Text,
90            env_filter: None,
91            with_file: false,
92            with_target: true,
93        }
94    }
95
96    /// Sets the maximum log level.
97    ///
98    /// Ignored if [`with_env_filter`](Self::with_env_filter) is also set.
99    #[must_use]
100    pub fn with_level(mut self, level: Level) -> Self {
101        self.level = level;
102        self
103    }
104
105    /// Sets the output format.
106    #[must_use]
107    pub fn with_format(mut self, format: LogFormat) -> Self {
108        self.format = format;
109        self
110    }
111
112    /// Enables environment-based filtering via [`EnvFilter`].
113    ///
114    /// When set, the filter string is used instead of the static level.
115    /// Supported syntax: `"info"`, `"info,my_crate=debug"`, etc.
116    ///
117    /// If not called, only the static [`with_level`](Logger::with_level) value applies.
118    #[must_use]
119    pub fn with_env_filter(mut self, filter: impl Into<String>) -> Self {
120        self.env_filter = Some(filter.into());
121        self
122    }
123
124    /// Controls whether the source file name appears in log lines.
125    ///
126    /// Disabled by default.
127    #[must_use]
128    pub fn with_file(mut self, enabled: bool) -> Self {
129        self.with_file = enabled;
130        self
131    }
132
133    /// Controls whether the module path (target) appears in log lines.
134    ///
135    /// Enabled by default.
136    #[must_use]
137    pub fn with_target(mut self, enabled: bool) -> Self {
138        self.with_target = enabled;
139        self
140    }
141
142    /// Returns the configured log level.
143    #[must_use]
144    pub fn level(&self) -> Level {
145        self.level
146    }
147
148    /// Returns the configured output format.
149    #[must_use]
150    pub fn format(&self) -> LogFormat {
151        self.format
152    }
153
154    /// Initializes the global tracing subscriber.
155    ///
156    /// Consumes the builder. Must be called only once per process;
157    /// subsequent calls return [`LoggerError::TryInitError`].
158    ///
159    /// # Errors
160    ///
161    /// - [`LoggerError::InvalidEnvFilter`] if an env filter was set and
162    ///   [`EnvFilter`] rejects it.
163    /// - [`LoggerError::TryInitError`] if a global subscriber is already set.
164    pub fn init(self) -> Result<(), LoggerError> {
165        let base = tracing_subscriber::fmt()
166            .with_thread_names(false)
167            .with_span_events(FmtSpan::FULL)
168            .with_file(self.with_file)
169            .with_target(self.with_target)
170            .with_timer(LocalTimer);
171
172        let env_filter = self
173            .env_filter
174            .as_deref()
175            .map(EnvFilter::try_new)
176            .transpose()
177            .map_err(|e| LoggerError::InvalidEnvFilter(e.to_string()))?;
178
179        let result = match self.format {
180            LogFormat::Json => {
181                let b = base.json();
182                match env_filter {
183                    Some(f) => b.with_env_filter(f).try_init(),
184                    None => b.with_max_level(self.level).try_init(),
185                }
186            }
187            LogFormat::Text => match env_filter {
188                Some(f) => base.with_env_filter(f).try_init(),
189                None => base.with_max_level(self.level).try_init(),
190            },
191        };
192
193        result.map_err(|e| LoggerError::TryInitError(e.to_string()))
194    }
195}
196
197impl Default for Logger {
198    fn default() -> Self {
199        Self::new()
200    }
201}