How can I init tracing-registry dynamically with multiple outputs?

I want to initialize a tracing registry with multiple outputs. But the outputs is dynamical for different environments.
I can't using chain init like this Registry::default().with().with().init().
When I init registry via for-loop, the registry is moved.
I don't know how to impl it. Can anyone help me?
The code is in the following.

use std::str::FromStr;

use serde_json::json;
use tracing::Level;
use tracing_appender::{non_blocking, rolling};
use tracing_subscriber::{
    fmt::{layer, writer::MakeWriterExt},
    layer::SubscriberExt,
    util::SubscriberInitExt,
    Registry,
};

fn main() {
    // How can I init registry dynamically with multiple outputs?
    let log_file_name = "hello";
    let registry = Registry::default();
    // the log config
    // there may be multiple outputs with different log level for different env
    // the config will come from outer in the future, and I don’t know the length now 
    let cfgs = json!([
        { "level": "debug", "output": "stdout" },   // output to `stdout` with level debug
        { "level": "error", "output": "stderr" },   // output to `stderr` with level error
        { "level": "info", "output": "log" }        // output to `log` dir with level info
    ]);
    let cfgs = cfgs.as_array().unwrap();
    for cfg in cfgs {
        let output = cfg.get("output").unwrap().as_str().unwrap();
        let level = cfg.get("level").unwrap().as_str().unwrap();
        let level = Level::from_str(level).unwrap();
        if output == "stdout" { // to stdout
            let stdout_layer = layer().with_writer(std::io::stdout.with_max_level(level));
            // !!! registry moved
            registry.with(stdout_layer)
        } else if output == "stderr" {  // to stderr
            let stderr_layer = layer().with_writer(std::io::stderr.with_max_level(level));
            // !!! registry moved
            registry.with(stderr_layer)
        } else {    // otherwise, to a dir
            let file_appender = rolling::daily(output, format!("{log_file_name}.log"));
            let (non_blocking_appender, _guard) = non_blocking(file_appender);
            let file_layer = layer().json().with_writer(non_blocking_appender.with_max_level(level));
            // !!! registry moved
            registry.with(file_layer)
        }
    }

    registry.init();

    tracing::debug!("hello debug");
    tracing::info!("hello info");
    tracing::warn!("hello warn");
    tracing::error!("hello error");
    tracing::info!(a_bool = true, answer = 42, message = "hello tracing");
}

What do you mean by environment exactly? Like running your program in different environments like production, staging, local, debug, et al.?

Yes.
It means in diffenent envs, the log config may be different.

What are your cfgs then? Is there one entry for each environment? Or is each environment passing different values as cfgs?

let cfgs = json!([
        { "level": "debug", "output": "stdout" },   // output to `stdout` with level debug
        { "level": "error", "output": "stderr" },   // output to `stderr` with level error
        { "level": "info", "output": "log" }        // output to `log` dir with level info
    ]);

For another env, the cfgs may be the following after parsed.

let cfgs = json!([
        { "level": "info", "output": "stdout" },   // output to `stdout` with level info
        { "level": "info", "output": "/app/log" }        // output to `/app/log` dir with level info
    ]);

to reduce overhead, the Layered structs are strongly typed, so the SubscriberExt::with() method consumes self (impl Subscriber) and returns Layered<L, Self, Self> (another impl Subscriber but different type).

to do what you described, you must erase the type of all the layers, then you can combine those layers together into a Layered<Box<dyn Layer>, Box<dyn Layer>, Registry>, and add the combined layer to the registry.

see Layer::boxed() and Layer::and_then():

1 Like

Thanks for your answer.
I tried the solution, and the code is compiled ok.
But something strange occurred, the file output does not write anything but only create a log file.
The code is in the following.

use std::str::FromStr;

use serde_json::json;
use tracing::Level;
use tracing_appender::{non_blocking, rolling};
use tracing_subscriber::{
    fmt::{layer, writer::MakeWriterExt},
    layer::SubscriberExt,
    util::SubscriberInitExt,
    Layer, Registry,
};

fn main() {
    // I create layer via `Layer::boxed()` and `Layer::and_then()`.
    // The code following compiled ok.
    // However, as the following example, threre are three outputs, the `stdout` and `stderr` is ok while the `file` output does't wirte the log but only create a file `log/hello.llog`
    let log_file_name = "hello";
    let registry = Registry::default();
    // the log config
    // there may be multiple outputs with different log level for different env
    let cfgs = json!([
        { "level": "debug", "output": "stdout" },   // output to `stdout` with level debug
        { "level": "warn", "output": "stderr" },    // output to `stderr` with level error
        { "level": "debug", "output": "log" }       // output to `log` dir with level info
    ]);
    let cfgs = cfgs.as_array().expect("log config should be valid");
    let mut layers: Option<Box<dyn Layer<Registry> + Sync + Send>> = None;
    for cfg in cfgs {
        let output = cfg.get("output").unwrap().as_str().unwrap();
        let level = cfg.get("level").unwrap().as_str().unwrap();
        let level = Level::from_str(level).unwrap();
        if output == "stdout" {
            // to stdout
            let stdout_layer = layer()
                .with_writer(std::io::stdout.with_max_level(level))
                .with_thread_names(true)
                .with_line_number(true);
            layers = Some(match layers {
                Some(layers) => layers.and_then(stdout_layer).boxed(),
                None => stdout_layer.boxed(),
            });
        } else if output == "stderr" {
            // to stderr
            let stderr_layer = layer().with_writer(std::io::stderr.with_max_level(level));
            layers = Some(match layers {
                Some(layers) => layers.and_then(stderr_layer).boxed(),
                None => stderr_layer.boxed(),
            });
        } else {
            // otherwise, to a dir
            // FIXME!!! NOT WRITE ANYTHING
            let file_appender = rolling::never(output, format!("{log_file_name}.log"));
            let (non_blocking_appender, _guard) = non_blocking(file_appender);
            let file_layer = layer()
                .json()
                .with_writer(non_blocking_appender.with_max_level(level))
                .with_thread_names(true)
                .with_line_number(true);
            layers = Some(match layers {
                Some(layers) => layers.and_then(file_layer).boxed(),
                None => file_layer.boxed(),
            });
        }
    }

    registry.with(layers.expect("log layers shoud be ok")).init();

    tracing::debug!("hello debug");
    tracing::info!("hello info");
    tracing::warn!("hello warn");
    tracing::error!("hello error");
    tracing::info!(a_bool = true, answer = 42, message = "hello tracing");
}

you are dropping the worker guard too early, the worker thread wasn't flushing the file when the program exit. store the guard in a bigger scope. e.g. use an Option or Vec, depending how many potential worker guards you might have at runtime.

let mut guards = vec![];
for cfg in cfgs {
    //...
    let (non_blocking_appender, guard) = non_blocking(file_appender);
    guards.push(guard);
    //...
}

Thanks! :+1: :+1: :+1:
Everything goes ok now.

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.