Module log::kv

source ·
Expand description

Structured logging.

Add the kv feature to your Cargo.toml to enable this module:

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

§Structured logging in log

Structured logging enhances traditional text-based log records with user-defined attributes. Structured logs can be analyzed using a variety of data processing techniques, without needing to find and parse attributes from unstructured text first.

In log, user-defined attributes are part of a Source on the log record. Each attribute is a key-value; a pair of Key and Value. Keys are strings and values are a datum of any type that can be formatted or serialized. Simple types like strings, booleans, and numbers are supported, as well as arbitrarily complex structures involving nested objects and sequences.

§Adding key-values to log records

Key-values appear before the message format in the log! macros:

info!(a = 1; "Something of interest");

Key-values support the same shorthand identifer syntax as format_args:

let a = 1;

info!(a; "Something of interest");

Values are capturing using the ToValue trait by default. To capture a value using a different trait implementation, use a modifier after its key. Here’s how the same example can capture a using its Debug implementation instead:

info!(a:? = 1; "Something of interest");

The following capturing modifiers are supported:

  • :? will capture the value using Debug.
  • :debug will capture the value using Debug.
  • :% will capture the value using Display.
  • :display will capture the value using Display.
  • :err will capture the value using std::error::Error (requires the kv_std feature).
  • :sval will capture the value using sval::Value (requires the kv_sval feature).
  • :serde will capture the value using serde::Serialize (requires the kv_serde feature).

§Working with key-values on log records

Use the Record::key_values method to access key-values.

Individual values can be pulled from the source by their key:

use log::kv::{Source, Key, Value};

// info!(a = 1; "Something of interest");

let a: Value = record.key_values().get(Key::from("a")).unwrap();
assert_eq!(1, a.to_i64().unwrap());

All key-values can also be enumerated using a VisitSource:

use std::collections::BTreeMap;

use log::kv::{self, Source, Key, Value, VisitSource};

struct Collect<'kvs>(BTreeMap<Key<'kvs>, Value<'kvs>>);

impl<'kvs> VisitSource<'kvs> for Collect<'kvs> {
    fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), kv::Error> {
        self.0.insert(key, value);

        Ok(())
    }
}

let mut visitor = Collect(BTreeMap::new());

// info!(a = 1, b = 2, c = 3; "Something of interest");

record.key_values().visit(&mut visitor)?;

let collected = visitor.0;

assert_eq!(
    vec!["a", "b", "c"],
    collected
        .keys()
        .map(|k| k.as_str())
        .collect::<Vec<_>>(),
);

Values have methods for conversions to common types:

use log::kv::{Source, Key};

// info!(a = 1; "Something of interest");

let a = record.key_values().get(Key::from("a")).unwrap();

assert_eq!(1, a.to_i64().unwrap());

Values also have their own VisitValue type. Value visitors are a lightweight API for working with primitives types:

use log::kv::{self, Source, Key, VisitValue};

struct IsNumeric(bool);

impl<'kvs> VisitValue<'kvs> for IsNumeric {
    fn visit_any(&mut self, _value: kv::Value) -> Result<(), kv::Error> {
        self.0 = false;
        Ok(())
    }

    fn visit_u64(&mut self, _value: u64) -> Result<(), kv::Error> {
        self.0 = true;
        Ok(())
    }

    fn visit_i64(&mut self, _value: i64) -> Result<(), kv::Error> {
        self.0 = true;
        Ok(())
    }

    fn visit_u128(&mut self, _value: u128) -> Result<(), kv::Error> {
        self.0 = true;
        Ok(())
    }

    fn visit_i128(&mut self, _value: i128) -> Result<(), kv::Error> {
        self.0 = true;
        Ok(())
    }

    fn visit_f64(&mut self, _value: f64) -> Result<(), kv::Error> {
        self.0 = true;
        Ok(())
    }
}

// info!(a = 1; "Something of interest");

let a = record.key_values().get(Key::from("a")).unwrap();

let mut visitor = IsNumeric(false);

a.visit(&mut visitor)?;

let is_numeric = visitor.0;

assert!(is_numeric);

To serialize a value to a format like JSON, you can also use either serde or sval:

#[derive(serde::Serialize)]
struct Data {
    a: i32, b: bool,
    c: &'static str,
}

let data = Data { a: 1, b: true, c: "Some data" };

// info!(a = data; "Something of interest");

let a = record.key_values().get(Key::from("a")).unwrap();

assert_eq!("{\"a\":1,\"b\":true,\"c\":\"Some data\"}", serde_json::to_string(&a)?);

The choice of serialization framework depends on the needs of the consumer. If you’re in a no-std environment, you can use sval. In other cases, you can use serde. Log producers and log consumers don’t need to agree on the serialization framework. A value can be captured using its serde::Serialize implementation and still be serialized through sval without losing any structure or data.

Values can also always be formatted using the standard Debug and Display traits:

struct Data {
    a: i32,
    b: bool,
    c: &'static str,
}

let data = Data { a: 1, b: true, c: "Some data" };

// info!(a = data; "Something of interest");

let a = record.key_values().get(Key::from("a")).unwrap();

assert_eq!("Data { a: 1, b: true, c: \"Some data\" }", format!("{a:?}"));

Structs§

  • An error encountered while working with structured data.
  • A key in a key-value.
  • A value in a key-value.

Traits§