tracing_ecs/
lib.rs

1//! Tracing subscriber that outputs json log lines compatible with ECS
2//! ([Elastic Common Schema](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html)).
3//!
4//! More specifically, this crate provides a [`Layer`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/trait.Layer.html)
5//! implementation that can be composed with an existing `Subscriber` from the
6//! `tracing-subscribers` crate.
7//!
8//! See how is implemented the [`install`](struct.ECSLayer.html#method.install) method
9//! to understand what's done under the hood.
10//!
11//! # How spans are handled
12//!
13//! All spans attributes are directly appended to the final json object.
14//!
15//! As a result, there might be duplicate keys in the resulting json if
16//! static extra fields have the same keys as some span attributes, or if
17//! an attribute is named `message` (which shall be reserved to the logged event).
18//!
19//! This behavior can be customized by implementing the [`AttributeMapper`](trait.AttributeMapper.html) trait.
20//!
21//! # JSON Normalization
22//!
23//! Output is normalized by default so there is no dot anymore in the resulting json keys. See
24//! <https://www.elastic.co/guide/en/ecs/current/ecs-guidelines.html>
25//!
26//! See [ECSLayerBuilder.normalize_json](struct.ECSLayerBuilder.html#method.normalize_json)
27//!
28//! # Examples
29//!
30//! Install a default subscriber that outputs json to stdout:
31//!
32//! ```rust
33//! use tracing_ecs::ECSLayerBuilder;
34//!
35//! ECSLayerBuilder::default()
36//!     .stdout()
37//!     .install()
38//!     .unwrap()
39//! ```
40//!
41//! Install a subscriber with custom extra fields that outputs
42//! json to stdout (here we use the `json!` macro but it accepts
43//! anything that serializes to a json map):
44//!
45//! ```rust
46//! use serde_json::json;
47//! use tracing_ecs::ECSLayerBuilder;
48//!
49//! ECSLayerBuilder::default()
50//!     .with_extra_fields(json!({
51//!         "labels": {
52//!             "env": "prod",
53//!         },
54//!         "tags": ["service", "foobar"]
55//!     }))
56//!     .unwrap()
57//!     .stdout()
58//!     .install()
59//!     .unwrap();
60//! ```
61//!
62//! With attributes name mapping:
63//!
64//! ```rust
65//! use tracing_ecs::ECSLayerBuilder;
66//! use std::borrow::Cow;
67//! use std::ops::Deref;
68//!
69//! ECSLayerBuilder::default()
70//!  .with_attribute_mapper(
71//!     |_span_name: &str, name: Cow<'static, str>| match name.deref() {
72//!         "txid" => "transaction.id".into(),
73//!         _ => name,
74//!     },
75//!  ).stdout().install().unwrap()
76//! ```
77use chrono::Utc;
78use ser::ECSLogLine;
79use ser::ECSSpanEvent;
80use ser::LogFile;
81use ser::LogOrigin;
82use serde::Serialize;
83use serde_json::Map;
84use serde_json::Value;
85use std::borrow::Cow;
86use std::collections::HashMap;
87use std::io;
88use std::io::sink;
89use std::io::Stderr;
90use std::io::Stdout;
91use std::io::Write;
92use std::sync::Mutex;
93use tracing_core::dispatcher::SetGlobalDefaultError;
94use tracing_core::span::Attributes;
95use tracing_core::span::Id;
96use tracing_core::span::Record;
97use tracing_core::Event;
98use tracing_core::Subscriber;
99use tracing_log::log_tracer::SetLoggerError;
100use tracing_log::LogTracer;
101use tracing_subscriber::fmt::format::FmtSpan;
102use tracing_subscriber::fmt::MakeWriter;
103use tracing_subscriber::fmt::SubscriberBuilder;
104use tracing_subscriber::layer::Context;
105use tracing_subscriber::registry::LookupSpan;
106use tracing_subscriber::EnvFilter;
107use tracing_subscriber::Layer;
108
109mod attribute_mapper;
110mod ser;
111mod visitor;
112
113pub use attribute_mapper::{AttributeMapper, EVENT_SPAN_NAME};
114
115/// The final Layer object to be used in a `tracing-subscriber` layered subscriber.
116///
117pub struct ECSLayer<W>
118where
119    W: for<'writer> MakeWriter<'writer> + 'static,
120{
121    writer: Mutex<W>,
122    attribute_mapper: Box<dyn AttributeMapper>,
123    extra_fields: serde_json::Map<String, Value>,
124    normalize_json: bool,
125    span_events: FmtSpan,
126}
127
128impl<W> ECSLayer<W>
129where
130    W: for<'writer> MakeWriter<'writer> + 'static + Send + Sync,
131{
132    /// Installs the layer in a no-output tracing subscriber.
133    ///
134    /// The tracing subscriber is configured with `EnvFilter::from_default_env()`.
135    ///
136    /// This also takes care of installing the [`tracing-log`](https://crates.io/crates/tracing-log)
137    /// compatibility layer so regular logging done from the [`log` crate](https://crates.io/crates/log) will
138    /// be correctly reported as tracing events.
139    ///
140    /// This is an opinionated way to use this layer. Look at the source of this method if you want a tight control
141    /// of how the underlying subscriber is constructed or if you want to disable classic logs to be output as tracing events...
142    ///
143    pub fn install(self) -> Result<(), Error> {
144        let noout = SubscriberBuilder::default()
145            .with_writer(sink)
146            .with_env_filter(EnvFilter::from_default_env())
147            .with_span_events(FmtSpan::EXIT)
148            .finish();
149        let subscriber = self.with_subscriber(noout);
150        tracing_core::dispatcher::set_global_default(tracing_core::dispatcher::Dispatch::new(
151            subscriber,
152        ))
153        .map_err(Error::from)?;
154        LogTracer::init().map_err(Error::from)?;
155
156        Ok(())
157    }
158}
159
160impl<W> ECSLayer<W>
161where
162    W: for<'writer> MakeWriter<'writer> + 'static,
163{
164    fn log_span_event<S: Subscriber + for<'a> LookupSpan<'a>>(
165        &self,
166        id: &Id,
167        ctx: &Context<'_, S>,
168        event_action: &'static str,
169    ) {
170        let span = ctx.span(id).expect("span not found, this is a bug");
171        let span_name = span.name();
172        let span_id = id.into_u64().to_string();
173
174        let mut span_fields = HashMap::<Cow<'static, str>, Value>::new();
175        if let Some(span_object) = span.extensions().get::<HashMap<Cow<'static, str>, Value>>() {
176            span_fields.extend(span_object.clone());
177        }
178
179        let span_event = ECSSpanEvent {
180            timestamp: Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Nanos, true),
181            event_kind: "span",
182            event_action,
183            span_id,
184            span_name: span_name.to_string(),
185            dynamic_fields: self
186                .extra_fields
187                .iter()
188                .map(|(key, value)| (key.clone(), value.clone()))
189                .chain(
190                    span_fields
191                        .into_iter()
192                        .map(|(key, value)| (key.to_string(), value)),
193                )
194                .collect(),
195        };
196
197        let writer = self.writer.lock().unwrap();
198        let mut writer = writer.make_writer();
199        let _ = if self.normalize_json {
200            serde_json::to_writer(writer.by_ref(), &span_event.normalize())
201        } else {
202            serde_json::to_writer(writer.by_ref(), &span_event)
203        };
204        let _ = writer.write(&[b'\n']);
205    }
206}
207
208impl<W, S> Layer<S> for ECSLayer<W>
209where
210    S: Subscriber + for<'a> LookupSpan<'a>,
211    W: for<'writer> MakeWriter<'writer> + 'static,
212{
213    fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
214        if self.span_events.clone() & FmtSpan::NEW == FmtSpan::NEW {
215            self.log_span_event(id, &ctx, "new");
216        }
217
218        let span = ctx.span(id).expect("span not found, this is a bug");
219
220        let mut extensions = span.extensions_mut();
221
222        if extensions.get_mut::<Map<String, Value>>().is_none() {
223            let mut object = HashMap::with_capacity(16);
224            let mut visitor = visitor::FieldVisitor::new(
225                &mut object,
226                span.name(),
227                self.attribute_mapper.as_ref(),
228            );
229            attrs.record(&mut visitor);
230            extensions.insert(object);
231        }
232    }
233
234    fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
235        let span = ctx.span(id).expect("span not found, this is a bug");
236        let mut extensions = span.extensions_mut();
237        if let Some(object) = extensions.get_mut::<HashMap<Cow<'static, str>, Value>>() {
238            let mut add_field_visitor =
239                visitor::FieldVisitor::new(object, span.name(), self.attribute_mapper.as_ref());
240            values.record(&mut add_field_visitor);
241        } else {
242            let mut object = HashMap::with_capacity(16);
243            let mut add_field_visitor = visitor::FieldVisitor::new(
244                &mut object,
245                span.name(),
246                self.attribute_mapper.as_ref(),
247            );
248            values.record(&mut add_field_visitor);
249            extensions.insert(object);
250        }
251    }
252
253    fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
254        if self.span_events.clone() & FmtSpan::ENTER == FmtSpan::ENTER {
255            self.log_span_event(id, &ctx, "enter");
256        }
257    }
258
259    fn on_exit(&self, id: &Id, ctx: Context<'_, S>) {
260        if self.span_events.clone() & FmtSpan::EXIT == FmtSpan::EXIT {
261            self.log_span_event(id, &ctx, "exit");
262        }
263    }
264
265    fn on_close(&self, id: Id, ctx: Context<'_, S>) {
266        if self.span_events.clone() & FmtSpan::CLOSE == FmtSpan::CLOSE {
267            self.log_span_event(&id, &ctx, "close");
268        }
269    }
270
271    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
272        let mut span_fields = HashMap::<Cow<'static, str>, Value>::new();
273
274        // Get span name
275        let span = ctx.current_span().id().and_then(|id| {
276            ctx.span_scope(id).map(|scope| {
277                scope.from_root().fold(String::new(), |mut spans, span| {
278                    // Add span fields to the base object
279                    if let Some(span_object) =
280                        span.extensions().get::<HashMap<Cow<'static, str>, Value>>()
281                    {
282                        span_fields.extend(span_object.clone());
283                    }
284                    if !spans.is_empty() {
285                        spans = format!("{}:{}", spans, span.name());
286                    } else {
287                        spans = span.name().to_string();
288                    }
289                    spans
290                })
291            })
292        });
293
294        if let Some(span) = span {
295            span_fields.insert("span.name".into(), span.into());
296        }
297
298        // Extract metadata
299        // Insert level
300        let metadata = event.metadata();
301        let level = metadata.level().as_str();
302        let mut target = metadata.target().to_string();
303
304        // extract fields
305        let mut fields = HashMap::with_capacity(16);
306        let mut visitor = visitor::FieldVisitor::new(
307            &mut fields,
308            EVENT_SPAN_NAME,
309            self.attribute_mapper.as_ref(),
310        );
311        event.record(&mut visitor);
312
313        // detect classic log message and convert them to our format
314        let mut log_origin = LogOrigin::from(metadata);
315        if target == "log"
316            && fields.contains_key("log.target")
317            && fields.contains_key("log.module_path")
318        {
319            fields.remove("log.module_path");
320            target = value_to_string(fields.remove("log.target").unwrap()); // this is tested in the if condition
321
322            if let (Some(file), Some(line)) = (fields.remove("log.file"), fields.remove("log.line"))
323            {
324                log_origin = LogOrigin {
325                    file: LogFile {
326                        line: line.as_u64().and_then(|u| u32::try_from(u).ok()),
327                        name: file.as_str().map(|file| file.to_owned().into()),
328                    },
329                };
330            }
331        }
332
333        let message = fields
334            .remove("message")
335            .map(value_to_string)
336            .unwrap_or_default();
337        let line = ECSLogLine {
338            timestamp: Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Nanos, true),
339            message,
340            level,
341            log_origin,
342            logger: &target,
343            dynamic_fields: self
344                .extra_fields
345                .iter()
346                .map(|(key, value)| (key.clone(), value.clone()))
347                .chain(
348                    span_fields
349                        .into_iter()
350                        .map(|(key, value)| (key.to_string(), value)),
351                )
352                .chain(
353                    fields
354                        .into_iter()
355                        .map(|(key, value)| (key.to_string(), value)),
356                )
357                .collect(),
358        };
359        let writer = self.writer.lock().unwrap(); // below code will not poison the lock so it should be safe here to unwrap()
360        let mut writer = writer.make_writer_for(metadata);
361        let _ = if self.normalize_json {
362            serde_json::to_writer(writer.by_ref(), &line.normalize())
363        } else {
364            serde_json::to_writer(writer.by_ref(), &line)
365        };
366        let _ = writer.write(&[b'\n']);
367    }
368}
369
370fn value_to_string(value: Value) -> String {
371    match value {
372        Value::String(string) => string,
373        _ => value.to_string(),
374    }
375}
376
377/// Builder for a subscriber Layer writing ECS compatible json lines to a writer.
378///
379/// Example:
380///
381/// ```rust
382/// use tracing_ecs::ECSLayerBuilder;
383///
384/// // creates a minimal layer logging to stdout, and install it
385/// ECSLayerBuilder::default()
386///     .stdout()
387///     .install()
388///     .unwrap();
389/// ```
390///
391pub struct ECSLayerBuilder {
392    extra_fields: Option<serde_json::Map<String, Value>>,
393    attribute_mapper: Box<dyn AttributeMapper>,
394    normalize_json: bool,
395    span_events: FmtSpan,
396}
397
398impl Default for ECSLayerBuilder {
399    fn default() -> Self {
400        Self {
401            extra_fields: Default::default(),
402            attribute_mapper: Default::default(),
403            normalize_json: true,
404            span_events: FmtSpan::NONE,
405        }
406    }
407}
408
409impl Default for Box<dyn AttributeMapper> {
410    fn default() -> Self {
411        Box::new(|_span_name: &str, name: Cow<'static, str>| name)
412    }
413}
414
415impl ECSLayerBuilder {
416    pub fn with_extra_fields<F: Serialize>(mut self, extra_fields: F) -> Result<Self, Error> {
417        let as_json = serde_json::to_value(&extra_fields)
418            .map_err(|_| Error::ExtraFieldNotSerializableAsJson)?;
419        match as_json {
420            Value::Object(extra_fields) => {
421                self.extra_fields = Some(extra_fields);
422                Ok(self)
423            }
424            _ => Err(Error::ExtraFieldNotAMap),
425        }
426    }
427
428    pub fn with_attribute_mapper<M>(mut self, attribute_mapper: M) -> Self
429    where
430        M: AttributeMapper,
431    {
432        self.attribute_mapper = Box::new(attribute_mapper);
433        self
434    }
435
436    ///
437    /// Setup span events logging. Events supported: `NEW`, `ENTER`, `EXIT`, `CLOSE`.
438    ///
439    /// Each configured span event (eg. enter span) will output a new log event line with an `event` object having the following attributes:
440    /// - `kind`: `span`
441    /// - `action`: either `new`, `enter`, `exit` or `close`
442    ///
443    /// Example:
444    ///
445    /// ```rust
446    /// use tracing_ecs::ECSLayerBuilder;
447    /// use tracing_subscriber::fmt::format::FmtSpan;
448    ///
449    /// ECSLayerBuilder::default()
450    ///     .with_span_events(FmtSpan::ENTER | FmtSpan::EXIT)
451    ///     .stdout()
452    ///     .install()
453    ///     .unwrap();
454    /// ```
455    ///
456    /// Event log line example:
457    ///
458    /// ```json
459    /// {
460    ///     "@timestamp": "2025-07-24T08:24:19.733176000Z",
461    ///     "event": {
462    ///         "action": "enter",
463    ///         "kind": "span"
464    ///     },
465    ///     "span": {
466    ///         "id": "1",
467    ///         "name": "span_name"
468    ///     }
469    /// }
470    /// ```
471    ///
472    ///
473    pub fn with_span_events(mut self, span_events: FmtSpan) -> Self {
474        self.span_events = span_events;
475        self
476    }
477
478    /// Control the normalization (keys de-dotting) of the generated json
479    /// in the sense of <https://www.elastic.co/guide/en/ecs/current/ecs-guidelines.html>.
480    ///
481    /// By default, normalization is enabled, thus logging `host.hostname="localhost"` will output:
482    ///
483    /// ```json
484    /// {"host":{"hostname": "localhost"}}
485    /// ```
486    ///
487    /// With normalization disabled:
488    ///
489    /// ```json
490    /// {"host.hostname": "localhost"}
491    /// ```
492    ///
493    /// Depending on your use case you may want to disable normalization if it's done
494    /// elsewhere in your log processing pipeline.
495    ///
496    /// Benchmarks suggests a 30% speed up on log generation. (benches run on an Apple M2 Pro) it will
497    /// also allocate less because, normalization recursively recreates a brand new json `Value` (not measured).
498    ///
499    pub fn normalize_json(self, normalize_json: bool) -> Self {
500        Self {
501            normalize_json,
502            ..self
503        }
504    }
505
506    pub fn stderr(self) -> ECSLayer<fn() -> Stderr> {
507        self.build_with_writer(io::stderr)
508    }
509
510    pub fn stdout(self) -> ECSLayer<fn() -> Stdout> {
511        self.build_with_writer(io::stdout)
512    }
513
514    pub fn build_with_writer<W>(self, writer: W) -> ECSLayer<W>
515    where
516        W: for<'writer> MakeWriter<'writer> + 'static,
517    {
518        ECSLayer {
519            writer: Mutex::new(writer),
520            attribute_mapper: self.attribute_mapper,
521            extra_fields: self.extra_fields.unwrap_or_default(),
522            normalize_json: self.normalize_json,
523            span_events: self.span_events,
524        }
525    }
526}
527
528#[derive(thiserror::Error, Debug)]
529pub enum Error {
530    #[error("Extra field cannot be serialized as json")]
531    ExtraFieldNotSerializableAsJson,
532    #[error("Extra field must be serializable as a json map")]
533    ExtraFieldNotAMap,
534    #[error("{0}")]
535    SetGlobalError(#[from] SetGlobalDefaultError),
536    #[error("{0}")]
537    SetLoggerError(#[from] SetLoggerError),
538}
539
540#[cfg(test)]
541mod test {
542
543    use std::{
544        io::{self, sink, BufRead, BufReader},
545        sync::{Arc, Mutex, MutexGuard, Once, TryLockError},
546        thread::{self},
547    };
548
549    use maplit::hashmap;
550    use serde_json::{json, Map, Value};
551    use tracing_log::LogTracer;
552    use tracing_subscriber::{
553        fmt::{format::FmtSpan, MakeWriter, SubscriberBuilder},
554        Layer,
555    };
556
557    use crate::ECSLayerBuilder;
558
559    static START: Once = Once::new();
560
561    pub(crate) struct MockWriter {
562        buf: Arc<Mutex<Vec<u8>>>,
563    }
564
565    impl MockWriter {
566        pub(crate) fn new(buf: Arc<Mutex<Vec<u8>>>) -> Self {
567            Self { buf }
568        }
569
570        pub(crate) fn map_error<Guard>(err: TryLockError<Guard>) -> io::Error {
571            match err {
572                TryLockError::WouldBlock => io::Error::from(io::ErrorKind::WouldBlock),
573                TryLockError::Poisoned(_) => io::Error::from(io::ErrorKind::Other),
574            }
575        }
576
577        pub(crate) fn buf(&self) -> io::Result<MutexGuard<'_, Vec<u8>>> {
578            self.buf.try_lock().map_err(Self::map_error)
579        }
580    }
581
582    impl io::Write for MockWriter {
583        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
584            self.buf()?.write(buf)
585        }
586
587        fn flush(&mut self) -> io::Result<()> {
588            self.buf()?.flush()
589        }
590    }
591
592    #[derive(Clone, Default)]
593    pub(crate) struct MockMakeWriter {
594        buf: Arc<Mutex<Vec<u8>>>,
595    }
596
597    impl MockMakeWriter {
598        pub(crate) fn buf(&self) -> MutexGuard<'_, Vec<u8>> {
599            self.buf.lock().unwrap()
600        }
601    }
602
603    impl<'a> MakeWriter<'a> for MockMakeWriter {
604        type Writer = MockWriter;
605
606        fn make_writer(&'a self) -> Self::Writer {
607            MockWriter::new(self.buf.clone())
608        }
609    }
610
611    fn run_test<T>(builder: ECSLayerBuilder, test: T) -> Vec<Map<String, Value>>
612    where
613        T: FnOnce() -> (),
614    {
615        START.call_once(|| LogTracer::init().unwrap());
616
617        let writer = MockMakeWriter::default();
618
619        let noout = SubscriberBuilder::default().with_writer(|| sink()).finish();
620        let subscriber = builder
621            .build_with_writer(writer.clone())
622            .with_subscriber(noout);
623        tracing_core::dispatcher::with_default(
624            &tracing_core::dispatcher::Dispatch::new(subscriber),
625            test,
626        );
627        let bytes: Vec<u8> = writer.buf().iter().copied().collect();
628        let mut ret = Vec::new();
629        for line in BufReader::new(bytes.as_slice()).lines() {
630            let line = line.expect("Unable to read line");
631            println!("{line}");
632            ret.push(serde_json::from_str(&line).expect("Invalid json line"));
633        }
634        ret
635    }
636
637    /// General tests
638    #[test]
639    fn test() {
640        let result = run_test(ECSLayerBuilder::default(), || {
641            log::info!("A classic log message outside spans");
642            tracing::info!("A classic tracing event outside spans");
643            let span = tracing::info_span!("span1", foo = "bar", transaction.id = "abcdef");
644            let enter = span.enter();
645            log::info!("A classic log inside a span");
646            tracing::info!(target: "foo_event_target", "A classic tracing event inside a span");
647            drop(enter);
648            log::info!(target: "foo_bar_target", "outside a span");
649        });
650        assert_eq!(result.len(), 5);
651        assert_string(
652            result[0].get("message"),
653            Some("A classic log message outside spans"),
654        );
655        assert_string(
656            result[1].get("message"),
657            Some("A classic tracing event outside spans"),
658        );
659        assert_string(
660            result[2].get("message"),
661            Some("A classic log inside a span"),
662        );
663        assert_string(
664            result[3].get("message"),
665            Some("A classic tracing event inside a span"),
666        );
667        assert_string(result[0].get("span"), None);
668        assert_string(result[1].get("span"), None);
669        assert_string(result[2].get("span").unwrap().get("name"), Some("span1"));
670        assert_string(result[4].get("span.name"), None);
671        assert_string(result[3].get("span").unwrap().get("name"), Some("span1"));
672        assert_string(result[0].get("transaction"), None);
673        assert_string(result[1].get("transaction"), None);
674        assert_string(
675            result[2].get("transaction").unwrap().get("id"),
676            Some("abcdef"),
677        );
678        assert_string(
679            result[3].get("transaction").unwrap().get("id"),
680            Some("abcdef"),
681        );
682        assert_string(result[4].get("transaction"), None);
683
684        // log.logger (aka rust target)
685        assert_string(
686            result[0].get("log").unwrap().get("logger"),
687            Some("tracing_ecs::test"),
688        );
689        assert_string(
690            result[1].get("log").unwrap().get("logger"),
691            Some("tracing_ecs::test"),
692        );
693        assert_string(
694            result[2].get("log").unwrap().get("logger"),
695            Some("tracing_ecs::test"),
696        );
697        assert_string(
698            result[3].get("log").unwrap().get("logger"),
699            Some("foo_event_target"),
700        );
701        assert_string(
702            result[4].get("log").unwrap().get("logger"),
703            Some("foo_bar_target"),
704        );
705
706        // logs have a @timestamp value
707        assert!(result[0]
708            .get("@timestamp")
709            .cloned()
710            .filter(Value::is_string)
711            .is_some());
712        assert!(result[1]
713            .get("@timestamp")
714            .cloned()
715            .filter(Value::is_string)
716            .is_some());
717    }
718
719    fn assert_string(value: Option<&Value>, expected: Option<&str>) {
720        assert_eq!(
721            value,
722            expected.map(|s| Value::String(s.to_string())).as_ref()
723        );
724    }
725
726    /// Extra fields: we can pass anything that is Serialize as extra fields
727    #[test]
728    fn test_extra_fields() {
729        let value = json!({
730            "tags": ["t1", "t2"],
731            "labels": {
732                "env": "prod",
733                "service": "foobar",
734            }
735        });
736        let result = run_test(
737            ECSLayerBuilder::default()
738                .with_extra_fields(&value)
739                .unwrap(),
740            || {
741                log::info!("A classic log message outside spans");
742                tracing::info!("A classic tracing event outside spans");
743                tracing::info!(tags = 123, "A classic tracing event outside spans");
744            },
745        );
746        assert_eq!(result.len(), 3);
747        assert_string(
748            result[0].get("message"),
749            Some("A classic log message outside spans"),
750        );
751        assert_string(
752            result[1].get("message"),
753            Some("A classic tracing event outside spans"),
754        );
755        assert_eq!(result[0].get("tags"), value.get("tags"));
756        assert_eq!(result[1].get("tags"), value.get("tags"));
757        assert_eq!(result[1].get("labels"), value.get("labels"));
758        assert_eq!(result[1].get("labels"), value.get("labels"));
759
760        // a span or an event overrode the tags value, the last prevails (in our case the event value)
761        assert_eq!(result[2].get("tags"), Some(&json!(123)));
762    }
763
764    #[test]
765    fn test_spans() {
766        let result = run_test(ECSLayerBuilder::default(), || {
767            tracing::info!("outside");
768            let sp1 = tracing::info_span!("span1", sp1 = "val1", same = "same1");
769            let _enter1 = sp1.enter();
770            tracing::info!("inside 1");
771            let sp2 = tracing::info_span!("span2", sp2 = "val2", same = "same2");
772            let _enter2 = sp2.enter();
773            tracing::info!("inside 2");
774            tracing::info!(same = "last prevails", "inside 2");
775        });
776        // span name (note that json is normalized so there is no dot anymore in keys)
777        assert_string(result[0].get("span"), None);
778        assert_string(result[1].get("span").unwrap().get("name"), Some("span1"));
779        assert_string(
780            result[2].get("span").unwrap().get("name"),
781            Some("span1:span2"),
782        );
783        assert_string(
784            result[3].get("span").unwrap().get("name"),
785            Some("span1:span2"),
786        );
787
788        // span attributes
789        assert_string(result[0].get("sp1"), None);
790        assert_string(result[1].get("sp1"), Some("val1"));
791        assert_string(result[2].get("sp1"), Some("val1"));
792        assert_string(result[3].get("sp1"), Some("val1"));
793
794        assert_string(result[0].get("sp2"), None);
795        assert_string(result[1].get("sp2"), None);
796        assert_string(result[2].get("sp2"), Some("val2"));
797        assert_string(result[3].get("sp2"), Some("val2"));
798
799        assert_string(result[0].get("same"), None);
800        assert_string(result[1].get("same"), Some("same1"));
801        assert_string(result[2].get("same"), Some("same2"));
802        assert_string(result[3].get("same"), Some("last prevails"));
803    }
804
805    #[test]
806    fn test_attribute_mapping() {
807        let result = run_test(
808            ECSLayerBuilder::default().with_attribute_mapper(
809                // this mapper will change "key1" name into "foobar" only in the "span1" span
810                hashmap! {
811                    "span1" => hashmap! {
812                        "key1" => "foobar"
813                    }
814                },
815            ),
816            || {
817                let sp1 = tracing::info_span!("span1", key1 = "val1", other1 = "o1");
818                let _enter1 = sp1.enter();
819                tracing::info!("inside 1");
820                let sp2 = tracing::info_span!("span2", key1 = "val2", other2 = "o2");
821                let _enter2 = sp2.enter();
822                tracing::info!("inside 2");
823            },
824        );
825
826        // span1 => key1 has been renamed
827        assert_string(result[0].get("key1"), None);
828        assert_string(result[0].get("foobar"), Some("val1"));
829        assert_string(result[0].get("other1"), Some("o1"));
830        assert_string(result[0].get("other2"), None);
831        // span2 => key1 renamed in span1... but also defined in span2
832        assert_string(result[1].get("key1"), Some("val2"));
833        assert_string(result[1].get("foobar"), Some("val1"));
834        assert_string(result[1].get("other1"), Some("o1"));
835        assert_string(result[1].get("other2"), Some("o2"));
836    }
837
838    #[test]
839    fn test_normalization() {
840        let value = json!({
841            "host.hostname": "localhost"
842        });
843        let result = run_test(
844            ECSLayerBuilder::default()
845                .with_extra_fields(&value)
846                .unwrap(),
847            || {
848                tracing::info!(source.ip = "1.2.3.4", "hello world");
849            },
850        );
851        assert_eq!(
852            result[0].get("host").unwrap(),
853            &json!({
854                "hostname": "localhost",
855            })
856        );
857        assert_eq!(
858            result[0].get("source").unwrap(),
859            &json!({
860                "ip": "1.2.3.4",
861            })
862        );
863    }
864    #[test]
865
866    fn test_normalization_disabled() {
867        let value = json!({
868            "host.hostname": "localhost"
869        });
870        let result = run_test(
871            ECSLayerBuilder::default()
872                .normalize_json(false)
873                .with_extra_fields(&value)
874                .unwrap(),
875            || {
876                tracing::info!(source.ip = "1.2.3.4", "hello world");
877            },
878        );
879        assert_eq!(result[0].get("host.hostname").unwrap(), &json!("localhost"));
880        assert_eq!(result[0].get("source.ip").unwrap(), &json!("1.2.3.4"));
881    }
882
883    #[test]
884    fn test_interleaving_logs() {
885        let result = run_test(ECSLayerBuilder::default(), || {
886            let mut join_handles = Vec::new();
887            for i in 0..5 {
888                // doing multi threaded test is a bit awkward as the current dispatcher needs
889                // to be installed into each spawned thread.
890                tracing_core::dispatcher::get_default(|dispatch| {
891                    let dispatch = dispatch.clone();
892                    join_handles.push(thread::spawn(move || {
893                        tracing_core::dispatcher::with_default(&dispatch, move || {
894                            for j in 0..1000 {
895                                tracing::info!(thread = i, iteration = j, "hello world");
896                            }
897                        });
898                    }));
899                });
900            }
901            for join_handle in join_handles {
902                join_handle.join().unwrap();
903            }
904        });
905        assert_eq!(result.len(), 5000);
906    }
907
908    #[test]
909    fn test_span_instrumentation() {
910        let result = run_test(
911            ECSLayerBuilder::default()
912                .normalize_json(false)
913                .with_span_events(FmtSpan::ENTER | FmtSpan::EXIT),
914            || {
915                let span = tracing::info_span!("test_span", foo = "bar");
916                let _enter = span.enter();
917                tracing::info!("inside span");
918            },
919        );
920
921        assert_eq!(result.len(), 3); // span enter, event, span exit
922
923        assert_eq!(result[0].get("event.kind"), Some(&json!("span")));
924        assert_eq!(result[0].get("event.action"), Some(&json!("enter")));
925        assert_eq!(result[0].get("span.name"), Some(&json!("test_span")));
926        assert_eq!(result[0].get("foo"), Some(&json!("bar")));
927        assert!(result[0].get("span.id").unwrap().is_string());
928
929        assert_eq!(result[1].get("message"), Some(&json!("inside span")));
930        assert_eq!(result[1].get("span.name"), Some(&json!("test_span")));
931        assert_eq!(result[1].get("foo"), Some(&json!("bar")));
932
933        assert_eq!(result[2].get("event.kind"), Some(&json!("span")));
934        assert_eq!(result[2].get("event.action"), Some(&json!("exit")));
935        assert_eq!(result[2].get("span.name"), Some(&json!("test_span")));
936        assert_eq!(result[2].get("foo"), Some(&json!("bar")));
937        assert_eq!(result[2].get("span.id"), result[0].get("span.id"));
938    }
939}