witchcraft_env_logger/lib.rs
1// Copyright 2025 Palantir Technologies, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! A simple Witchcraft logger that can be configured with an environment variable.
15//!
16//! This is similar to the [env_logger](https://docs.rs/env_logger) crate, but using the [`witchcraft_log`] crate
17//! instead of the `log` crate. Configuration of logging levels is the same as `env_logger` except for the additional
18//! `fatal` log level. Regex filters are not supported.
19//!
20//! Logs are written to standard error in the standard Witchcraft `service.1` JSON format.
21//!
22//! # Example
23//!
24//! ```
25//! use witchcraft_log::{debug, error, info, Level};
26//!
27//! witchcraft_env_logger::init();
28//!
29//! debug!("this is a debug message");
30//! error!("this is printed by default");
31//!
32//! if witchcraft_log::enabled!(Level::Info) {
33//! let x = 3 * 4; // expensive computation
34//! info!("figured out the answer", safe: { answer: x });
35//! }
36//! ```
37//!
38//! ```not_rust
39//! $ RUST_LOG=error ./main
40//! {"type":"service.1","level":"ERROR","time":"2025-05-26T16:45:59.204677531Z","origin":"main","thread":"main","message":"this is printed by default","safe":true,"params":{"file":"witchcraft-env-logger/examples/main.rs","line":7}}
41//! ```
42//!
43//! ```not_rust
44//! $ RUST_LOG=info ./main
45//! {"type":"service.1","level":"ERROR","time":"2025-05-26T16:46:31.043928664Z","origin":"main","thread":"main","message":"this is printed by default","safe":true,"params":{"file":"witchcraft-env-logger/examples/main.rs","line":7}}
46//! {"type":"service.1","level":"INFO","time":"2025-05-26T16:46:31.043976765Z","origin":"main","thread":"main","message":"figured out the answer","safe":true,"params":{"answer":12,"file":"witchcraft-env-logger/examples/main.rs","line":11}}
47//! ```
48//!
49//! ```not_rust
50//! $ RUST_LOG=main=debug ./main
51//! {"type":"service.1","level":"DEBUG","time":"2025-05-26T16:47:04.831643644Z","origin":"main","thread":"main","message":"this is a debug message","safe":true,"params":{"file":"witchcraft-env-logger/examples/main.rs","line":6}}
52//! {"type":"service.1","level":"ERROR","time":"2025-05-26T16:47:04.831691314Z","origin":"main","thread":"main","message":"this is printed by default","safe":true,"params":{"file":"witchcraft-env-logger/examples/main.rs","line":7}}
53//! {"type":"service.1","level":"INFO","time":"2025-05-26T16:47:04.831720469Z","origin":"main","thread":"main","message":"figured out the answer","safe":true,"params":{"answer":12,"file":"witchcraft-env-logger/examples/main.rs","line":11}}
54//! ```
55#![warn(missing_docs)]
56
57use std::{
58 env,
59 io::{self, Write},
60};
61
62use conjure_serde::json;
63use witchcraft_log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
64use witchcraft_log_util::{filter::Filter, service};
65
66struct Logger {
67 filter: Filter,
68}
69
70impl Log for Logger {
71 fn enabled(&self, metadata: &Metadata<'_>) -> bool {
72 self.filter.enabled(metadata)
73 }
74
75 fn log(&self, record: &Record<'_>) {
76 if !self.enabled(record.metadata()) {
77 return;
78 }
79
80 let service_log = service::from_record(record);
81 let mut buf = json::to_string(&service_log).unwrap();
82 buf.push('\n');
83 // Using the macro so output is intercepted in tests properly
84 eprint!("{buf}");
85 }
86
87 fn flush(&self) {
88 let _ = io::stderr().flush();
89 }
90}
91
92/// Initializes the global logger, reading configuration from the `RUST_LOG` environment variable.
93///
94/// Returns an error if the logger is already initialized.
95pub fn try_init() -> Result<(), SetLoggerError> {
96 let mut builder = Filter::builder();
97
98 if let Ok(rust_log) = env::var("RUST_LOG") {
99 for directive in rust_log.split(",") {
100 let mut it = directive.splitn(2, "=");
101 let first = it.next().unwrap();
102 let second = it.next();
103
104 match second {
105 Some(level) => {
106 if let Ok(level) = level.parse::<LevelFilter>() {
107 builder = builder.target_level(first, level);
108 };
109 }
110 None => match first.parse::<LevelFilter>() {
111 Ok(level) => builder = builder.level(level),
112 Err(_) => builder = builder.target_level(first, LevelFilter::Trace),
113 },
114 }
115 }
116 }
117
118 let filter = builder.build();
119 let max_level = filter.max_level();
120
121 witchcraft_log::set_boxed_logger(Box::new(Logger { filter }))?;
122 witchcraft_log::set_max_level(max_level);
123
124 Ok(())
125}
126
127/// Like [`try_init()`], but panics if the logger is already initialized.
128pub fn init() {
129 try_init().unwrap();
130}