rustrust-tokiorust-tracing

Issues with Box<dyn> and tokio tracing subscriber


I am working with the tokio tracing/tracing_subscriber crates building an internal toolkit and have been running into issues trying to add an option to pass a custom formatter to the layer.

Using the trait bounds

S: Subscriber + for<`a> LookupSpan<`a>,
T: FormatEvent<S, JsonFields> + Send + Sync + `static,

I think I have satisfied the requirements for a custom json formatter however the function return type of

Box<dyn Layer<S> + Send + Sync + `static,

Which compiled fine when returning either the standard Json format or my own custom Json format now is raising a compiler error for unknown size at compile time.

error[E0277]: the size for values of type `dyn __tracing_subscriber_Layer<S> + std::marker::Send + Sync` cannot be know at compilation time

My best guess is that this has become an issue because the size can be known at if the passed type is known at compile time ie my custom struct? Whereas this is not the case with a trait bound genric type?

At this point more interested in the reason why than fixing it.

Heres the relevant code:

pub fn init_subscriber<S, T>(
    exporter_endpoint: &str,
    resource_metrics: Resource,
    tracer_name: &str,
    custom_fmt: Option<T>
) -> Result<(), axum::BoxError> 
where
    S: Subscriber + for<'a> LookupSpan<'a>,
    T: FormatEvent<S, JsonFields> + Send + Sync + `static,
{
    let registery = tracing_subscriber::registry()
        .with(build_otlp_layer(
            exporter_endpoint,
            resource_metrics,
            tracer_name
        )?)
        .with(build_loglevel_filter_layer())
        .with(build_logger_text(custom_fmt));
    registery.init();
    info!("started otlp logging & tracing");
    Ok(())
}

pub fn build_logger_text<S, T>(custom_fmt: Option<T>) -> Box<dyn Layer<S> + Send + Sync + 'static>
where
    S: Subscriber + for<'a> LookupSpan<'a>,
    T: FormatEvent<S, JsonFields> + Send + Sync + `static,
{
    match custom_fmt {
        Some(fmt) => {
            tracing_subscriber::fmt::layer()
                .json()
                .with_current_span(true)
                .event_format(fmt)
                .boxed()
        }
        None => {
            tracing_subscriber::fmt::layer()
                .json()
                .with_current_span(true)
                .event_format(TraceIdFormat)
                .boxed()
        }
    }
}

Solution

  • The issue stems from custom_fmt which only works for an unknown type S, but you're asking it to work with a specific subscriber type built by registry().with(...).with(...). The complaint from the compiler about the size of dyn Layer is a red-herring.

    The solutions that come to mind are a bit grizzly IMO. You basically need a higher-kinded type to express that your custom_fmt works for any S (with constraints), but the only way to get that would be via another trait:

    pub trait FormatEventMaker {
        fn make<S: Subscriber + for<'a> LookupSpan<'a>>(
            self,
        ) -> impl FormatEvent<S, JsonFields> + Send + Sync + 'static;
    }
    
    pub fn init_subscriber<T>(
        exporter_endpoint: &str,
        resource_metrics: Resource,
        tracer_name: &str,
        custom_fmt: Option<T>,
    ) -> Result<(), Box<dyn std::error::Error + Sync + Send + 'static>>
    where
        T: FormatEventMaker + Send + Sync + 'static,
    {
        let registery = tracing_subscriber::registry()
            .with(build_otlp_layer(
                exporter_endpoint,
                resource_metrics,
                tracer_name,
            )?)
            .with(build_loglevel_filter_layer())
            .with(build_logger_text(custom_fmt));
        registery.init();
        info!("started otlp logging & tracing");
        Ok(())
    }
    
    pub fn build_logger_text<S, T>(custom_fmt: Option<T>) -> Box<dyn Layer<S> + Send + Sync + 'static>
    where
        S: Subscriber + for<'a> LookupSpan<'a>,
        T: FormatEventMaker + Send + Sync + 'static,
    {
        match custom_fmt {
            Some(fmt) => tracing_subscriber::fmt::layer()
                .json()
                .with_current_span(true)
                .event_format(fmt.make())
                .boxed(),
            None => tracing_subscriber::fmt::layer()
                .json()
                .with_current_span(true)
                .event_format(TraceIdFormat)
                .boxed(),
        }
    }
    

    This removes S entirely from the init_subscriber function since the subscriber type is only known within the function. You would just need a wrapper around your event formatter.


    If you do happen to know what S will be, then you can just substitute S with it when defining init_subscriber. You didn't provide definitions, but its something akin to this (if namable at all):

    Layered<Box<dyn Layer<Layered<Box<dyn Layer<Registry> + Send + Sync>, Registry>> + Send + Sync>, Layered<Box<dyn Layer<Registry> + Send + Sync>, Registry>, Layered<Box<dyn Layer<Registry> + Send + Sync>, Registry>>
    

    Save us TAIT.


    You may also get away with type erasure - i.e. convert the built registry into a Box<dyn Subscriber> and use that instead of S, but I'm not 100% if that would work with LookupSpan in the mix.