Tracing domain-specific objects

I'm trying to integrate the tracing crate into a project of mine, but I want to trace domain-specific objects. So I'm slapped with an error that tracing::Value is not implemented for my type.
I'd try to manually implement the trait, but it's sealed.

So my question is:
How can I trace my domain-specific objects?

Seems the unstable valuable, i.e. valuable - Rust crate, is what you'll be interested in.

Yeah I was afraid of that.
I've been looking at it, but its design doesn't make it easy to follow what's going on
and what needs to happen.

In addition it really feels like all I wanted was a sandwich, but in order to get it now I also have to get the kitchen sink, the kitchen, and the rest of the house :confused:

1 Like

tracing ’s Value trait is intentionally minimalist: it supports only a small number of Rust primitives as typed values, and only permits recording user-defined types with their fmt::Debug or fmt::Display implementations.

So will the debug or display implementation be enough? Like Rust Playground [1]


  1. Sorry, I just realized Arguments in std::fmt - Rust is a precompiled struct. ↩︎

Hmm somehow I missed that limitation, thanks for pointing it out.

For my project it means I'll go with serde instead and pay the allocation overhead for serialization.

are you using custom Subscribers or the FmtSubscriber from the tracing-subscriber crate?

with FmtSubscriber, you just need to derive valuable::Valuable for your type, and enable the unstable feature for both the tracing and tracing-subscriber crates, the json visitor will serialize your type automatically.

just remember to add the "--cfg tracing_unstable" compiler flag, and call as_value() when you record the object in your span or event.

#[derive(Valuable)]
struct MyObject {
    id: u32,
    name: String,
}
let _ = tracing::info_span!("important span", object = my_object.as_value());

if you are using custom Subscriber, you also need to implement the record_value method, in addition to your typical Subscriber functionality.

Thank you for the suggestion!
Ideally I'd keep all options open.

However, I also wasn't kidding with my kitchen sink comment: what I really want is a better view of the program state, and what I would end up doing with tracing is implement a bunch of infrastructure (i.e. use valuable) to support using yet another piece of infrastructure (i.e. tracing) just to make that happen, not to mention that I'd then have to figure out how all the pieces fit together.

On top of that, my state hierarchy is non-trivial in size, and a portion of the types are generic in both types and lifetimes. That means that I likely wouldn't be able to use the valuable::Valuable macro for those types¹, so I'd have to write manual impls for them.

Using serde might be less performant, but it's much simpler.

¹Im not saying it can't be done in principle, but I've never seen a derive macro satisfactorily handle arbitrary generic types as of yet. Seems tricky to accomplish without access to compiler internals, too.

1 Like

yeah, that's annoying, but that's somewhat to be expected: rust doesn't have builtin runtime reflection by design, (think java java.lang.reflect, C# System.Reflection, etc), libraries have to pick some mechanism to do runtime inspection if they want to support arbitrary user defined types, and being generic isn't always feasible.

in the case of tracing, the trade off (between usability and flexibility) is, in most common cases, you can record your custom type with the string representation through Display or Debug; but you still have the option to control how the inspection should behave through Valuable if the former doesn't suit your need.

personally, I think this design is good enough, as I barely need to record very complicated states into traces, and Valuable isn't too hard to incorporate into my own code. altough the valuable feature is explicitly stated to be unstable API, I don't see it will be ditched very soon, after all, they need to support some form of runtime inspection anyway. yeah, visitor pattern is OO, and I don't like it, but that's what we have today.

serde is a perfectly valid option if Debug (or Display) is not suitable. and personally I would not worry about the performance in such cases.

for one thing, the tracing crate employs many tricks to keep the overhead as low as possible, for instance, each span!() or event!() invocation is conditional executed, guarded by looking up through a static ·Callsite object, and the fast path, when the callsite is disabled, is just a relaxed atomic load followed by a single branch instruction;

on the other hand, I barely instrument performance critical code paths (inside tight loops), and use mostly TRACE level if I have to, since I like to disable TRACE level in release builds using "release_max_level_debug" feature flag.

1 Like

That's a good way to keep overhead low, but I've got it beat: when I'm not using my custom tracing system, the overhead is 0. The price of that is that it's controlled by a feature flag (i.e. it's not runtime-switchable) but given my use cases that's ok.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.