Skip to content

Commit

Permalink
subscriber, serde: add valuable support to the JSON formatter (toki…
Browse files Browse the repository at this point in the history
…o-rs#1862)

This branch introduces support for `valuable` in `tracing-subscriber`'s
JSON formatter, and in `tracing-serde`, using the `valuable-serde`
bridge.

This allows the `fmt::Json` subscriber to actually record `valuable`
values as structured JSON. Here's an example, where a field is first
recorded using `fmt::Debug`, and then again using `field::valuable`:

```
:; RUSTFLAGS="--cfg tracing_unstable" cargo run --example valuable_json | jq
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/examples/valuable_json`
{
  "timestamp": "2022-01-25T21:36:30.729658Z",
  "level": "INFO",
  "fields": {
    "valuable": false,
    "user": "User { name: \"Arwen Undomiel\", age: 3000, address: Address { country: \"Middle Earth\", city: \"Rivendell\", street: \"leafy lane\" } }"
  },
  "target": "valuable_json"
}
{
  "timestamp": "2022-01-25T21:36:30.729720Z",
  "level": "INFO",
  "fields": {
    "valuable": true,
    "user": {
      "name": "Arwen Undomiel",
      "age": 3000,
      "address": {
        "country": "Middle Earth",
        "city": "Rivendell",
        "street": "leafy lane"
      }
    }
  },
  "target": "valuable_json"
}
```

As a side note, this branch also nicely validates that recording `Valuable`
values from a subscriber is actually possible, which the previous valuable PR
didn't have an example of. 

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
  • Loading branch information
hawkw authored and kaffarell committed May 22, 2024
1 parent d8a4e18 commit f426714
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 0 deletions.
63 changes: 63 additions & 0 deletions examples/examples/valuable_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#![allow(dead_code)]
//! This example shows how a field value may be recorded using the `valuable`
//! crate (https://crates.io/crates/valuable).
//!
//! `valuable` provides a lightweight but flexible way to record structured data, allowing
//! visitors to extract individual fields or elements of structs, maps, arrays, and other
//! nested structures.
//!
//! `tracing`'s support for `valuable` is currently feature flagged. Additionally, `valuable`
//! support is considered an *unstable feature*: in order to use `valuable` with `tracing`,
//! the project must be built with `RUSTFLAGS="--cfg tracing_unstable`.
//!
//! Therefore, when `valuable` support is not enabled, this example falls back to using
//! `fmt::Debug` to record fields that implement `valuable::Valuable`.
#[cfg(tracing_unstable)]
use tracing::field::valuable;
use valuable::Valuable;

#[derive(Clone, Debug, Valuable)]
struct User {
name: String,
age: u32,
address: Address,
}

#[derive(Clone, Debug, Valuable)]
struct Address {
country: String,
city: String,
street: String,
}

fn main() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE)
.json()
.init();

let user = User {
name: "Arwen Undomiel".to_string(),
age: 3000,
address: Address {
country: "Middle Earth".to_string(),
city: "Rivendell".to_string(),
street: "leafy lane".to_string(),
},
};

// for comparison, record `user` without using its `Valuable`
// implementation:
tracing::info!(valuable = false, user = ?user);

// If the `valuable` feature is enabled, record `user` using its'
// `valuable::Valuable` implementation:
#[cfg(tracing_unstable)]
tracing::info!(valuable = true, user = valuable(&user));

#[cfg(not(tracing_unstable))]
tracing::warn!(
"note: this example was run without `valuable` support enabled!\n\
rerun with `RUSTFLAGS=\"--cfg tracing_unstable\" to enable `valuable`",
);
}
7 changes: 7 additions & 0 deletions tracing-serde/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ rust-version = "1.63.0"
default = ["std"]
std = ["serde/std", "tracing-core/std"]

[features]
valuable = ["valuable_crate", "valuable-serde", "tracing-core/valuable"]

