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}