1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! Structures to represent an event created by [`tracing`](https://docs.rs/tracing).

use anyhow::Context;
use anyhow::Result;
use itertools::Itertools;
use nu_ansi_term::Color;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::fmt::{Display, Formatter, Result as FmtResult};

/// Represents an event created by `tracing`.
///
/// It is not possible to construct one directly from a `tracing` event.
/// Instead its expected that they are returned by one of the methods on [`crate::BinProcess`] which retrives them by parsing `tracing`'s JSON output.
#[derive(serde::Deserialize, Debug, Clone, PartialEq)]
pub struct Event {
    /// The timestamp of the event.
    pub timestamp: String,
    /// The level of the event.
    pub level: Level,
    /// The target of the event, this is usually the module name of the code that triggered the event.
    pub target: String,
    /// Contains the message and other fields included in the event.
    pub fields: Fields,
    /// The last span that was entered before the event was triggered.
    #[serde(default)]
    pub span: HashMap<String, JsonValue>,
    /// Every span that was active while the event was triggered.
    #[serde(default)]
    pub spans: Vec<HashMap<String, JsonValue>>,
}

/// The level of the `Event`.
#[derive(serde::Deserialize, Debug, Clone, PartialEq)]
pub enum Level {
    #[serde(rename = "ERROR")]
    Error,
    #[serde(rename = "WARN")]
    Warn,
    #[serde(rename = "INFO")]
    Info,
    #[serde(rename = "DEBUG")]
    Debug,
    #[serde(rename = "TRACE")]
    Trace,
}

impl Display for Level {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        match self {
            Level::Error => write!(f, "{}", Color::Red.paint("ERROR")),
            Level::Warn => write!(f, " {}", Color::Yellow.paint("WARN")),
            Level::Info => write!(f, " {}", Color::Green.paint("INFO")),
            Level::Debug => write!(f, " {}", Color::Blue.paint("DEBUG")),
            Level::Trace => write!(f, " {}", Color::Purple.paint("TRACE")),
        }
    }
}

/// Contains the message and other fields included in the event.
#[derive(serde::Deserialize, Debug, Clone, PartialEq)]
pub struct Fields {
    /// The message of the event.
    /// Some events dont have a message in which case this is an empty `String`.
    #[serde(default)]
    pub message: String,
    /// All fields other than the message.
    /// For an event created by: `tracing::info!("message", some_field=4)`
    /// This would contain the `HashMap`: `{"some_field", JsonValue::Number(4)}`.
    #[serde(flatten)]
    pub fields: HashMap<String, JsonValue>,
}

impl Display for Event {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        write!(
            f,
            "{}",
            Color::Default.dimmed().paint(
                // If timestamp in expected rfc3339 format then strip the date from the datetime.
                // The date portion isnt useful in integration tests.
                if chrono::DateTime::parse_from_rfc3339(&self.timestamp).is_ok() {
                    // Now that we have confirmed it as an rfc3339 date we can take the easy path of just stripping the date prefix.
                    &self.timestamp[11..]
                } else {
                    &self.timestamp
                }
            )
        )?;

        if let Some(backtrace) = self.fields.fields.get("panic.backtrace") {
            // Special case panics.
            // Panics usually get reasonable formatting locally and we dont want to regress on that just because we had to stuff them through tracing
            writeln!(
                f,
                " {} {}",
                Color::Red.reverse().paint("PANIC"),
                self.fields.message,
            )?;
            match backtrace {
                JsonValue::String(backtrace) => {
                    for line in backtrace.lines() {
                        // we can have a little color as a treat
                        if line.trim().starts_with("at ") {
                            writeln!(f, "{}", Color::Default.dimmed().paint(line))?;
                        } else {
                            writeln!(f, "{}", line)?;
                        }
                    }
                }
                backtrace => write!(f, "{backtrace}")?,
            }
        } else {
            // Regular old formatting, matches the formatting used by the default tracing subscriber
            write!(f, " {} ", self.level)?;
            if !self.spans.is_empty() {
                for span in &self.spans {
                    let name = span.get("name").unwrap().as_str().unwrap();
                    write!(f, "{}", Color::Default.bold().paint(name))?;
                    write!(f, "{}", Color::Default.bold().paint("{"))?;
                    let mut first = true;
                    for (key, value) in span.iter().sorted_by_key(|(key, _)| <&String>::clone(key))
                    {
                        if key != "name" {
                            if !first {
                                write!(f, " ")?;
                            }
                            first = false;

                            write!(f, "{}", key)?;
                            write!(f, "{}", Color::Default.dimmed().paint("="))?;
                            write!(f, "{}", value)?;
                        }
                    }
                    write!(f, "{}", Color::Default.bold().paint("}"))?;
                    write!(f, "{}", Color::Default.dimmed().paint(":"))?;
                }
                write!(f, " ")?;
            }

            write!(
                f,
                "{}{}",
                Color::Default.dimmed().paint(&self.target),
                Color::Default.dimmed().paint(":"),
            )?;

            if !self.fields.message.is_empty() {
                write!(f, " {}", self.fields.message)?;
            }

            for (key, value) in self
                .fields
                .fields
                .iter()
                .sorted_by_key(|(key, _)| <&String>::clone(key))
            {
                write!(f, " {}", Color::Default.italic().paint(key))?;
                write!(f, "{}", Color::Default.dimmed().paint("="))?;
                write!(f, "{}", QuotelessDisplay(value))?;
            }
        }

        Ok(())
    }
}

struct QuotelessDisplay<'a>(&'a JsonValue);

impl<'a> Display for QuotelessDisplay<'a> {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        match self {
            QuotelessDisplay(JsonValue::String(str)) => write!(f, "{str}"),
            QuotelessDisplay(value) => write!(f, "{value}"),
        }
    }
}

impl Event {
    /// Constructs an Event by parsing a single event from `tracing`'s JSON output.
    pub fn from_json_str(s: &str) -> Result<Self> {
        serde_json::from_str(s).context(format!("Failed to parse json: {s}"))
    }
}