Having trouble on creating Layers under tracing

I have the following code

        // configs is just a struct maintaining the logger / layer's settings
        for config in loggers.configs.iter() {
            match config.logger_type.as_str() {
                // stdout will create a layer to log at std-out simply
                "stdout" => {
                    // the fn returns a Result<Layer<Registry>, std::io::Error>; hence I simply unwrap it
                    let layer = setup_stdout_logger().unwrap();
                    // * this failed as 
                    // `subscriber` moved due to this method call, in previous iteration of loop
                    subscriber.with(layer); 
                }
                "file" => {
                    let layer = setup_file_logger().unwrap();
                    // * this doesn't work either as the return data type after with() is 
                    // different (should say incremental)
                    subscriber = subscriber.with(layer).boxed();
                }
                _ => {}
            }
        }

so I could not find a solution on this; need some help

  • issue 1 : subscriber.with(xxx) won't work as moved
  • issue 2 : re-assignment not work as well since the original subscriber is of another type (Registry) but after calling with() returning Layer<x, y> instead

if not within a loop; this somehow works as expected

let subscriber = Registry::default().with(xxx).with(yyy);

short answer is, the generic Layered<L, I, S> instantiated with different argument types are distinct types. changing the types with the builder is fine if it is statically known. if you need a runtime configurable layered subscriber, use erased type for the generic type parameter, e.g. Layered<Box<dyn Layer>, Box<dyn Layer>, S>.

see also this previous thread:

Hi~ thanks for the prompt reply; I haven't try the code mentioned in the link, but reading it seems it is using the and_then() fn; based on documentation it means making the layer additive (log the event to layer A, then layer B, then layer C....) tracing_subscriber::layer - Rust

So I am wondering if this approach has disadvantage comparing with the normal wtih()? The reason is whether the with() layers are treated separately; hence affecting the tracing performance. Each layer handled by a diff thread or every layer run in sequence by 1 single thread. Of course I might be totally wrong on the theory as I haven't dig into the source code of and_then() vs with()

they are pretty much the same.

Layer::and_then() allows you to combine all the layers into a single layer, and finally you use this combined layer on top of a subscriber.

SubscriberExt::with() combines a subscriber and a layer to get a new subscriber, which can be combined with another layer, and so on and so on.

suppose we have a subscriber S, and three layers L1, L2, L3, you can combine them in two ways:

fn combine1(s: S, l1: L1, l2: L2, l3: L3) -> Layered<L3, Layered<L2, Layered<L1, S>>> {
    s.with(l1).with(l2).with(l3)
}
fn combine2(s: S, l1: L1, l2: L2, l3: L3) -> Layered<L3, Layered<L2, Layered<L1, S>, S>, S> {
    let l123 = l1.and_then(l2).and_then(l3);
    // the following is equivalent to: l123.with_subscriber(s)
    s.with(l123)
}

note, due to some technicalities, the Layered wrapper has 3 generic type parameters [1], so the two resulted subscribers have slight different types, but should have the same functionality nevertheless.

use arithmetic, combine1 is doing the combination (analog to "multiplication") left associated, while combine2 is right associated (analog to "exponential"):

combine1 s l1 l2 l3 = ((s + l1) + l2) + l3
combine2 s l1 l2 l3 = s ^ (l3 ^ (l2 ^ l1))

in the case of runtime configurable subscribers, there's a difference in the implementation though, namely, how to do the type erasure?

if you use a chain of subscriber.with() for each layer, you must erase the subscriber type, i.e. Box<dyn Subscriber>, since each call of subscriber.with() will change the type of the subscriber.

if you use a chain of layer.and_then(), you must erase the layer type, i.e. Box<dyn Layer<S>>, where S is the "inner"-most subscriber such as fmt::Subscriber.

be becareful about the way you implement it though, if possible, when the runtime configuration space is not too big, you should tabluate them explicitly and only one level of dynamic dispatch is required (this is true no matter you erase the layer type or the subscriber type), with the trade-off of potential duplicated code.


  1. it serves dual purpose of Layer and Subscriber, depending on the wrapped "inner" type. ↩︎