[dependencies]
serde = { version = "1.0.139", default-features = false, features = ["alloc"] }
tracing-core = { path = "../tracing-core", version = "0.2", default-features = false }

[dev-dependencies]
serde_json = "1.0.82"

[target.'cfg(tracing_unstable)'.dependencies]
valuable_crate = { package = "valuable", version = "0.1.0", optional = true, default_features = false }
valuable-serde = { version = "0.1.0", optional = true, default_features = false }

[badges]
maintenance = { status = "experimental" }
18 changes: 18 additions & 0 deletions tracing-serde/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,15 @@ impl<S> Visit for SerdeMapVisitor<S>
where
S: SerializeMap,
{
#[cfg(all(tracing_unstable, feature = "valuable"))]
fn record_value(&mut self, field: &Field, value: valuable_crate::Value<'_>) {
if self.state.is_ok() {
self.state = self
.serializer
.serialize_entry(field.name(), &valuable_serde::Serializable::new(value));
}
}

fn record_bool(&mut self, field: &Field, value: bool) {
// If previous fields serialized successfully, continue serializing,
// otherwise, short-circuit and do nothing.
Expand Down Expand Up @@ -426,6 +435,15 @@ impl<S> Visit for SerdeStructVisitor<S>
where
S: SerializeStruct,
{
#[cfg(all(tracing_unstable, feature = "valuable"))]
fn record_value(&mut self, field: &Field, value: valuable_crate::Value<'_>) {
if self.state.is_ok() {
self.state = self
.serializer
.serialize_field(field.name(), &valuable_serde::Serializable::new(value));
}
}

fn record_bool(&mut self, field: &Field, value: bool) {
// If previous fields serialized successfully, continue serializing,
// otherwise, short-circuit and do nothing.
Expand Down
5 changes: 5 additions & 0 deletions tracing-subscriber/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fmt = ["registry", "std"]
ansi = ["fmt", "nu-ansi-term"]
registry = ["sharded-slab", "thread_local", "std"]
json = ["tracing-serde", "serde", "serde_json"]
valuable = ["tracing-core/valuable", "valuable_crate", "valuable-serde", "tracing-serde/valuable"]
# Enables support for local time when using the `time` crate timestamp
# formatters.
local-time = ["time/local-offset"]
Expand Down Expand Up @@ -68,6 +69,10 @@ chrono = { version = "0.4.26", default-features = false, features = ["clock", "s
sharded-slab = { version = "0.1.4", optional = true }
thread_local = { version = "1.1.4", optional = true }

[target.'cfg(tracing_unstable)'.dependencies]
valuable_crate = { package = "valuable", version = "0.1.0", optional = true, default_features = false }
valuable-serde = { version = "0.1.0", optional = true, default_features = false }

[dev-dependencies]
tracing = { path = "../tracing", version = "0.2" }
tracing-mock = { path = "../tracing-mock", features = ["tracing-subscriber"] }
Expand Down
20 changes: 20 additions & 0 deletions tracing-subscriber/src/fmt/format/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,26 @@ impl<'a> crate::field::VisitOutput<fmt::Result> for JsonVisitor<'a> {
}

impl<'a> field::Visit for JsonVisitor<'a> {
#[cfg(all(tracing_unstable, feature = "valuable"))]
fn record_value(&mut self, field: &Field, value: valuable_crate::Value<'_>) {
let value = match serde_json::to_value(valuable_serde::Serializable::new(value)) {
Ok(value) => value,
Err(_e) => {
#[cfg(debug_assertions)]
unreachable!(
"`valuable::Valuable` implementations should always serialize \
successfully, but an error occurred: {}",
_e,
);

#[cfg(not(debug_assertions))]
return;
}
};

self.values.insert(field.name(), value);
}

/// Visit a double precision floating point value.
fn record_f64(&mut self, field: &Field, value: f64) {
self.values
Expand Down

0 comments on commit f426714

Please sign in to comment.