tokio-rs / tracing

Application level tracing for Rust.
https://tracing.rs
MIT License
5.19k stars 677 forks source link

[Tracing][Subscriber] Extend root fields with (static) custom fields #2887

Open gheorghitamutu opened 5 months ago

gheorghitamutu commented 5 months ago

Feature Request

Crates

tracing-subscriber

Motivation

Custom root fields for a log entry (in formatter).

Proposal

At the moment we can customize only fields that are in a span (not the static metadata structure). I'd like to globally set several fields at layer/subscriber initialization and they would be used through the entire logging process for that application.

I know that we can use an option called flatten_event and set it to true to move all the fields for an event at the root level but this requires that every event (along with info!, error!, etc macros) to contain said fields which I don't find it as a reasonable approach (a lot of code repetition and the fields can be easily forgotten to be added in each macro).

I found out that several options were requsted/described in these issues: https://github.com/tokio-rs/tracing/issues/1531 https://github.com/tokio-rs/tracing/issues/2670

I'd like to add this feature in a Subscriber because, for my case it was JSON, the formatter is initialized as it follows:

In https://github.com/tokio-rs/tracing/blob/master/tracing-subscriber/src/fmt/fmt_subscriber.rs#L581

    pub fn json(self) -> Subscriber<C, format::JsonFields, format::Format<format::Json, T>, W> {
        Subscriber {
            fmt_event: self.fmt_event.json(),
            fmt_fields: format::JsonFields::new(),
            fmt_span: self.fmt_span,
            make_writer: self.make_writer,
            // always disable ANSI escapes in JSON mode!
            is_ansi: false,
            log_internal_errors: self.log_internal_errors,
            _inner: self._inner,
        }
    }

With self.fmt_event.json() implemented in https://github.com/tokio-rs/tracing/blob/master/tracing-subscriber/src/fmt/format/mod.rs#L685 as:

    pub fn json(self) -> Format<Json, T> {
        Format {
            format: Json::default(),
            timer: self.timer,
            ansi: self.ansi,
            display_target: self.display_target,
            display_timestamp: self.display_timestamp,
            display_level: self.display_level,
            display_thread_id: self.display_thread_id,
            display_thread_name: self.display_thread_name,
            display_filename: self.display_filename,
            display_line_number: self.display_line_number,
        }
    }

What I'm thinking of is that we can add a function to json (or format at general) that can say:

     let file = File::create("myapp.log")?;
     let subscriber = tracing_subscriber::fmt::subscriber()
         .with_thread_names(true)
         .with_target(true)
         .with_root_fields(vec![("a", "value1"), ("b", "value2")])
         .json()
         .with_writer(file)

Or any type of structure passed to function with_root_fields and each custom formatter (json, pretty, compact) can treat them as it does with filename, target, etc:

            if self.display_filename {
                if let Some(filename) = meta.file() {
                    serializer.serialize_entry("filename", filename)?;
                }
            }
.................
            // pseudocode
            if self.custom_root_fields_container_option { // optional, you can just not have elements in container
                for (k,v) in meta.custom_root_fields_container() {
                    serializer.serialize_entry(k, v)?;
                }
            }

If you think this works as a rough proposal I can try and code this and make a PR for a review (also including the tests required).

I could also try coding other alternatives that you might propose to this issue.

Please let me know what you think. Thank you!