sounds great... ok I have updated my code to the following, not yet tested.

    let mut layers: Option<Box<dyn Layer<Registry> + Sync + Send>> = None;

        for config in loggers.configs.iter() {
            let logger_type = config.logger_type.as_str();

            if logger_type == LOGGER_TYPE_STDOUT {
                // stdout layer
                let layer = layer::<Registry>()
                    .with_writer(std::io::stdout.with_min_level(
                        format::translate_logger_level_to_tracing_level(config.logger_level.as_str())))
                    .event_format(format::GeneralTracingEventFormatter);
                layers = Some(match layers {
                    Some(layers) => layers.and_then(layer).boxed(),
                    None => layer.boxed(),
                });
            } else if logger_type == LOGGER_TYPE_FILE {
                // normal file (non rolling and keeps growing)
                let dir = config.fields.as_ref().unwrap().get("dir").map_or("./", |v| v.trim());
                let path = config.fields.as_ref().unwrap().get("path").map_or("log.log", |v| v.trim());

                // create a json file
                let file_appender = RollingFileAppender::new(Rotation::NEVER, dir, path);
                let (non_blocking_appender, _guard) = non_blocking(file_appender);
                let layer_json = layer::<Registry>()
                    .with_writer(non_blocking_appender.with_min_level(
                        format::translate_logger_level_to_tracing_level(config.logger_level.as_str())))
                    .json();


                // create a pretty format file (human readable but less integratable)

            }
            
            

        } // end for loggers.configs

I got a question on setting the layer to log events in json format through .json(); it throws an error ...

no method named `json` found for struct `tracing_subscriber::fmt::Layer` in the current scope
method not found in `Layer<Registry, DefaultFields, Format, WithMinLevel<NonBlocking>>`

again confusing a bit on the error message.

PS. my goal here is to create 2 layers; 1 is a pretty format (human readable) and 1 is json for production level integration (integrate with elasticsearch or some other logging tools for analysis)

you need to enable the json feature of the tracing-subscriber crate.

see this earlier reply: More tracing questions - #3 by nerditation

cool~ adding the json feature works perfectly!

for the double appender (1 for pretty , 1 for json); I simply add a layer for each of them and work fine too

however I noticed that during my integration tests, the registry created seems non-replacable or resetable. Which means if I have mutliple tests, each trying to read a particular config and create layers to test the tracing; it won't work since the registry seems to be accumulative on the layers (since can't reset)

Also I might be wrong on this idea too, there were 2 file related appenders

  • rolling
  • non_blocking

I actually would create a non-rolling file appender using rolling and set rotation to NEVER; interestingly it works until I restart my tests; then the contents of the non-rolling file was truncated

after reading the docs, this seems to be an expected behavior; so my question :

  • in case I have an app using rolling file appender; and by some reason I have to restart the app; hence if rotation is daily and the restart exactly stays within a day... then the previous logged content would be gone~~~ what might be the best practice on such??
  • is it better to use non_blocking instead in case I am looking for a append mode logger?

many thx and sorry for having quite a bit of conceptual questions here

you should not install the global subscriber in tests, instead, use with_default() to run the test:

/// install global subscriber in "normal" program
fn main() {
    registry(),with(layer_xxx).init();
    info!("global subscriber installed");
}

/// use scoped subscriber for tests
#[test]
fn foo() {
    let s = registry().with(layer_xxx);
    with_default(s, || {
        info!("inside scope of subscriber");
    });
    info!("ouside scope of subscriber, you can't see this message");
}

I belive this is a race condition due to multiple file descriptors opened for the same output file, because different tests cases are running concurrently.

you can verify if this is the case by running the tests sequentially, i.e.:

$ cargo test -- --test-threads=1

note the extra --, you need to pass --test-threads to the test harness, not cargo.

if I remember correctly, this is not the case, the log file is opened in append mode. the truncation you saw in the tests should not be inherent in the appender implementation, I believe it is just the consequence of the race condition.

non_blocking is not about appending vs truncating, it solves another problem: don't block the thread when it records a tracing event, this is particularly important for highly concurrent async code, such as network servers.