log

repository·master·Indexed 25 days ago

https://github.com/rust-lang/log

A lightweight logging facade for Rust (version 0.4.33) that allows libraries to emit logs without coupling to a specific implementation. It provides a standardized API and macros like info!, trace!, and warn!, leaving the choice of output (stdout, file, syslog, etc.) to the end-user application. The crate also supports structured logging via the kv feature, enabling key-value pairs to be captured using traits like ToValue and integrated with serialization frameworks such as serde and sval.

Tokens
9.6K
Snippets
18
Records
45
Agent score
82%

What's inside log

  1. What is structured logging and why use it?

    master

    Traditional logging captures information as a blob of text (level, message, metadata), which is often difficult for machines to parse and for humans to read when metadata is inlined.

    Structured logging retains the original structure of data in a machine-readable format (like JSON). This allows for:

    • Efficient querying: Easily find all records with a specific correlation_id or count errors within a time range.
    • Better human readability: Presenting the human-readable message clearly while keeping ambient metadata (like service, module, or took) separate.

    Example transformation: Textual log: [INF 2018-09-27T09:32:03Z basic] [service: database, correlation: 123] Operation completed successfully in 18ms

    Structured log (JSON):

    {
        "ts": 1538040723000,
        "lvl": "INFO",
        "msg": "Operation completed successfully in 18ms",
        "module": "basic",
        "service": "database",
        "correlation": 123,
        "took": 18
    }
  2. Enable structured logging with the `kv` feature

    master

    By enabling the kv feature in the log crate, you can associate structured data (key-value pairs) with your log records. This allows you to capture data using specific traits like serde::Serialize, Debug, or Display directly within the macro call.

    Common syntax patterns:

    • target = "name": Sets the log target.
    • key:serde: Captures the value using its serde::Serialize implementation.
    • key:?: Captures the value using its Debug implementation.
    • key:%: Captures the value using its Display implementation.
    • key:err: Captures the value using its std::error::Error implementation.
    use log::{info, trace, warn};
    
    pub fn shave_the_yak(yak: &mut Yak) {
        // `yak:serde` will capture `yak` using its `serde::Serialize` impl
        // You could also use `:?` for `Debug`, or `:%` for `Display`.
        trace!(target = "yak_events", yak:serde; "Commencing yak shaving");
    
        loop {
            match find_a_razor() {
                Ok(razor) => {
                    info!(razor; "Razor located");
                    yak.shave(razor);
                    break;
                }
                Err(e) => {
                    // `e:err` will capture `e` using its `std::error::Error` impl
                    warn!(e:err; "Unable to locate a razor, retrying");
                }
            }
        }
    }
  3. How the log facade works

    master

    The log crate acts as a logging facade. It provides a single, standardized logging API that abstracts over the actual logging implementation.

    This separation of concerns allows:

    1. Libraries to emit log messages using the log API without being tied to a specific output format or destination.
    2. Executables (the consumers of those libraries) to choose and initialize a specific logging implementation (a 'logger') that suits their needs.

    Note that any log messages generated before a logger implementation is initialized in the executable will be ignored.

  4. How structured logging integrates with existing frameworks

    master

    The log crate's approach to structured logging involves bridging different serialization frameworks.

    • Framework Interoperability: The design aims to allow values captured by one framework (like sval) to be consumed by another (like serde) while retaining their underlying structure. This is achieved through internal one-to-one integrations (shims) between supported frameworks.
    • The log! Macros: In the initial implementation, existing log! macros will not change. Instead, log will rely on new macro implementations and existing structured frameworks (such as slog and tokio-trace) to capture key-value pairs.
    • Design Philosophy: Rather than grafting structured support onto old macros, the goal is to eventually use macros that are structured by design. The Value type acts as an opaque container that allows producers to plug in data using their framework of choice, and consumers to plug in Values into their framework of choice.
  5. Use log in a library

    master

    When developing a library, you should only depend on the log crate. Use its provided macros (like info!, trace!, warn!, etc.) to emit logs. This allows your library's users to decide how those logs are handled in their own applications.

    [dependencies]
    log = "0.4"
    use log::{info, trace, warn};
    
    pub fn shave_the_yak(yak: &mut Yak) {
        trace!("Commencing yak shaving");
    
        loop {
            match find_a_razor() {
                Ok(razor) => {
                    info!("Razor located: {razor}");
                    yak.shave(razor);
                    break;
                }
                Err(err) => {
                    warn!("Unable to locate a razor: {err}, retrying");
                }
            }
        }
    }
  6. How to implement the `ToValue` trait for custom types

    master

    To include a custom type in a structured log record, the type must implement the ToValue trait. This trait converts your type into a Value container that normalizes the structure for the logging pipeline.

    For Newtypes (e.g., UUIDs)

    You can implement ToValue by delegating to an underlying type that already implements it, or by using Debug formatting.

    // Option 1: Delegate to an underlying primitive
    impl ToValue for Uuid {
        fn to_value(&self) -> Value {
            self.as_u128().to_value()
        }
    }
    
    // Option 2: Use Debug implementation
    impl ToValue for Uuid {
        fn to_value(&self) -> Value {
            Value::from_debug(self)
        }
    }

    For Complex Structures (e.g., Structs)

    For complex types like structs, simply using Debug loses the field-level structure. Instead, integrate with serialization frameworks like sval or serde using the corresponding log Cargo features.

    pub trait ToValue {
        fn to_value<'v>(&'v self) -> Value<'v>;
    }
  7. Use a logger implementation in an executable

    master

    To actually see log output in an executable, you must choose a logger implementation compatible with the log facade and initialize it early in your program's runtime.

    Common logger options include:

    • Simple minimal loggers: env_logger, colog, simple_logger, simplelog, pretty_env_logger, stderrlog, flexi_logger, call_logger, std-logger, structured-logger, clang_log, ftail.
    • Complex configurable frameworks: log4rs, logforth, fern, spdlog-rs.
    • Platform/Facility adaptors: syslog, systemd-journal-logger, slog-stdlog, android_log, win_dbg_logger, db_logger, log-to-defmt, logcontrol-log.
    • WebAssembly: console_log.

    If you are building a dynamic library (cdylib), you may need to construct an FFI-safe wrapper over log to initialize it correctly.

  8. Enable `serde` or `sval` integration for structured logging

    master

    To log complex structures (like maps or structs) while retaining their field-level structure, you must enable the integration features in your Cargo.toml. This allows types to implement ToValue via serde or sval.

    Using sval integration

    Add the kv_sval feature:

    [dependencies.log]
    features = ["kv_sval"]

    Using serde integration

    Add the kv_serde feature:

    [dependencies.log]
    features = ["kv_serde"]
    [dependencies.log]
    features = ["kv_sval"]
  9. Configure `log` structured logging features

    master

    The log crate provides feature flags to enable integration with specific serialization frameworks:

    • kv_sval: Enables integration with the sval framework. Any Value will implement sval::Value, allowing consumers to see the underlying structure via sval::Streams.
    • kv_serde: Enables integration with serde. Requires std, serde, erased-serde, and sval. Any Value will implement serde::Serialize, allowing consumers to use serde::Serializers.
    [features]
    std = []
    kv_sval = ["sval"]
    kv_serde = ["std", "serde", "erased-serde", "sval"]
  10. How `Source` and `VisitSource` work together

    master

    The Source trait represents a collection of key-value pairs. Unlike a standard iterator which uses a pull-based approach, Source uses a push-based API via the VisitSource trait.

    To inspect the data within a Source, you must implement the VisitSource trait and pass a mutable reference of your implementation to the Source::visit method. This allows the source to 'push' each key-value pair to your visitor one by one.

    Common types that implement Source include:

    • Single pairs: (K, V)
    • Slices and arrays: [S], [S; N]
    • Collections: Vec<S>, HashMap<K, V>, BTreeMap<K, V>
    • Wrappers: Option<S>, Box<S>, Arc<S>, Rc<S>
    use log::kv::{self, Source, Key, Value, VisitSource};
    
    // 1. Define a visitor by implementing VisitSource
    struct Printer;
    
    impl<'kvs> VisitSource<'kvs> for Printer {
        fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), kv::Error> {
            println!("{key}: {value}");
            Ok(())
        }
    }
    
    fn main() -> Result<(), log::kv::Error> {
        // 2. Create a Source (e.g., a slice of pairs)
        let source = &[("a", 1), ("b", 2), ("c", 3)];
    
        // 3. Pass the visitor to the source to iterate
        source.visit(&mut Printer)?;
        
        Ok(())
    }
  11. Capture structured values with `Value`

    master

    The Value type is an anonymous bag used to capture structured data for logging. You can capture values using several methods:

    1. Value::from_* methods: Use specific constructors for different types like Value::from_debug, Value::from_display, or Value::from_serde (requires kv_serde feature).
    2. ToValue trait: Implement ToValue on your types to allow generic capture. This is the bound used by Source.
    3. From trait: Standard types that implement ToValue also implement From<T> for Value.

    Supported Data Types:

    • Null: Represented by Value::null(). Note that Some(Value::null()) (a logged key with an empty value) is distinct from None (a key that was never logged).
    • Strings: str, char.
    • Booleans: bool.
    • Integers: u8-u128, i8-i128, and NonZero* types.
    • Floating point: f32, f64.
    • Errors: dyn (Error + 'static) (requires kv_std feature).
    • Serialization frameworks: Any type in serde's data model (requires kv_serde) or sval's data model (requires kv_sval).
  12. Initialize a logger in an executable

    master

    Loggers are installed using set_logger. Because set_logger requires a &'static Log, it can be difficult to use with loggers that require runtime configuration. If you have the std feature enabled, you can use set_boxed_logger to pass a Box<Log> instead.

    Important: You must also call set_max_level to set the global maximum log level. By default, the level is Off, meaning no logs will be captured even if a logger is installed.