Skip to main content

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}