aingle_observability/
lib.rs

1#![warn(missing_docs)]
2//! # Structured Contextual Logging (or tracing)
3//! ## Why
4//! [Watch](https://www.youtube.com/watch?v=JjItsfqFIdo) or [Read](https://tokio.rs/blog/2019-08-tracing/)
5//!
6//! ## Intention of this crate
7//! This crate is designed ot be a place to experiment with ideas around
8//! tracing and structured logging. This crate will probably never stabilize.
9//! Instead it is my hope to feed any good ideas back into the underlying
10//! dependencies.
11//!
12//! ## Usage
13//! There are a couple of ways to use structured logging.
14//! ### Console and filter
15//! If you want to try and filter in on an issue it might be easiest to simply log to the console and filter on what you want.
16//! Here's an example command:
17//! ```bash
18//! RUST_LOG='core[a{something="foo"}]=debug' my_bin
19//! ```
20//! Or a more simple version using the default `Log`:
21//! ```bash
22//! RUST_LOG=trace my_bin
23//! ```
24//! #### Types of tracing
25//! There are many types of tracing exposed by this crate.
26//! The [Output] type is designed to be used with something like [structopt](https://docs.rs/structopt/0.3.20/structopt/)
27//! so you can easily set which type you want with a command line arg.
28//! You could also use an environment variable.
29//! The [Output] variant is passing into the [init_fmt] function on start up.
30//! #### Filtering
31//! ```bash
32//! RUST_LOG='core[a{something="foo"}]=debug'
33//! ```
34//! Here we are saying show me all the events that are:
35//! - In the `core` module
36//! - Inside a span called `a`
37//! - The span `a` has to have a field called `something` that is equal to `foo`
38//! - They are at least debug level.
39//!
40//! Most of these options are optional.
41//! They can be combined like:
42//! ```bash
43//! RUST_LOG='[{}]=error,[{something}]=debug'
44//! ```
45//! > The above means show me errors from anywhere but also any event or span with the field something that's at least debug.
46//!
47//! [See here](https://docs.rs/tracing-subscriber/0.2.2/tracing_subscriber/filter/struct.EnvFilter.html) for more info.
48//!
49//! #### Json
50//! Sometimes there's too much data and it's better to capture it to interact with using another tool later.
51//! For this we can output everything as Json using the flag `--structured Json`.
52//! Then you can pipe the output from stdout to you're file of choice.
53//! Here's some sample output:
54//! ```json
55//! {"time":"2020-03-03T08:07:05.910Z","name":"event crates/sim2h/src/sim2h_im_state.rs:695","level":"INFO","target":"sim2h::sim2h_im_state","module_path":"sim2h::sim2h_im_state","file":"crates/sim2h/src/sim2h_im_stat
56//! e.rs","line":695,"fields":{"space_hashes":"[]"},"spans":[{"id":[1099511627778],"name":"check_gossip","level":"INFO","target":"sim2h::sim2h_im_state","module_path":"sim2h::sim2h_im_state","file":"crates/sim2h/src/s
57//! im2h_im_state.rs","line":690}]}
58//! ```
59//! Every log will include the above information expect for the spans which will only show up if there are parent spans in the context of the event.
60//!
61//! You can combine filter with Json as well.
62//!
63//! ##### Tools
64//! Some useful tools for formatting and using the json data.
65//! - [json2csv](https://www.npmjs.com/package/json2csv)
66//! - [jq](https://stedolan.github.io/jq/)
67//! - [tad](https://www.tadviewer.com/)
68//!
69//! A sample workflow:
70//! ```bash
71//! RUST_LOG='core[{}]=debug' my_bin --structured Json > log.json
72//! cat out.json | jq '. | {time: .time, name: .name, message: .fields.message, file: .file, line: .line, fields: .fields, spans: .spans}' | json2csv -o log.csv
73//! tad log.csv
74//! ```
75
76use tracing::Subscriber;
77use tracing_subscriber::{
78    fmt::format::FmtSpan,
79    registry::LookupSpan,
80    EnvFilter,
81    prelude::*,
82};
83
84use std::{str::FromStr, sync::Once};
85
86use flames::{toml_path, FlameTimed};
87use fmt::*;
88
89mod flames;
90mod fmt;
91pub mod metrics;
92mod open;
93
94pub use open::{Config, Context, MsgWrap, OpenSpanExt};
95
96pub use tracing;
97
98#[derive(Debug, Clone)]
99/// Sets the kind of structured logging output you want
100pub enum Output {
101    /// More compact version of above
102    Compact,
103    /// Outputs everything as json
104    Json,
105    /// Json with timed spans
106    JsonTimed,
107    /// Regular logging (default)
108    Log,
109    /// Regular logging plus timed spans
110    LogTimed,
111    /// Creates a flamegraph from timed spans
112    FlameTimed,
113    /// Creates a flamegraph from timed spans using idle time
114    IceTimed,
115    /// Opentelemetry tracing
116    OpenTel,
117    /// No logging to console
118    None,
119}
120
121/// ParseError is a String
122pub type ParseError = String;
123
124static INIT: Once = Once::new();
125
126impl FromStr for Output {
127    type Err = ParseError;
128    fn from_str(day: &str) -> Result<Self, Self::Err> {
129        match day {
130            "Json" => Ok(Output::Json),
131            "JsonTimed" => Ok(Output::JsonTimed),
132            "IceTimed" => Ok(Output::IceTimed),
133            "Log" => Ok(Output::Log),
134            "LogTimed" => Ok(Output::LogTimed),
135            "FlameTimed" => Ok(Output::FlameTimed),
136            "Compact" => Ok(Output::Compact),
137            "OpenTel" => Ok(Output::OpenTel),
138            "None" => Ok(Output::None),
139            _ => Err("Could not parse log output type".into()),
140        }
141    }
142}
143
144/// Run logging in a unit test
145/// RUST_LOG or CUSTOM_FILTER must be set or
146/// this is a no-op
147pub fn test_run() -> Result<(), errors::TracingError> {
148    if std::env::var_os("RUST_LOG").is_none() {
149        return Ok(());
150    }
151    init_fmt(Output::Log)
152}
153
154/// Run tracing in a test that uses open telemetry to
155/// send span contexts across process and thread boundaries.
156pub fn test_run_open() -> Result<(), errors::TracingError> {
157    if std::env::var_os("RUST_LOG").is_none() {
158        return Ok(());
159    }
160    init_fmt(Output::OpenTel)
161}
162
163/// Same as test_run but with timed spans
164pub fn test_run_timed() -> Result<(), errors::TracingError> {
165    if std::env::var_os("RUST_LOG").is_none() {
166        return Ok(());
167    }
168    init_fmt(Output::LogTimed)
169}
170
171/// Same as test_run_timed but saves as json
172pub fn test_run_timed_json() -> Result<(), errors::TracingError> {
173    if std::env::var_os("RUST_LOG").is_none() {
174        return Ok(());
175    }
176    init_fmt(Output::JsonTimed)
177}
178
179/// Generate a flamegraph from timed spans "busy time".
180/// Takes a path where you are piping the output into.
181/// If the path is provided a flamegraph will automatically be generated.
182/// TODO: Get auto inferno to work
183/// for now use (fish, or the bash equiv):
184/// `2>| inferno-flamegraph > flamegraph_test_ice_(date +'%d-%m-%y-%X').svg`
185/// And run with `cargo test --quiet`
186pub fn test_run_timed_flame(path: Option<&str>) -> Result<Option<impl Drop>, errors::TracingError> {
187    if std::env::var_os("RUST_LOG").is_none() {
188        return Ok(None);
189    }
190    init_fmt(Output::FlameTimed)?;
191    Ok(path.and_then(|p| {
192        toml_path().map(|mut t| {
193            t.push(p);
194            FlameTimed::new(t)
195        })
196    }))
197}
198
199/// Generate a flamegraph from timed spans "idle time".
200/// Takes a path where you are piping the output into.
201/// If the path is provided a flamegraph will automatically be generated.
202/// TODO: Get auto inferno to work
203/// for now use (fish, or the bash equiv):
204/// `2>| inferno-flamegraph -c blue > flamegraph_test_ice_(date +'%d-%m-%y-%X').svg`
205/// And run with `cargo test --quiet`
206pub fn test_run_timed_ice(path: Option<&str>) -> Result<Option<impl Drop>, errors::TracingError> {
207    if std::env::var_os("RUST_LOG").is_none() {
208        return Ok(None);
209    }
210    init_fmt(Output::IceTimed)?;
211    Ok(path.and_then(|p| {
212        toml_path().map(|mut t| {
213            t.push(p);
214            FlameTimed::new(t)
215        })
216    }))
217}
218
219/// This checks RUST_LOG for a filter but doesn't complain if there is none or it doesn't parse.
220/// It then checks for CUSTOM_FILTER which if set will output an error if it doesn't parse.
221pub fn init_fmt(output: Output) -> Result<(), errors::TracingError> {
222    let mut filter = match std::env::var("RUST_LOG") {
223        Ok(_) => EnvFilter::from_default_env(),
224        Err(_) => EnvFilter::from_default_env().add_directive("[wasm_debug]=debug".parse()?),
225    };
226    if std::env::var("CUSTOM_FILTER").is_ok() {
227        EnvFilter::try_from_env("CUSTOM_FILTER")
228            .map_err(|e| eprintln!("Failed to parse CUSTOM_FILTER {:?}", e))
229            .map(|f| {
230                filter = f;
231            })
232            .ok();
233    }
234    // Custom formatters removed for tracing-subscriber 0.3 compatibility
235    // TODO: Re-implement custom formatters using the new FormatEvent trait if needed
236
237    match output {
238        Output::Json => {
239            let subscriber = tracing_subscriber::fmt()
240                .with_writer(std::io::stderr)
241                .with_target(true)
242                .json()
243                .with_env_filter(filter)
244                .finish();
245            finish(subscriber)
246        }
247        Output::JsonTimed => {
248            let subscriber = tracing_subscriber::fmt()
249                .with_writer(std::io::stderr)
250                .with_target(true)
251                .with_span_events(FmtSpan::CLOSE)
252                .json()
253                .with_env_filter(filter)
254                .finish();
255            finish(subscriber)
256        }
257        Output::Log => {
258            let subscriber = tracing_subscriber::fmt()
259                .with_writer(std::io::stderr)
260                .with_target(true)
261                .with_env_filter(filter)
262                .finish();
263            finish(subscriber)
264        }
265        Output::LogTimed => {
266            let subscriber = tracing_subscriber::fmt()
267                .with_writer(std::io::stderr)
268                .with_target(true)
269                .with_span_events(FmtSpan::CLOSE)
270                .with_env_filter(filter)
271                .finish();
272            finish(subscriber)
273        }
274        Output::FlameTimed => {
275            let subscriber = tracing_subscriber::fmt()
276                .with_writer(std::io::stderr)
277                .with_target(true)
278                .with_span_events(FmtSpan::CLOSE)
279                .with_env_filter(filter)
280                .finish();
281            finish(subscriber)
282        }
283        Output::IceTimed => {
284            let subscriber = tracing_subscriber::fmt()
285                .with_writer(std::io::stderr)
286                .with_target(true)
287                .with_span_events(FmtSpan::CLOSE)
288                .with_env_filter(filter)
289                .finish();
290            finish(subscriber)
291        }
292        Output::Compact => {
293            let subscriber = tracing_subscriber::fmt()
294                .with_writer(std::io::stderr)
295                .with_target(true)
296                .compact()
297                .with_env_filter(filter)
298                .finish();
299            finish(subscriber)
300        }
301        Output::OpenTel => {
302            // OpenTelemetry support removed - use Log instead
303            let subscriber = tracing_subscriber::fmt()
304                .with_writer(std::io::stderr)
305                .with_target(true)
306                .with_env_filter(filter)
307                .finish();
308            finish(subscriber)
309        }
310        Output::None => Ok(()),
311    }
312}
313
314fn finish<S>(subscriber: S) -> Result<(), errors::TracingError>
315where
316    S: Subscriber + Send + Sync + for<'span> LookupSpan<'span>,
317{
318    let mut result = Ok(());
319    INIT.call_once(|| {
320        result = tracing::subscriber::set_global_default(subscriber).map_err(Into::into);
321    });
322    result
323}
324
325pub mod errors {
326    //! Error in the tracing/logging framework
327
328    use thiserror::Error;
329
330    /// Error in the tracing/logging framework
331    #[allow(missing_docs)] // should be self-explanatory
332    #[derive(Error, Debug)]
333    pub enum TracingError {
334        #[error(transparent)]
335        SetGlobal(#[from] tracing::subscriber::SetGlobalDefaultError),
336        #[error("Failed to setup tracing flame")]
337        TracingFlame,
338        #[error(transparent)]
339        BadDirective(#[from] tracing_subscriber::filter::ParseError),
340    }
341}