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}