Skip to content

Commit 637b823

Browse files
committed
[add] builder api for creating loggers.
1 parent 71ef98c commit 637b823

File tree

4 files changed

+238
-15
lines changed

4 files changed

+238
-15
lines changed

crates/lambda-rs-logging/README.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,11 @@ fn main() {
5151
use lambda_rs_logging as logging;
5252

5353
fn main() {
54-
let logger = logging::Logger::new(logging::LogLevel::INFO, "my-app");
55-
logger.add_handler(Box::new(logging::handler::ConsoleHandler::new("my-app")));
54+
let logger = logging::Logger::builder()
55+
.name("my-app")
56+
.level(logging::LogLevel::INFO)
57+
.with_handler(Box::new(logging::handler::ConsoleHandler::new("my-app")))
58+
.build();
5659

5760
logger.info("Hello world".to_string());
5861
logger.warn("Be careful".to_string());
@@ -64,8 +67,11 @@ fn main() {
6467
use lambda_rs_logging as logging;
6568

6669
fn main() {
67-
let logger = logging::Logger::new(logging::LogLevel::DEBUG, "app");
68-
logger.add_handler(Box::new(logging::handler::ConsoleHandler::new("app")));
70+
let logger = logging::Logger::builder()
71+
.name("app")
72+
.level(logging::LogLevel::DEBUG)
73+
.with_handler(Box::new(logging::handler::ConsoleHandler::new("app")))
74+
.build();
6975

7076
// Set the global logger before any macros are used
7177
logging::Logger::init(logger).expect("global logger can only be initialized once");
@@ -74,10 +80,24 @@ fn main() {
7480
}
7581
```
7682

83+
### Configure level from environment
84+
```rust
85+
use lambda_rs_logging as logging;
86+
87+
fn main() {
88+
// LAMBDA_LOG can be: trace|debug|info|warn|error|fatal
89+
// Example: export LAMBDA_LOG=debug
90+
logging::env::init_global_from_env().ok();
91+
92+
logging::info!("respects env filter");
93+
}
94+
```
95+
7796
## Notes
7897
- Thread-safe global with `OnceLock<Arc<Logger>>`.
7998
- Handlers are `Send + Sync` and receive a `Record` internally (phase 1 refactor).
8099
- `fatal!` logs at FATAL level but does not exit the process. Prefer explicit exits in your app logic.
100+
- Console handler colors only when attached to a TTY and writes WARN+ to stderr.
81101

82102
## Examples
83103
This crate ships with examples. From the repository root:
@@ -86,3 +106,8 @@ cargo run -p lambda-rs-logging --example 01_global_macros
86106
cargo run -p lambda-rs-logging --example 02_custom_logger
87107
cargo run -p lambda-rs-logging --example 03_global_init
88108
```
109+
110+
### Environment example
111+
```bash
112+
LAMBDA_LOG=debug cargo run -p lambda-rs-logging --example 01_global_macros
113+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// When building examples inside the crate, the library is available as `logging`.
2+
// If you copy this example to another crate, import with `use lambda_rs_logging as logging;`
3+
use logging;
4+
5+
fn main() {
6+
// Build a custom logger and apply env level
7+
let logger = logging::Logger::builder()
8+
.name("builder-env")
9+
.level(logging::LogLevel::INFO)
10+
.with_handler(Box::new(logging::handler::ConsoleHandler::new(
11+
"builder-env",
12+
)))
13+
.build();
14+
15+
logging::env::apply_env_level(&logger, Some("LAMBDA_LOG"));
16+
17+
logger.debug("filtered unless LAMBDA_LOG=debug".to_string());
18+
logger.info("visible at info".to_string());
19+
}

crates/lambda-rs-logging/src/handler.rs

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
33
use std::{
44
fs::OpenOptions,
5-
io::Write,
5+
io::{
6+
self,
7+
IsTerminal,
8+
Write,
9+
},
610
sync::Mutex,
711
time::SystemTime,
812
};
@@ -105,15 +109,45 @@ impl Handler for ConsoleHandler {
105109
timestamp, record.level, self.name, record.message
106110
);
107111

108-
let colored_message = match record.level {
109-
LogLevel::TRACE => format!("\x1B[37m{}\x1B[0m", log_message),
110-
LogLevel::DEBUG => format!("\x1B[35m{}\x1B[0m", log_message),
111-
LogLevel::INFO => format!("\x1B[32m{}\x1B[0m", log_message),
112-
LogLevel::WARN => format!("\x1B[33m{}\x1B[0m", log_message),
113-
LogLevel::ERROR => format!("\x1B[31;1m{}\x1B[0m", log_message),
114-
LogLevel::FATAL => format!("\x1B[31;1m{}\x1B[0m", log_message),
115-
};
116-
117-
println!("{}", colored_message);
112+
// Select output stream based on level.
113+
let warn_or_higher = matches!(
114+
record.level,
115+
LogLevel::WARN | LogLevel::ERROR | LogLevel::FATAL
116+
);
117+
if warn_or_higher {
118+
let mut e = io::stderr().lock();
119+
let use_color = io::stderr().is_terminal();
120+
if use_color {
121+
let colored = match record.level {
122+
LogLevel::TRACE => format!("\x1B[37m{}\x1B[0m", log_message),
123+
LogLevel::DEBUG => format!("\x1B[35m{}\x1B[0m", log_message),
124+
LogLevel::INFO => format!("\x1B[32m{}\x1B[0m", log_message),
125+
LogLevel::WARN => format!("\x1B[33m{}\x1B[0m", log_message),
126+
LogLevel::ERROR | LogLevel::FATAL => {
127+
format!("\x1B[31;1m{}\x1B[0m", log_message)
128+
}
129+
};
130+
let _ = writeln!(e, "{}", colored);
131+
} else {
132+
let _ = writeln!(e, "{}", log_message);
133+
}
134+
} else {
135+
let mut o = io::stdout().lock();
136+
let use_color = io::stdout().is_terminal();
137+
if use_color {
138+
let colored = match record.level {
139+
LogLevel::TRACE => format!("\x1B[37m{}\x1B[0m", log_message),
140+
LogLevel::DEBUG => format!("\x1B[35m{}\x1B[0m", log_message),
141+
LogLevel::INFO => format!("\x1B[32m{}\x1B[0m", log_message),
142+
LogLevel::WARN => format!("\x1B[33m{}\x1B[0m", log_message),
143+
LogLevel::ERROR | LogLevel::FATAL => {
144+
format!("\x1B[31;1m{}\x1B[0m", log_message)
145+
}
146+
};
147+
let _ = writeln!(o, "{}", colored);
148+
} else {
149+
let _ = writeln!(o, "{}", log_message);
150+
}
151+
}
118152
}
119153
}

crates/lambda-rs-logging/src/lib.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ impl Logger {
7171
}
7272
}
7373

74+
/// Creates a builder for configuring a `Logger`.
75+
pub fn builder() -> LoggerBuilder {
76+
LoggerBuilder::default()
77+
}
78+
7479
/// Returns the global logger (thread-safe). Initializes with a default
7580
/// console handler if not explicitly initialized via `init`.
7681
pub fn global() -> &'static Arc<Self> {
@@ -176,6 +181,93 @@ impl Logger {
176181
pub enum InitError {
177182
AlreadyInitialized,
178183
}
184+
185+
/// Logger builder for ergonomic configuration.
186+
pub struct LoggerBuilder {
187+
name: String,
188+
level: LogLevel,
189+
handlers: Vec<Box<dyn handler::Handler>>,
190+
}
191+
192+
impl Default for LoggerBuilder {
193+
fn default() -> Self {
194+
Self {
195+
name: "lambda-rs".to_string(),
196+
level: LogLevel::INFO,
197+
handlers: Vec::new(),
198+
}
199+
}
200+
}
201+
202+
impl LoggerBuilder {
203+
pub fn name(mut self, name: &str) -> Self {
204+
self.name = name.to_string();
205+
self
206+
}
207+
208+
pub fn level(mut self, level: LogLevel) -> Self {
209+
self.level = level;
210+
self
211+
}
212+
213+
pub fn with_handler(mut self, handler: Box<dyn handler::Handler>) -> Self {
214+
self.handlers.push(handler);
215+
self
216+
}
217+
218+
pub fn build(self) -> Logger {
219+
let logger = Logger::new(self.level, &self.name);
220+
for h in self.handlers {
221+
logger.add_handler(h);
222+
}
223+
logger
224+
}
225+
}
226+
227+
/// Environment configuration helpers.
228+
pub mod env {
229+
use super::{
230+
LogLevel,
231+
Logger,
232+
};
233+
234+
/// Parse a log level from a string like "trace", "debug", ...
235+
pub fn parse_level(s: &str) -> Option<LogLevel> {
236+
match s.trim().to_ascii_lowercase().as_str() {
237+
"trace" => Some(LogLevel::TRACE),
238+
"debug" => Some(LogLevel::DEBUG),
239+
"info" => Some(LogLevel::INFO),
240+
"warn" | "warning" => Some(LogLevel::WARN),
241+
"error" => Some(LogLevel::ERROR),
242+
"fatal" => Some(LogLevel::FATAL),
243+
_ => None,
244+
}
245+
}
246+
247+
/// Applies a level from the environment to the provided logger.
248+
///
249+
/// Reads the specified `var` (default: "LAMBDA_LOG"). If it parses to a level,
250+
/// updates the logger's level.
251+
pub fn apply_env_level(logger: &Logger, var: Option<&str>) {
252+
let key = var.unwrap_or("LAMBDA_LOG");
253+
if let Ok(val) = std::env::var(key) {
254+
if let Some(level) = parse_level(&val) {
255+
logger.set_level(level);
256+
}
257+
}
258+
}
259+
260+
/// Initialize a global logger with a console handler and apply env level.
261+
pub fn init_global_from_env() -> Result<(), super::InitError> {
262+
let logger = Logger::builder()
263+
.name("lambda-rs")
264+
.level(LogLevel::INFO)
265+
.with_handler(Box::new(crate::handler::ConsoleHandler::new("lambda-rs")))
266+
.build();
267+
apply_env_level(&logger, Some("LAMBDA_LOG"));
268+
super::Logger::init(logger)
269+
}
270+
}
179271
/// Returns whether the global logger would log at `level`.
180272
pub fn enabled(level: LogLevel) -> bool {
181273
Logger::global().compare_levels(level)
@@ -425,4 +517,57 @@ mod tests {
425517
// If guard fails, formatting Boom would panic.
426518
super::trace!("{}", Boom);
427519
}
520+
521+
#[test]
522+
fn builder_sets_name_level_and_handlers() {
523+
#[derive(Default)]
524+
struct Capture {
525+
out: Arc<Mutex<Vec<String>>>,
526+
}
527+
impl handler::Handler for Capture {
528+
fn log(&self, record: &Record) {
529+
self
530+
.out
531+
.lock()
532+
.unwrap()
533+
.push(format!("{}:{}", record.target, record.level as u8));
534+
}
535+
}
536+
537+
let out = Arc::new(Mutex::new(Vec::new()));
538+
let logger = Logger::builder()
539+
.name("builder-app")
540+
.level(LogLevel::WARN)
541+
.with_handler(Box::new(Capture { out: out.clone() }))
542+
.build();
543+
544+
logger.info("drop".to_string());
545+
logger.error("keep".to_string());
546+
547+
let v = out.lock().unwrap();
548+
assert_eq!(v.len(), 1);
549+
assert_eq!(v[0], "builder-app:4"); // ERROR => 4 per to_u8 mapping
550+
}
551+
552+
#[test]
553+
fn env_parse_and_apply_level() {
554+
// no panic if env missing
555+
super::env::apply_env_level(
556+
&Logger::new(LogLevel::TRACE, "tmp"),
557+
Some("__NOT_SET__"),
558+
);
559+
560+
assert_eq!(super::env::parse_level("trace"), Some(LogLevel::TRACE));
561+
assert_eq!(super::env::parse_level("DEBUG"), Some(LogLevel::DEBUG));
562+
assert_eq!(super::env::parse_level("warning"), Some(LogLevel::WARN));
563+
assert_eq!(super::env::parse_level("nope"), None);
564+
565+
// apply
566+
let logger = Logger::new(LogLevel::ERROR, "tmp");
567+
std::env::set_var("LAMBDA_LOG", "info");
568+
super::env::apply_env_level(&logger, Some("LAMBDA_LOG"));
569+
assert!(logger.compare_levels(LogLevel::INFO));
570+
// restore
571+
std::env::remove_var("LAMBDA_LOG");
572+
}
428573
}

0 commit comments

Comments
 (0)