tincre_logger/logger.rs
1//! A simple, "zero-setup" logger that works out-of-the-box.
2//!
3//! This module provides a set of functions for logging messages at different
4//! levels (`info`, `warn`, `error`, `debug`). It automatically initializes
5//! on the first log call and respects the `RUST_LOG` environment variable.
6//!
7//! # Example
8//!
9//! ```
10//! use tincre_logger::logger;
11//!
12//! fn main() {
13//! logger::info("Server has started.");
14//! logger::warn("Low disk space detected.");
15//! logger::error("Failed to connect to database!");
16//! // To see debug messages, run with `RUST_LOG=debug`
17//! logger::debug("User 'admin' logged in.");
18//! }
19
20use chrono::Utc;
21use serde_json::Value;
22use tracing::{debug, error, info, warn};
23
24// --- Setup ---
25#[cfg_attr(coverage, coverage(off))]
26#[inline(always)]
27fn ensure_initialized() {
28 #[cfg(not(test))]
29 {
30 use std::sync::Once;
31 use tracing_subscriber::{prelude::*, EnvFilter};
32
33 static INIT: Once = Once::new();
34 INIT.call_once(|| {
35 let env_filter =
36 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
37
38 tracing_subscriber::registry()
39 .with(tracing_subscriber::fmt::layer().with_ansi(true))
40 .with(env_filter)
41 .init();
42 });
43 }
44}
45
46// --- Public API Functions ---
47
48/// Logs a message at the `INFO` level. This is an alias for `info()`.
49///
50/// # Example
51///
52/// ```
53/// use tincre_logger::logger;
54///
55/// logger::log("This is an informational message.");
56/// ```
57pub fn log(message: &str) {
58 ensure_initialized();
59 info!(message);
60}
61
62/// Logs a message at the `INFO` level.
63///
64/// # Example
65///
66/// ```
67/// use tincre_logger::logger;
68///
69/// logger::info("An informational message.");
70/// ```
71pub fn info(message: &str) {
72 ensure_initialized();
73 info!(message);
74}
75
76/// Logs a message at the `WARN` level.
77///
78/// # Example
79///
80/// ```
81/// use tincre_logger::logger;
82///
83/// logger::warn("A warning message.");
84/// ```
85pub fn warn(message: &str) {
86 ensure_initialized();
87 warn!(message);
88}
89
90/// Logs a message at the `ERROR` level.
91///
92/// # Example
93///
94/// ```
95/// use tincre_logger::logger;
96///
97/// logger::error("An error message.");
98/// ```
99pub fn error(message: &str) {
100 ensure_initialized();
101 error!(message);
102}
103
104/// Logs a message at the `DEBUG` level.
105///
106/// By default, debug messages are hidden. They can be enabled by setting
107/// the `RUST_LOG` environment variable (e.g., `RUST_LOG=debug`).
108///
109/// # Example
110///
111/// ```
112/// use tincre_logger::logger;
113///
114/// // To see this message, run your application with `RUST_LOG=debug`
115/// logger::debug("A verbose debug message for developers.");
116/// ```
117pub fn debug(message: &str) {
118 ensure_initialized();
119 debug!(message);
120}
121
122/// Logs a message at the `INFO` level with additional structured metadata.
123///
124/// This function accepts a message and a second parameter representing structured
125/// data. The data is serialized and logged as part of the event. A UTC timestamp is
126/// automatically injected.
127///
128/// # Example
129///
130/// ```
131/// use tincre_logger::logger;
132/// use serde_json::json;
133///
134/// logger::info_with("User signed in", json!({ "user_id": 42, "method": "oauth" }));
135/// ```
136pub fn info_with(message: &str, data: impl Into<Value>) {
137 ensure_initialized();
138 let timestamp = Utc::now().to_rfc3339();
139 info!(%timestamp, message = %message, data = ?data.into());
140}
141
142/// Logs a message at the `WARN` level with additional structured metadata.
143///
144/// This function is useful for highlighting warnings while attaching extra
145/// information, such as rate limit states or configuration drift. A UTC timestamp
146/// is automatically injected.
147///
148/// # Example
149///
150/// ```
151/// use tincre_logger::logger;
152/// use serde_json::json;
153///
154/// logger::warn_with("Cache miss", json!({ "key": "homepage", "attempts": 2 }));
155/// ```
156pub fn warn_with(message: &str, data: impl Into<Value>) {
157 ensure_initialized();
158 let timestamp = Utc::now().to_rfc3339();
159 warn!(%timestamp, message = %message, data = ?data.into());
160}
161
162/// Logs a message at the `ERROR` level with additional structured metadata.
163///
164/// This function is intended for errors that should be captured in monitoring
165/// pipelines with relevant context, such as error codes or service names.
166/// A UTC timestamp is automatically injected.
167///
168/// # Example
169///
170/// ```
171/// use tincre_logger::logger;
172/// use serde_json::json;
173///
174/// logger::error_with("Database write failed", json!({ "table": "users", "code": 500 }));
175/// ```
176pub fn error_with(message: &str, data: impl Into<Value>) {
177 ensure_initialized();
178 let timestamp = Utc::now().to_rfc3339();
179 error!(%timestamp, message = %message, data = ?data.into());
180}
181
182/// Logs a message at the `DEBUG` level with additional structured metadata.
183///
184/// By default, debug messages are hidden. They can be enabled by setting
185/// the `RUST_LOG` environment variable (e.g., `RUST_LOG=debug`). A UTC timestamp
186/// is automatically injected.
187///
188/// # Example
189///
190/// ```
191/// use tincre_logger::logger;
192/// use serde_json::json;
193///
194/// // To see this message, run your application with `RUST_LOG=debug`
195/// logger::debug_with("Loaded config", json!({ "env": "dev", "debug_mode": true }));
196/// ```
197pub fn debug_with(message: &str, data: impl Into<Value>) {
198 ensure_initialized();
199 let timestamp = Utc::now().to_rfc3339();
200 debug!(%timestamp, message = %message, data = ?data.into());
201}
202
203// --- Unit Tests ---
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use serde_json::json;
208 use std::io;
209 use std::sync::{Arc, Mutex};
210 use tracing_subscriber::{filter::LevelFilter, fmt, layer::SubscriberExt, registry};
211
212 #[derive(Clone)]
213 struct TestWriter {
214 buf: Arc<Mutex<Vec<u8>>>,
215 }
216
217 impl TestWriter {
218 fn new() -> Self {
219 Self {
220 buf: Arc::new(Mutex::new(Vec::new())),
221 }
222 }
223
224 fn get_contents(&self) -> String {
225 let mut buf = self.buf.lock().unwrap();
226 let output = String::from_utf8(buf.clone()).expect("Logs should be valid UTF-8");
227 buf.clear();
228 output
229 }
230 }
231
232 #[cfg_attr(coverage, coverage(off))]
233 impl io::Write for TestWriter {
234 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
235 self.buf.lock().unwrap().write(buf)
236 }
237
238 fn flush(&mut self) -> io::Result<()> {
239 self.buf.lock().unwrap().flush()
240 }
241 }
242
243 #[test]
244 fn it_logs_all_levels() {
245 let writer = TestWriter::new();
246 let writer_clone = writer.clone();
247
248 // Build a subscriber that captures all log levels for this test
249 let subscriber = registry()
250 .with(
251 fmt::layer()
252 .with_writer(move || writer_clone.clone())
253 .with_ansi(false),
254 )
255 // Explicitly set the filter to capture all levels down to TRACE
256 .with(LevelFilter::TRACE);
257
258 tracing::subscriber::with_default(subscriber, || {
259 log("hello world");
260 info("this is info");
261 warn("a warning message");
262 error("an error message");
263 debug("a debug message");
264 });
265
266 let output = writer.get_contents();
267
268 assert!(output.contains("INFO") && output.contains("hello world"));
269 assert!(output.contains("INFO") && output.contains("this is info"));
270 assert!(output.contains("WARN") && output.contains("a warning message"));
271 assert!(output.contains("ERROR") && output.contains("an error message"));
272 assert!(output.contains("DEBUG") && output.contains("a debug message"));
273 }
274
275 #[test]
276 fn it_logs_all_levels_with_data() {
277 let writer = TestWriter::new();
278 let writer_clone = writer.clone();
279
280 let subscriber = registry()
281 .with(
282 fmt::layer()
283 .with_writer(move || writer_clone.clone())
284 .with_ansi(false),
285 )
286 .with(LevelFilter::TRACE);
287
288 tracing::subscriber::with_default(subscriber, || {
289 info_with("structured info", json!({ "k": "v" }));
290 warn_with("structured warn", json!({ "warn_level": 2 }));
291 error_with("structured error", json!({ "err": "boom" }));
292 debug_with("structured debug", json!({ "flag": true }));
293 });
294
295 let output = writer.get_contents();
296
297 assert!(output.contains("INFO") && output.contains("structured info"));
298 assert!(output.contains("WARN") && output.contains("structured warn"));
299 assert!(output.contains("ERROR") && output.contains("structured error"));
300 assert!(output.contains("DEBUG") && output.contains("structured debug"));
301 assert!(output.contains("timestamp")); // check for injected field
302 assert!(output.contains('k') && output.contains('v'));
303 }
304}