├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── tracing-forest-macros ├── Cargo.toml ├── README.md └── src │ ├── attribute.rs │ ├── derive.rs │ └── lib.rs └── tracing-forest ├── Cargo.toml ├── README.md ├── src ├── cfg.rs ├── fail.rs ├── layer │ ├── id.rs │ └── mod.rs ├── lib.rs ├── printer │ ├── mod.rs │ └── pretty.rs ├── processor.rs ├── runtime.rs ├── tag.rs └── tree │ ├── field.rs │ ├── mod.rs │ └── ser.rs └── tests ├── captured.rs ├── demo.rs ├── id.rs ├── set_global.rs ├── simulate_connections.rs ├── simulate_connections_blocking.rs ├── spawned_tasks.rs └── tag.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tracing Forest CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Check rustfmt 18 | run: cargo fmt --all -- --check 19 | - name: Check clippy 20 | run: cargo clippy --all-targets --all-features -- -D warnings 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Build 26 | run: cargo build --verbose 27 | - name: Run tests 28 | run: cargo test --verbose 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | test_feature_permutations.py 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "tracing-forest", 4 | # "tracing-forest-macros" 5 | ] 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tracing-forest 2 | [![github-img]][github-url] [![crates-img]][crates-url] [![docs-img]][docs-url] 3 | 4 | [github-url]: https://github.com/QnnOkabayashi/tracing-forest 5 | [crates-url]: https://crates.io/crates/tracing-forest 6 | [docs-url]: https://docs.rs/tracing-forest/latest/tracing_forest/ 7 | [github-img]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github 8 | [crates-img]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust 9 | [docs-img]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K 10 | 11 | Preserve contextual coherence among trace data from concurrent tasks. 12 | 13 | # Overview 14 | 15 | [`tracing`] is a framework for instrumenting programs to collect structured 16 | and async-aware diagnostics via the `Subscriber` trait. The 17 | [`tracing-subscriber`] crate provides tools for composing `Subscriber`s 18 | from smaller units. This crate extends [`tracing-subscriber`] by providing 19 | `ForestLayer`, a `Layer` that preserves contextual coherence of trace 20 | data from concurrent tasks when logging. 21 | 22 | This crate is intended for programs running many nontrivial and disjoint 23 | tasks concurrently, like server backends. Unlike other `Subscriber`s 24 | which simply keep track of the context of an event, `tracing-forest` preserves 25 | the contextual coherence when writing logs even in parallel contexts, allowing 26 | readers to easily trace a sequence of events from the same task. 27 | 28 | `tracing-forest` is intended for authoring applications. 29 | 30 | [`tracing`]: https://crates.io/crates/tracing 31 | [`tracing-subscriber`]: https://crates.io/crates/tracing-subscriber 32 | 33 | # Getting started 34 | 35 | The easiest way to get started is to enable all features. Do this by 36 | adding the following to your `Cargo.toml` file: 37 | ```toml 38 | tracing-forest = { version = "0.1.6", features = ["full"] } 39 | ``` 40 | Then, add `tracing_forest::init` to your main function: 41 | ```rust 42 | fn main() { 43 | tracing_forest::init(); 44 | // ... 45 | } 46 | ``` 47 | 48 | # Contextual coherence in action 49 | 50 | Similar to this crate, the `tracing-tree` crate collects and writes trace 51 | data as a tree. Unlike this crate, it doesn't maintain contextual coherence 52 | in parallel contexts. 53 | 54 | Observe the below program, which simulates serving multiple clients concurrently. 55 | ```rust 56 | use tracing::info; 57 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Registry}; 58 | use tracing_tree::HierarchicalLayer; 59 | 60 | #[tracing::instrument] 61 | async fn conn(id: u32) { 62 | for i in 0..3 { 63 | some_expensive_operation().await; 64 | info!(id, "step {}", i); 65 | } 66 | } 67 | 68 | #[tokio::main(flavor = "multi_thread")] 69 | async fn main() { 70 | // Use a `tracing-tree` subscriber 71 | Registry::default() 72 | .with(HierarchicalLayer::default()) 73 | .init(); 74 | 75 | let connections: Vec<_> = (0..3) 76 | .map(|id| tokio::spawn(conn(id))) 77 | .collect(); 78 | 79 | for conn in connections { 80 | conn.await.unwrap(); 81 | } 82 | } 83 | ``` 84 | `tracing-tree` isn't intended for concurrent use, and this is demonstrated 85 | by the output of the program: 86 | ```log 87 | conn id=2 88 | conn id=0 89 | conn id=1 90 | 23ms INFO step 0, id=2 91 | 84ms INFO step 0, id=1 92 | 94ms INFO step 1, id=2 93 | 118ms INFO step 0, id=0 94 | 130ms INFO step 1, id=1 95 | 193ms INFO step 2, id=2 96 | 97 | 217ms INFO step 1, id=0 98 | 301ms INFO step 2, id=1 99 | 100 | 326ms INFO step 2, id=0 101 | 102 | ``` 103 | We can instead use `tracing-forest` as a drop-in replacement for `tracing-tree`. 104 | ```rust 105 | use tracing::info; 106 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Registry}; 107 | use tracing_forest::ForestLayer; 108 | 109 | #[tracing::instrument] 110 | async fn conn(id: u32) { 111 | // -- snip -- 112 | } 113 | 114 | #[tokio::main(flavor = "multi_thread")] 115 | async fn main() { 116 | // Use a `tracing-forest` subscriber 117 | Registry::default() 118 | .with(ForestLayer::default()) 119 | .init(); 120 | 121 | // -- snip -- 122 | } 123 | ``` 124 | Now we can easily trace what happened: 125 | ```log 126 | INFO conn [ 150µs | 100.00% ] id: 1 127 | INFO ┝━ i [info]: step 0 | id: 1 128 | INFO ┝━ i [info]: step 1 | id: 1 129 | INFO ┕━ i [info]: step 2 | id: 1 130 | INFO conn [ 343µs | 100.00% ] id: 0 131 | INFO ┝━ i [info]: step 0 | id: 0 132 | INFO ┝━ i [info]: step 1 | id: 0 133 | INFO ┕━ i [info]: step 2 | id: 0 134 | INFO conn [ 233µs | 100.00% ] id: 2 135 | INFO ┝━ i [info]: step 0 | id: 2 136 | INFO ┝━ i [info]: step 1 | id: 2 137 | INFO ┕━ i [info]: step 2 | id: 2 138 | ``` 139 | 140 | ## License 141 | `tracing-forest` is open-source software, distributed under the MIT license. 142 | -------------------------------------------------------------------------------- /tracing-forest-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tracing-forest-macros" 3 | version = "0.1.3" 4 | authors = ["Quinn Okabayashi"] 5 | edition = "2018" 6 | description = "Macros for tracing-forest" 7 | license = "MIT" 8 | repository = "https://github.com/QnnOkabayashi/tracing-forest" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [features] 14 | sync = [] 15 | derive = ["syn/derive"] 16 | attributes = [] 17 | 18 | [dependencies] 19 | quote = "1.0" 20 | proc-macro2 = "1.0" 21 | 22 | [dependencies.syn] 23 | version = "1.0" 24 | features = ["parsing"] 25 | 26 | [package.metadata.docs.rs] 27 | all-features = true 28 | 29 | [dev-dependencies] 30 | tracing-forest = { path = "../tracing-forest", features = ["full"] } 31 | tokio = { version = "1", features = ["sync", "rt", "macros"] } 32 | tracing = "0.1" -------------------------------------------------------------------------------- /tracing-forest-macros/README.md: -------------------------------------------------------------------------------- 1 | # tracing-forest-macros 2 | 3 | Macros for working with `tracing-forest` 4 | 5 | ## License 6 | `tracing-forest` is open-source software, distributed under the MIT license. -------------------------------------------------------------------------------- /tracing-forest-macros/src/attribute.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | use syn::parse::Parser; 4 | type AttributeArgs = syn::punctuated::Punctuated; 5 | 6 | fn token_stream_to_compile_err(mut tokens: TokenStream, err: syn::Error) -> TokenStream { 7 | tokens.extend(TokenStream::from(err.into_compile_error())); 8 | tokens 9 | } 10 | 11 | pub fn main(args: TokenStream, item: TokenStream) -> TokenStream { 12 | let input: syn::ItemFn = match syn::parse(item.clone()) { 13 | Ok(input) => input, 14 | Err(e) => return token_stream_to_compile_err(item, e), 15 | }; 16 | 17 | impl_attribute(input, args, false).unwrap_or_else(|e| token_stream_to_compile_err(item, e)) 18 | } 19 | 20 | pub fn test(args: TokenStream, item: TokenStream) -> TokenStream { 21 | let input: syn::ItemFn = match syn::parse(item.clone()) { 22 | Ok(input) => input, 23 | Err(e) => return token_stream_to_compile_err(item, e), 24 | }; 25 | 26 | if let Some(attr) = input.attrs.iter().find(|attr| attr.path.is_ident("test")) { 27 | let msg = "Second #[test] attribute is supplied"; 28 | return token_stream_to_compile_err(item, syn::Error::new_spanned(&attr, msg)); 29 | } 30 | 31 | impl_attribute(input, args, true).unwrap_or_else(|e| token_stream_to_compile_err(item, e)) 32 | } 33 | 34 | #[cfg(feature = "sync")] 35 | fn ident(string: &str) -> proc_macro2::Ident { 36 | proc_macro2::Ident::new(string, proc_macro2::Span::call_site()) 37 | } 38 | 39 | #[cfg(feature = "sync")] 40 | fn tokio_attribute_path(is_test: bool) -> syn::Path { 41 | let mut segments = syn::punctuated::Punctuated::new(); 42 | segments.push(ident("tokio").into()); 43 | segments.push(ident(if is_test { "test" } else { "main" }).into()); 44 | 45 | syn::Path { 46 | leading_colon: None, 47 | segments, 48 | } 49 | } 50 | 51 | fn impl_attribute( 52 | input: syn::ItemFn, 53 | args: TokenStream, 54 | is_test: bool, 55 | ) -> syn::Result { 56 | if !input.sig.inputs.is_empty() { 57 | let msg = "Cannot accept arguments"; 58 | return Err(syn::Error::new_spanned(&input.sig.ident, msg)); 59 | } 60 | 61 | let args = AttributeArgs::parse_terminated.parse(args)?; 62 | 63 | if let Some(_async) = input.sig.asyncness { 64 | #[cfg(not(feature = "sync"))] 65 | return Err(syn::Error::new_spanned( 66 | _async, 67 | "feature `sync` required for async functions", 68 | )); 69 | 70 | #[cfg(feature = "sync")] 71 | { 72 | let path = tokio_attribute_path(is_test); 73 | 74 | if !input.attrs.iter().any(|attr| attr.path == path) { 75 | let msg = if is_test { 76 | "Attribute must be succeeded by #[tokio::test] for async tests" 77 | } else { 78 | "Attribute must be succeeded by #[tokio::main] for async functions" 79 | }; 80 | return Err(syn::Error::new_spanned(args, msg)); 81 | } 82 | 83 | impl_async(Config::parse(args, is_test)?, input) 84 | } 85 | } else { 86 | impl_sync(Config::parse(args, is_test)?, input) 87 | } 88 | } 89 | 90 | #[cfg(feature = "sync")] 91 | fn impl_async(config: Config, mut input: syn::ItemFn) -> syn::Result { 92 | let builder = config.builder(); 93 | 94 | let brace_token = input.block.brace_token; 95 | let block = input.block; 96 | input.block = syn::parse2(quote! { 97 | { 98 | #builder.async_layer().on_future(async #block).await 99 | } 100 | }) 101 | .expect("Parsing failure"); 102 | input.block.brace_token = brace_token; 103 | 104 | Ok(quote! { #input }.into()) 105 | } 106 | 107 | fn impl_sync(config: Config, mut input: syn::ItemFn) -> syn::Result { 108 | let header = if config.is_test { 109 | quote! { #[::core::prelude::v1::test] } 110 | } else { 111 | quote! {} 112 | }; 113 | 114 | let builder = config.builder(); 115 | 116 | let brace_token = input.block.brace_token; 117 | let block = input.block; 118 | input.block = syn::parse2(quote! { 119 | { 120 | #builder.blocking_layer().on_closure(|| #block) 121 | } 122 | }) 123 | .expect("Parsing failure"); 124 | input.block.brace_token = brace_token; 125 | 126 | Ok(quote! { 127 | #header 128 | #input 129 | } 130 | .into()) 131 | } 132 | 133 | enum Formatter { 134 | Json, 135 | Pretty, 136 | } 137 | 138 | impl ToTokens for Formatter { 139 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 140 | tokens.extend(match self { 141 | Formatter::Json => quote! { .json() }, 142 | Formatter::Pretty => quote! { .pretty() }, 143 | }) 144 | } 145 | } 146 | 147 | struct Config { 148 | formatter: Option, 149 | tag: Option, 150 | is_test: bool, 151 | } 152 | 153 | impl Config { 154 | fn new(is_test: bool) -> Self { 155 | Config { 156 | formatter: None, 157 | tag: None, 158 | is_test, 159 | } 160 | } 161 | 162 | fn parse(args: AttributeArgs, is_test: bool) -> syn::Result { 163 | let mut config = Config::new(is_test); 164 | 165 | for arg in args { 166 | match arg { 167 | syn::NestedMeta::Meta(syn::Meta::NameValue(namevalue)) => { 168 | let ident = namevalue 169 | .path 170 | .get_ident() 171 | .ok_or_else(|| { 172 | syn::Error::new_spanned(&namevalue, "Must have a specified ident") 173 | })? 174 | .to_string() 175 | .to_lowercase(); 176 | match ident.as_str() { 177 | "tag" => config.set_tag(&namevalue)?, 178 | "fmt" => config.set_formatter(&namevalue)?, 179 | name => { 180 | let message = format!( 181 | "Unknown argument `{}` is specified; expected one of: `tag`, `fmt`", 182 | name, 183 | ); 184 | return Err(syn::Error::new_spanned(namevalue, message)); 185 | } 186 | } 187 | } 188 | other => { 189 | return Err(syn::Error::new_spanned( 190 | other, 191 | "Unknown argument inside the macro", 192 | )); 193 | } 194 | } 195 | } 196 | 197 | Ok(config) 198 | } 199 | 200 | fn set_formatter(&mut self, namevalue: &syn::MetaNameValue) -> syn::Result<()> { 201 | if self.formatter.is_some() { 202 | Err(syn::Error::new_spanned( 203 | namevalue, 204 | "Argument `fmt` is defined multiple times", 205 | )) 206 | } else if let syn::Lit::Str(ref s) = namevalue.lit { 207 | match s.value().as_str() { 208 | "json" => self.formatter = Some(Formatter::Json), 209 | "pretty" => self.formatter = Some(Formatter::Pretty), 210 | value => { 211 | let msg = format!( 212 | "Argument `fmt` expects either `pretty` or `json`, but found: `{}`", 213 | value 214 | ); 215 | return Err(syn::Error::new_spanned(&namevalue.lit, msg)); 216 | } 217 | } 218 | Ok(()) 219 | } else { 220 | Err(syn::Error::new_spanned( 221 | &namevalue.lit, 222 | "Argument `fmt` expects a string literal value", 223 | )) 224 | } 225 | } 226 | 227 | fn set_tag(&mut self, namevalue: &syn::MetaNameValue) -> syn::Result<()> { 228 | if self.tag.is_some() { 229 | Err(syn::Error::new_spanned( 230 | namevalue, 231 | "Argument `tag` is defined multiple times", 232 | )) 233 | } else if let syn::Lit::Str(s) = &namevalue.lit { 234 | let ident = proc_macro2::Ident::new(s.value().as_str(), s.span()); 235 | self.tag = Some(ident); 236 | Ok(()) 237 | } else { 238 | Err(syn::Error::new_spanned( 239 | namevalue, 240 | "Argument `tag` expects a string literal value", 241 | )) 242 | } 243 | } 244 | 245 | fn builder(self) -> proc_macro2::TokenStream { 246 | let mut builder = quote! { ::tracing_forest::builder() }; 247 | 248 | if let Some(formatter) = self.formatter { 249 | builder = quote! { #builder #formatter } 250 | } 251 | 252 | if self.is_test { 253 | builder = quote! { #builder.with_test_writer() }; 254 | } 255 | 256 | if let Some(tag) = self.tag { 257 | builder = quote! { #builder.with_tag::() }; 258 | } 259 | 260 | builder 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /tracing-forest-macros/src/derive.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::{quote, ToTokens}; 4 | use syn::parse::{Parse, ParseStream}; 5 | 6 | pub fn tag(input: TokenStream) -> TokenStream { 7 | let input = syn::parse_macro_input!(input as syn::DeriveInput); 8 | // Check that it's visible at the crate level. 9 | if let syn::Visibility::Restricted(vis) = &input.vis { 10 | if syn::parse_str::("crate").expect("Parsing failure") == *vis.path { 11 | let res = match &input.data { 12 | syn::Data::Struct(data) => impl_struct(data, &input), 13 | syn::Data::Enum(data) => impl_enum(data, &input), 14 | syn::Data::Union(_) => Err(syn::Error::new_spanned( 15 | input, 16 | "union tags are not supported", 17 | )), 18 | }; 19 | 20 | return res.unwrap_or_else(|err| err.to_compile_error()).into(); 21 | } 22 | } 23 | syn::Error::new_spanned(input.vis, "must be visible in the crate, use `pub(crate)`") 24 | .to_compile_error() 25 | .into() 26 | } 27 | 28 | #[derive(Clone, Copy)] 29 | enum Level { 30 | Trace, 31 | Debug, 32 | Info, 33 | Warn, 34 | Error, 35 | } 36 | 37 | impl Parse for Level { 38 | fn parse(input: ParseStream) -> syn::Result { 39 | let ident = input.parse::()?; 40 | match ident.to_string().as_str() { 41 | "trace" => Ok(Level::Trace), 42 | "debug" => Ok(Level::Debug), 43 | "info" => Ok(Level::Info), 44 | "warn" => Ok(Level::Warn), 45 | "error" => Ok(Level::Error), 46 | value => { 47 | let message = format!("invalid level: {}", value); 48 | Err(syn::Error::new_spanned(ident, message)) 49 | } 50 | } 51 | } 52 | } 53 | 54 | impl Level { 55 | fn quote(&self) -> TokenStream2 { 56 | match self { 57 | Level::Trace => quote! { trace }, 58 | Level::Debug => quote! { debug }, 59 | Level::Info => quote! { info }, 60 | Level::Warn => quote! { warn }, 61 | Level::Error => quote! { error }, 62 | } 63 | } 64 | 65 | fn quote_icon(&self) -> TokenStream2 { 66 | let constant = match self { 67 | Level::Trace => quote! { TRACE_ICON }, 68 | Level::Debug => quote! { DEBUG_ICON }, 69 | Level::Info => quote! { INFO_ICON }, 70 | Level::Warn => quote! { WARN_ICON }, 71 | Level::Error => quote! { ERROR_ICON }, 72 | }; 73 | quote! { ::tracing_forest::private::#constant } 74 | } 75 | } 76 | 77 | struct TagRepr { 78 | level: Level, 79 | icon: Option, 80 | message: syn::LitStr, 81 | tag_macro: Option, 82 | } 83 | 84 | struct TagMacro { 85 | ident: syn::Ident, 86 | variant_path: TokenStream2, 87 | } 88 | 89 | impl ToTokens for TagRepr { 90 | fn to_tokens(&self, tokens: &mut TokenStream2) { 91 | let message = &self.message; 92 | let icon = self 93 | .icon 94 | .as_ref() 95 | .map(|icon| quote! { #icon }) 96 | .unwrap_or_else(|| { 97 | let level = self.level.quote_icon(); 98 | quote! { #level } 99 | }); 100 | 101 | quote! { ::tracing_forest::tag::TagData { message: #message, icon: #icon } } 102 | .to_tokens(tokens) 103 | } 104 | } 105 | 106 | impl TagRepr { 107 | fn declare_macro(&self) -> Option { 108 | self.tag_macro.as_ref().map(|tag_macro| { 109 | let TagMacro { 110 | ident, 111 | variant_path, 112 | } = tag_macro; 113 | let level = self.level.quote(); 114 | 115 | quote! { 116 | macro_rules! #ident { 117 | ($tokens:tt) => { 118 | ::tracing::#level!( 119 | __event_tag = ::tracing_forest::Tag::as_field( 120 | &$crate::tracing_forest_tag::#variant_path 121 | ), 122 | $tokens 123 | ) 124 | }; 125 | } 126 | } 127 | }) 128 | } 129 | } 130 | 131 | fn parse_tag_attr( 132 | span: &dyn ToTokens, 133 | fields: &syn::Fields, 134 | attrs: &[syn::Attribute], 135 | path: TokenStream2, 136 | ) -> syn::Result { 137 | if !matches!(fields, syn::Fields::Unit) { 138 | return Err(syn::Error::new_spanned(fields, "expected unit type")); 139 | } 140 | 141 | let mut tag = None; 142 | 143 | for attr in attrs.iter() { 144 | if !attr.path.is_ident("tag") { 145 | continue; 146 | } 147 | 148 | if tag.is_some() { 149 | return Err(syn::Error::new_spanned( 150 | attr, 151 | "cannot have multiple #[tag(...)] attributes on same item", 152 | )); 153 | } 154 | 155 | if let syn::Meta::List(list) = attr.parse_meta()? { 156 | let mut lvl = None; 157 | let mut icon = None; 158 | let mut msg = None; 159 | let mut tag_macro = None; 160 | for field in list.nested.iter() { 161 | match field { 162 | syn::NestedMeta::Meta(syn::Meta::NameValue(namevalue)) => { 163 | let ident = namevalue 164 | .path 165 | .get_ident() 166 | .ok_or_else(|| { 167 | syn::Error::new_spanned(&namevalue, "Must have a specified ident") 168 | })? 169 | .to_string() 170 | .to_lowercase(); 171 | match ident.as_str() { 172 | "icon" => { 173 | if icon.is_some() { 174 | return Err(syn::Error::new_spanned( 175 | namevalue, 176 | "defined `icon` multiple times", 177 | )); 178 | } else if let syn::Lit::Char(litchar) = &namevalue.lit { 179 | icon = Some(litchar.clone()); 180 | } else { 181 | return Err(syn::Error::new_spanned( 182 | namevalue.lit.clone(), 183 | "`icon` accepts a char argument", 184 | )); 185 | } 186 | } 187 | "msg" => { 188 | if msg.is_some() { 189 | return Err(syn::Error::new_spanned( 190 | namevalue, 191 | "defined `msg` multiple times", 192 | )); 193 | } else if let syn::Lit::Str(litstr) = &namevalue.lit { 194 | msg = Some(litstr.clone()); 195 | } else { 196 | return Err(syn::Error::new_spanned( 197 | namevalue.lit.clone(), 198 | "`msg` accepts a string literal argument", 199 | )); 200 | } 201 | } 202 | "lvl" => { 203 | if lvl.is_some() { 204 | return Err(syn::Error::new_spanned( 205 | namevalue, 206 | "defined `lvl` multiple times", 207 | )); 208 | } else if let syn::Lit::Str(litstr) = &namevalue.lit { 209 | match litstr.value().as_str() { 210 | "trace" => lvl = Some(Level::Trace), 211 | "debug" => lvl = Some(Level::Debug), 212 | "info" => lvl = Some(Level::Info), 213 | "warn" => lvl = Some(Level::Warn), 214 | "error" => lvl = Some(Level::Error), 215 | _ => { 216 | return Err(syn::Error::new_spanned( 217 | namevalue.lit.clone(), 218 | r#"`lvl` accepts either "trace", "debug", "info", "warn", or "error""#, 219 | )) 220 | } 221 | } 222 | } else { 223 | return Err(syn::Error::new_spanned( 224 | namevalue.lit.clone(), 225 | "`lvl` accepts a string literal argument", 226 | )); 227 | } 228 | } 229 | "macro" => { 230 | if tag_macro.is_some() { 231 | return Err(syn::Error::new_spanned( 232 | namevalue, 233 | "defined `macro` multiple times", 234 | )); 235 | } else if let syn::Lit::Str(litstr) = &namevalue.lit { 236 | match syn::parse_str::(&litstr.value()) { 237 | Ok(mut ident) => { 238 | ident.set_span(litstr.span()); 239 | let path = path.clone(); 240 | tag_macro = Some(TagMacro { ident, variant_path: path }); 241 | } 242 | Err(_) => return Err(syn::Error::new_spanned( 243 | litstr, 244 | "`macro` requires a string literal of a valid ident, received an invalid ident", 245 | )), 246 | } 247 | } else { 248 | return Err(syn::Error::new_spanned( 249 | namevalue.lit.clone(), 250 | "`macro` requires an ident as a string literal argument", 251 | )); 252 | } 253 | } 254 | name => { 255 | let message = format!( 256 | "Unknown argument `{}` is specified; expected one of: `lvl`, `msg`, `icon`, or `macro`", 257 | name, 258 | ); 259 | return Err(syn::Error::new_spanned(namevalue, message)); 260 | } 261 | } 262 | } 263 | other => { 264 | return Err(syn::Error::new_spanned( 265 | other, 266 | r#"#[tag(..)] only accepts named arguments with literal values, try #[tag(level = "..", msg = "..")]"#, 267 | )) 268 | } 269 | } 270 | } 271 | 272 | if let (Some(level), Some(message)) = (lvl, msg) { 273 | tag = Some(TagRepr { 274 | level, 275 | icon, 276 | message, 277 | tag_macro, 278 | }); 279 | } else { 280 | return Err(syn::Error::new_spanned( 281 | span, 282 | "`lvl` and `msg` are required fields", 283 | )); 284 | } 285 | } else { 286 | return Err(syn::Error::new_spanned( 287 | span, 288 | r#"#[tag(..)] attribute expects a list of arguments, try #[tag(icon = "..", msg = "..")]"#, 289 | )); 290 | } 291 | } 292 | 293 | tag.ok_or_else(|| syn::Error::new_spanned(span, "missing #[tag(..)] attribute")) 294 | } 295 | 296 | fn impl_struct(data: &syn::DataStruct, input: &syn::DeriveInput) -> syn::Result { 297 | let ident = &input.ident; 298 | let tag = parse_tag_attr(input, &data.fields, &input.attrs, quote! { #ident })?; 299 | 300 | let into_arms = quote! { _ => 0, }; 301 | let from_arms = quote! { 0 => #tag, }; 302 | 303 | let impl_trait = impl_trait(&input.ident, into_arms, from_arms); 304 | let declare_macro = tag.declare_macro().unwrap_or_else(|| quote! {}); 305 | Ok(quote! { 306 | #impl_trait 307 | #declare_macro 308 | }) 309 | } 310 | 311 | fn impl_enum(data: &syn::DataEnum, input: &syn::DeriveInput) -> syn::Result { 312 | let ident = &input.ident; 313 | let tags = data 314 | .variants 315 | .iter() 316 | .map(|variant| { 317 | let var_ident = &variant.ident; 318 | parse_tag_attr( 319 | variant, 320 | &variant.fields, 321 | &variant.attrs, 322 | quote! { #ident::#var_ident }, 323 | ) 324 | }) 325 | .collect::>>()?; 326 | 327 | let len = data.variants.len(); 328 | let variant_names = data.variants.iter().map(|v| &v.ident); 329 | let ids = 0..len as u64; 330 | let into_arms = quote! { #( Self::#variant_names => #ids, )* }; 331 | 332 | let ids = 0..len as u64; 333 | let from_arms = quote! { #( #ids => #tags, )* }; 334 | 335 | let impl_trait = impl_trait(&input.ident, into_arms, from_arms); 336 | let declare_macros = tags.iter().filter_map(TagRepr::declare_macro); 337 | 338 | Ok(quote! { 339 | #impl_trait 340 | #( #declare_macros )* 341 | }) 342 | } 343 | 344 | fn impl_trait( 345 | name: &proc_macro2::Ident, 346 | into_arms: TokenStream2, 347 | from_arms: TokenStream2, 348 | ) -> TokenStream2 { 349 | quote! { 350 | unsafe impl ::tracing_forest::Tag for #name { 351 | fn as_field(&self) -> u64 { 352 | match *self { 353 | #into_arms 354 | } 355 | } 356 | 357 | fn from_field(value: u64) -> ::tracing_forest::tag::TagData { 358 | match value { 359 | #from_arms 360 | _ => panic!("A tag type was set, but an unrecognized tag was sent: {}. Make sure you're using the same tag type, and that you're not using `__event_tag` as a field name for anything except tags.", value), 361 | } 362 | } 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /tracing-forest-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Macros for use with `tracing-forest`. 2 | 3 | #[cfg(any(feature = "attributes", feature = "derive"))] 4 | use proc_macro::TokenStream; 5 | 6 | #[cfg(feature = "attributes")] 7 | mod attribute; 8 | #[cfg(feature = "derive")] 9 | mod derive; 10 | 11 | /// Derive macro generating an implementation of the 12 | /// [`Tag`](../tracing_forest/tag/trait.Tag.html) trait. 13 | /// 14 | /// See [`tag` module documentation](../tracing_forest/tag/index.html) 15 | /// for details on how to define and use tags. 16 | #[cfg(feature = "derive")] 17 | #[proc_macro_derive(Tag, attributes(tag))] 18 | pub fn tag(input: TokenStream) -> TokenStream { 19 | derive::tag(input) 20 | } 21 | 22 | /// Marks test to run in the context of a 23 | /// [`TreeLayer`](../tracing_forest/layer/struct.TreeLayer.html) 24 | /// subscriber, suitable to test environment. 25 | /// 26 | /// For more configuration options, see the 27 | /// [`builder` module documentation](../tracing_forest/builder/index.html) 28 | /// 29 | /// # Examples 30 | /// 31 | /// By default, logs are pretty-printed to stdout. 32 | /// 33 | /// ``` 34 | /// #[tracing_forest::test] 35 | /// fn test_subscriber() { 36 | /// tracing::info!("Hello, world!"); 37 | /// } 38 | /// ``` 39 | /// ```log 40 | /// INFO 💬 [info]: Hello, world! 41 | /// ``` 42 | /// Equivalent code not using `#[tracing_forest::test]` 43 | /// ``` 44 | /// #[test] 45 | /// fn test_subscriber() { 46 | /// tracing_forest::builder() 47 | /// .with_test_writer() 48 | /// .blocking_layer() 49 | /// .on_closure(|| { 50 | /// tracing::info!("Hello, world!"); 51 | /// }) 52 | /// } 53 | /// ``` 54 | /// 55 | /// ### Tags and formatting 56 | /// 57 | /// Custom tags and formatting can be configured with the `tag` and `fmt` 58 | /// arguments respectively. Currently, only `"pretty"` and `"json"` are 59 | /// supported formats, and tag types must implement the 60 | /// [`Tag`](../tracing_forest/tag/trait.Tag.html) trait. 61 | /// 62 | /// ``` 63 | /// tracing_forest::declare_tags! { 64 | /// use tracing_forest::Tag; 65 | /// 66 | /// #[derive(Tag)] 67 | /// pub(crate) enum GreetingTag { 68 | /// #[tag(lvl = "info", msg = "greeting", macro = "greeting")] 69 | /// Greeting, 70 | /// } 71 | /// } 72 | /// 73 | /// #[tracing_forest::test(tag = "GreetingTag", fmt = "json")] 74 | /// fn test_tag_and_formatted() { 75 | /// greeting!("Hello in JSON"); 76 | /// } 77 | /// ``` 78 | /// ```json 79 | /// {"level":"INFO","kind":{"Event":{"tag":,"message":"Hello in JSON","fields":{}}}} 80 | /// ``` 81 | /// Equivalent code not using `#[tracing_forest::test]` 82 | /// ``` 83 | /// tracing_forest::declare_tags! { 84 | /// use tracing_forest::Tag; 85 | /// 86 | /// #[derive(Tag)] 87 | /// pub(crate) enum GreetingTag { 88 | /// #[tag(lvl = "info", msg = "greeting", macro = "greeting")] 89 | /// Greeting, 90 | /// } 91 | /// } 92 | /// 93 | /// #[test] 94 | /// fn test_tags_and_formatted() { 95 | /// tracing_forest::builder() 96 | /// .json() 97 | /// .with_test_writer() 98 | /// .with_tag::() 99 | /// .blocking_layer() 100 | /// .on_closure(|| { 101 | /// tracing::info!("Hello, world!"); 102 | /// }) 103 | /// } 104 | /// ``` 105 | /// 106 | /// ### Using with Tokio runtime 107 | /// 108 | /// When the `sync` feature is enabled, this attribute can also be proceeded by 109 | /// the [`#[tokio::test]`] attribute to run the test in the context of an async 110 | /// runtime. 111 | /// ``` 112 | /// #[tracing_forest::test] 113 | /// #[tokio::test] 114 | /// async fn test_tokio() { 115 | /// tracing::info!("Hello from Tokio!"); 116 | /// } 117 | /// ``` 118 | /// ```log 119 | /// INFO 💬 [info]: Hello from Tokio! 120 | /// ``` 121 | /// Equivalent code not using `#[tracing_forest::test]` 122 | /// ``` 123 | /// #[tokio::test] 124 | /// async fn test_tokio() { 125 | /// tracing_forest::builder() 126 | /// .with_test_writer() 127 | /// .async_layer() 128 | /// .on_future(async { 129 | /// tracing::info!("Hello from Tokio!"); 130 | /// }) 131 | /// .await 132 | /// } 133 | /// ``` 134 | #[cfg(feature = "attributes")] 135 | #[proc_macro_attribute] 136 | pub fn test(args: TokenStream, item: TokenStream) -> TokenStream { 137 | attribute::test(args, item) 138 | } 139 | 140 | /// Marks function to run in the context of a 141 | /// [`TreeLayer`](../tracing_forest/layer/struct.TreeLayer.html) subscriber. 142 | /// 143 | /// For more configuration options, see the 144 | /// [`builder` module documentation](../tracing_forest/builder/index.html) 145 | /// 146 | /// # Examples 147 | /// 148 | /// By default, logs are pretty-printed to stdout. 149 | /// 150 | /// ``` 151 | /// # #[allow(clippy::needless_doctest_main)] 152 | /// #[tracing_forest::main] 153 | /// fn main() { 154 | /// tracing::info!("Hello, world!"); 155 | /// } 156 | /// ``` 157 | /// ```log 158 | /// INFO 💬 [info]: Hello, world! 159 | /// ``` 160 | /// Equivalent code not using `#[tracing_forest::main]` 161 | /// ``` 162 | /// # #[allow(clippy::needless_doctest_main)] 163 | /// fn main() { 164 | /// tracing_forest::builder() 165 | /// .blocking_layer() 166 | /// .on_closure(|| { 167 | /// tracing::info!("Hello, world!"); 168 | /// }) 169 | /// } 170 | /// ``` 171 | /// 172 | /// ### Tags and formatting 173 | /// 174 | /// Custom tags and formatting can be configured with the `tag` and `fmt` 175 | /// arguments respectively. Currently, only `"pretty"` and `"json"` are 176 | /// supported formats, and tag types must implement the 177 | /// [`Tag`](../tracing_forest/tag/trait.Tag.html) trait. 178 | /// 179 | /// ``` 180 | /// tracing_forest::declare_tags! { 181 | /// use tracing_forest::Tag; 182 | /// 183 | /// #[derive(Tag)] 184 | /// pub(crate) enum GreetingTag { 185 | /// #[tag(lvl = "info", msg = "greeting", macro = "greeting")] 186 | /// Greeting, 187 | /// } 188 | /// } 189 | /// 190 | /// # #[allow(clippy::needless_doctest_main)] 191 | /// #[tracing_forest::main(tag = "GreetingTag", fmt = "json")] 192 | /// fn main() { 193 | /// greeting!("Hello in JSON"); 194 | /// } 195 | /// ``` 196 | /// ```json 197 | /// {"level":"INFO","kind":{"Event":{"tag":"greeting","message":"Hello in JSON","fields":{}}}} 198 | /// ``` 199 | /// Equivalent code not using `#[tracing_forest::main]` 200 | /// ``` 201 | /// tracing_forest::declare_tags! { 202 | /// use tracing_forest::Tag; 203 | /// 204 | /// #[derive(Tag)] 205 | /// pub(crate) enum GreetingTag { 206 | /// #[tag(lvl = "info", msg = "greeting", macro = "greeting")] 207 | /// Greeting, 208 | /// } 209 | /// } 210 | /// 211 | /// # #[allow(clippy::needless_doctest_main)] 212 | /// fn main() { 213 | /// tracing_forest::builder() 214 | /// .json() 215 | /// .with_tag::() 216 | /// .blocking_layer() 217 | /// .on_closure(|| { 218 | /// greeting!("Hello in JSON"); 219 | /// }) 220 | /// } 221 | /// ``` 222 | /// 223 | /// ### Using with Tokio runtime 224 | /// 225 | /// When the `sync` feature is enabled, this attribute can also be proceeded by 226 | /// the [`#[tokio::main]`] attribute to run the function in the 227 | /// context of an async runtime. 228 | /// ``` 229 | /// #[tracing_forest::main] 230 | /// #[tokio::main(flavor = "current_thread")] 231 | /// async fn main() { 232 | /// tracing::info!("Hello from Tokio!"); 233 | /// } 234 | /// ``` 235 | /// ```log 236 | /// INFO 💬 [info]: Hello from Tokio! 237 | /// ``` 238 | /// Equivalent code not using `#[tracing_forest::main]` 239 | /// ``` 240 | /// # #[allow(clippy::needless_doctest_main)] 241 | /// #[tokio::main(flavor = "current_thread")] 242 | /// async fn main() { 243 | /// tracing_forest::builder() 244 | /// .async_layer() 245 | /// .on_future(async { 246 | /// tracing::info!("Hello from Tokio!"); 247 | /// }) 248 | /// .await 249 | /// } 250 | /// ``` 251 | #[cfg(feature = "attributes")] 252 | #[proc_macro_attribute] 253 | pub fn main(args: TokenStream, item: TokenStream) -> TokenStream { 254 | attribute::main(args, item) 255 | } 256 | -------------------------------------------------------------------------------- /tracing-forest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tracing-forest" 3 | version = "0.1.6" 4 | authors = ["Quinn Okabayashi"] 5 | edition = "2018" 6 | description = "Preserving contextual coherence among trace data from concurrent tasks" 7 | keywords = ["tracing", "async", "tokio", "tracing-subscriber", "logging"] 8 | categories = [ 9 | "asynchronous", 10 | "development-tools::debugging", 11 | "development-tools::profiling", 12 | ] 13 | license = "MIT" 14 | repository = "https://github.com/QnnOkabayashi/tracing-forest" 15 | 16 | [features] 17 | default = ["smallvec"] 18 | full = ["uuid", "chrono", "smallvec", "tokio", "serde", "env-filter", "ansi"] 19 | env-filter = ["tracing-subscriber/env-filter"] 20 | ansi = ["ansi_term"] 21 | 22 | [dependencies] 23 | tracing = "0.1" 24 | tracing-subscriber = "0.3" 25 | thiserror = "2.0.12" 26 | 27 | [dependencies.uuid] 28 | version = "1" 29 | features = ["v4", "serde"] 30 | optional = true 31 | 32 | [dependencies.chrono] 33 | version = "0.4" 34 | optional = true 35 | 36 | [dependencies.smallvec] 37 | version = "1.7" 38 | features = ["write"] 39 | optional = true 40 | 41 | [dependencies.tokio] 42 | version = "1" 43 | features = ["sync", "rt", "macros", "time"] 44 | optional = true 45 | 46 | [dependencies.serde] 47 | version = "1.0" 48 | features = ["derive"] 49 | optional = true 50 | 51 | [dependencies.ansi_term] 52 | version = "0.12" 53 | optional = true 54 | 55 | [dev-dependencies] 56 | tracing-forest = { path = ".", features = ["full"] } 57 | rand = "0.8.4" 58 | tokio = { version = "1", features = ["full"] } 59 | serde_json = "1.0" 60 | 61 | [package.metadata.docs.rs] 62 | all-features = true 63 | -------------------------------------------------------------------------------- /tracing-forest/README.md: -------------------------------------------------------------------------------- 1 | # tracing-forest 2 | [![github-img]][github-url] [![crates-img]][crates-url] [![docs-img]][docs-url] 3 | 4 | [github-url]: https://github.com/QnnOkabayashi/tracing-forest 5 | [crates-url]: https://crates.io/crates/tracing-forest 6 | [docs-url]: https://docs.rs/tracing-forest/latest/tracing_forest/ 7 | [github-img]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github 8 | [crates-img]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust 9 | [docs-img]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K 10 | 11 | Preserve contextual coherence among trace data from concurrent tasks. 12 | 13 | # Overview 14 | 15 | [`tracing`] is a framework for instrumenting programs to collect structured 16 | and async-aware diagnostics via the `Subscriber` trait. The 17 | [`tracing-subscriber`] crate provides tools for composing `Subscriber`s 18 | from smaller units. This crate extends [`tracing-subscriber`] by providing 19 | `ForestLayer`, a `Layer` that preserves contextual coherence of trace 20 | data from concurrent tasks when logging. 21 | 22 | This crate is intended for programs running many nontrivial and disjoint 23 | tasks concurrently, like server backends. Unlike other `Subscriber`s 24 | which simply keep track of the context of an event, `tracing-forest` preserves 25 | the contextual coherence when writing logs even in parallel contexts, allowing 26 | readers to easily trace a sequence of events from the same task. 27 | 28 | `tracing-forest` is intended for authoring applications. 29 | 30 | [`tracing`]: https://crates.io/crates/tracing 31 | [`tracing-subscriber`]: https://crates.io/crates/tracing-subscriber 32 | 33 | # Getting started 34 | 35 | The easiest way to get started is to enable all features. Do this by 36 | adding the following to your `Cargo.toml` file: 37 | ```toml 38 | tracing-forest = { version = "0.1.6", features = ["full"] } 39 | ``` 40 | Then, add `tracing_forest::init` to your main function: 41 | ```rust 42 | fn main() { 43 | tracing_forest::init(); 44 | // ... 45 | } 46 | ``` 47 | 48 | # Contextual coherence in action 49 | 50 | Similar to this crate, the `tracing-tree` crate collects and writes trace 51 | data as a tree. Unlike this crate, it doesn't maintain contextual coherence 52 | in parallel contexts. 53 | 54 | Observe the below program, which simulates serving multiple clients concurrently. 55 | ```rust 56 | use tracing::info; 57 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Registry}; 58 | use tracing_tree::HierarchicalLayer; 59 | 60 | #[tracing::instrument] 61 | async fn conn(id: u32) { 62 | for i in 0..3 { 63 | some_expensive_operation().await; 64 | info!(id, "step {}", i); 65 | } 66 | } 67 | 68 | #[tokio::main(flavor = "multi_thread")] 69 | async fn main() { 70 | // Use a `tracing-tree` subscriber 71 | Registry::default() 72 | .with(HierarchicalLayer::default()) 73 | .init(); 74 | 75 | let connections: Vec<_> = (0..3) 76 | .map(|id| tokio::spawn(conn(id))) 77 | .collect(); 78 | 79 | for conn in connections { 80 | conn.await.unwrap(); 81 | } 82 | } 83 | ``` 84 | `tracing-tree` isn't intended for concurrent use, and this is demonstrated 85 | by the output of the program: 86 | ```log 87 | conn id=2 88 | conn id=0 89 | conn id=1 90 | 23ms INFO step 0, id=2 91 | 84ms INFO step 0, id=1 92 | 94ms INFO step 1, id=2 93 | 118ms INFO step 0, id=0 94 | 130ms INFO step 1, id=1 95 | 193ms INFO step 2, id=2 96 | 97 | 217ms INFO step 1, id=0 98 | 301ms INFO step 2, id=1 99 | 100 | 326ms INFO step 2, id=0 101 | 102 | ``` 103 | We can instead use `tracing-forest` as a drop-in replacement for `tracing-tree`. 104 | ```rust 105 | use tracing::info; 106 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Registry}; 107 | use tracing_forest::ForestLayer; 108 | 109 | #[tracing::instrument] 110 | async fn conn(id: u32) { 111 | // -- snip -- 112 | } 113 | 114 | #[tokio::main(flavor = "multi_thread")] 115 | async fn main() { 116 | // Use a `tracing-forest` subscriber 117 | Registry::default() 118 | .with(ForestLayer::default()) 119 | .init(); 120 | 121 | // -- snip -- 122 | } 123 | ``` 124 | Now we can easily trace what happened: 125 | ```log 126 | INFO conn [ 150µs | 100.00% ] id: 1 127 | INFO ┝━ i [info]: step 0 | id: 1 128 | INFO ┝━ i [info]: step 1 | id: 1 129 | INFO ┕━ i [info]: step 2 | id: 1 130 | INFO conn [ 343µs | 100.00% ] id: 0 131 | INFO ┝━ i [info]: step 0 | id: 0 132 | INFO ┝━ i [info]: step 1 | id: 0 133 | INFO ┕━ i [info]: step 2 | id: 0 134 | INFO conn [ 233µs | 100.00% ] id: 2 135 | INFO ┝━ i [info]: step 0 | id: 2 136 | INFO ┝━ i [info]: step 1 | id: 2 137 | INFO ┕━ i [info]: step 2 | id: 2 138 | ``` 139 | 140 | ## License 141 | `tracing-forest` is open-source software, distributed under the MIT license. 142 | -------------------------------------------------------------------------------- /tracing-forest/src/cfg.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | #[macro_export] 3 | macro_rules! cfg_tokio { 4 | ($($item:item)*) => { 5 | $( 6 | #[cfg(feature = "tokio")] 7 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] 8 | $item 9 | )* 10 | } 11 | } 12 | 13 | #[doc(hidden)] 14 | #[macro_export] 15 | macro_rules! cfg_serde { 16 | ($($item:item)*) => { 17 | $( 18 | #[cfg(feature = "serde")] 19 | #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] 20 | $item 21 | )* 22 | } 23 | } 24 | 25 | #[doc(hidden)] 26 | #[macro_export] 27 | macro_rules! cfg_uuid { 28 | ($($item:item)*) => { 29 | $( 30 | #[cfg(feature = "uuid")] 31 | #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))] 32 | $item 33 | )* 34 | } 35 | } 36 | 37 | #[doc(hidden)] 38 | #[macro_export] 39 | macro_rules! cfg_chrono { 40 | ($($item:item)*) => { 41 | $( 42 | #[cfg(feature = "chrono")] 43 | #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] 44 | $item 45 | )* 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tracing-forest/src/fail.rs: -------------------------------------------------------------------------------- 1 | use crate::cfg_uuid; 2 | 3 | pub const SPAN_NOT_IN_CONTEXT: &str = "Span not in context, this is a bug"; 4 | pub const OPENED_SPAN_NOT_IN_EXTENSIONS: &str = 5 | "Span extension doesn't contain `OpenedSpan`, this is a bug"; 6 | pub const PROCESSING_ERROR: &str = "Processing logs failed"; 7 | 8 | cfg_uuid! { 9 | pub const NO_CURRENT_SPAN: &str = "The subscriber isn't in any spans"; 10 | pub const NO_FOREST_LAYER: &str = "The span has no `Span` in extensions, perhaps you forgot to add a `ForestLayer` to your subscriber?"; 11 | 12 | #[cold] 13 | #[inline(never)] 14 | pub fn subscriber_not_found<'a, S>() -> &'a S { 15 | panic!( 16 | "Subscriber could not be downcasted to `{}`", 17 | std::any::type_name::() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tracing-forest/src/layer/id.rs: -------------------------------------------------------------------------------- 1 | use crate::fail; 2 | use crate::layer::OpenedSpan; 3 | use tracing::Subscriber; 4 | use tracing_subscriber::{registry::LookupSpan, Registry}; 5 | use uuid::Uuid; 6 | 7 | /// Gets the current [`Uuid`] of an entered span within a `tracing-forest` 8 | /// subscriber. 9 | /// 10 | /// # Examples 11 | /// 12 | /// Passing in a `Uuid` to a span, and then retreiving it from within the span: 13 | /// ``` 14 | /// # use tracing::{info, info_span}; 15 | /// # use uuid::Uuid; 16 | /// # tracing_forest::init(); 17 | /// let uuid = Uuid::new_v4(); 18 | /// 19 | /// // Tracing's syntax allows us to omit the redundent naming of the field here 20 | /// info_span!("my_span", %uuid).in_scope(|| { 21 | /// assert!(tracing_forest::id() == uuid); 22 | /// }); 23 | /// ``` 24 | /// 25 | /// # Panics 26 | /// 27 | /// This function panics if there is no current subscriber, if the subscriber 28 | /// isn't composed with a [`ForestLayer`], or if the subscriber isn't in a span. 29 | /// 30 | /// [`ForestLayer`]: crate::layer::ForestLayer 31 | #[must_use] 32 | pub fn id() -> Uuid { 33 | tracing::dispatcher::get_default(|dispatch| { 34 | let subscriber = dispatch 35 | .downcast_ref::() 36 | .unwrap_or_else(fail::subscriber_not_found); 37 | 38 | let current = subscriber.current_span(); 39 | 40 | let id = current.id().expect(fail::NO_CURRENT_SPAN); 41 | 42 | subscriber 43 | .span(id) 44 | .expect(fail::SPAN_NOT_IN_CONTEXT) 45 | .extensions() 46 | .get::() 47 | .expect(fail::NO_FOREST_LAYER) 48 | .uuid() 49 | }) 50 | } 51 | 52 | // Credit: https://github.com/uuid-rs/uuid/blob/main/src/parser.rs 53 | 54 | pub(crate) const fn try_parse(input: &[u8]) -> Option { 55 | match (input.len(), input) { 56 | // Inputs of 32 bytes must be a non-hyphenated UUID 57 | (32, s) => parse_simple(s), 58 | // Hyphenated UUIDs may be wrapped in various ways: 59 | // - `{UUID}` for braced UUIDs 60 | // - `urn:uuid:UUID` for URNs 61 | // - `UUID` for a regular hyphenated UUID 62 | (36, s) 63 | | (38, [b'{', s @ .., b'}']) 64 | | (45, [b'u', b'r', b'n', b':', b'u', b'u', b'i', b'd', b':', s @ ..]) => { 65 | parse_hyphenated(s) 66 | } 67 | // Any other shaped input is immediately invalid 68 | _ => None, 69 | } 70 | } 71 | 72 | #[inline] 73 | const fn parse_simple(s: &[u8]) -> Option { 74 | // This length check here removes all other bounds 75 | // checks in this function 76 | if s.len() != 32 { 77 | return None; 78 | } 79 | 80 | let mut buf: [u8; 16] = [0; 16]; 81 | let mut i = 0; 82 | 83 | while i < 16 { 84 | // Convert a two-char hex value (like `A8`) 85 | // into a byte (like `10101000`) 86 | let h1 = HEX_TABLE[s[i * 2] as usize]; 87 | let h2 = HEX_TABLE[s[i * 2 + 1] as usize]; 88 | 89 | // We use `0xff` as a sentinel value to indicate 90 | // an invalid hex character sequence (like the letter `G`) 91 | if h1 | h2 == 0xff { 92 | return None; 93 | } 94 | 95 | // The upper nibble needs to be shifted into position 96 | // to produce the final byte value 97 | buf[i] = SHL4_TABLE[h1 as usize] | h2; 98 | i += 1; 99 | } 100 | 101 | Some(Uuid::from_bytes(buf)) 102 | } 103 | 104 | #[inline] 105 | const fn parse_hyphenated(s: &[u8]) -> Option { 106 | // This length check here removes all other bounds 107 | // checks in this function 108 | if s.len() != 36 { 109 | return None; 110 | } 111 | 112 | // We look at two hex-encoded values (4 chars) at a time because 113 | // that's the size of the smallest group in a hyphenated UUID. 114 | // The indexes we're interested in are: 115 | // 116 | // uuid : 936da01f-9abd-4d9d-80c7-02af85c822a8 117 | // | | || || || || | | 118 | // hyphens : | | 8| 13| 18| 23| | | 119 | // positions: 0 4 9 14 19 24 28 32 120 | 121 | // First, ensure the hyphens appear in the right places 122 | match [s[8], s[13], s[18], s[23]] { 123 | [b'-', b'-', b'-', b'-'] => {} 124 | _ => return None, 125 | } 126 | 127 | let positions: [u8; 8] = [0, 4, 9, 14, 19, 24, 28, 32]; 128 | let mut buf: [u8; 16] = [0; 16]; 129 | let mut j = 0; 130 | 131 | while j < 8 { 132 | let i = positions[j]; 133 | 134 | // The decoding here is the same as the simple case 135 | // We're just dealing with two values instead of one 136 | let h1 = HEX_TABLE[s[i as usize] as usize]; 137 | let h2 = HEX_TABLE[s[(i + 1) as usize] as usize]; 138 | let h3 = HEX_TABLE[s[(i + 2) as usize] as usize]; 139 | let h4 = HEX_TABLE[s[(i + 3) as usize] as usize]; 140 | 141 | if h1 | h2 | h3 | h4 == 0xff { 142 | return None; 143 | } 144 | 145 | buf[j * 2] = SHL4_TABLE[h1 as usize] | h2; 146 | buf[j * 2 + 1] = SHL4_TABLE[h3 as usize] | h4; 147 | j += 1; 148 | } 149 | 150 | Some(Uuid::from_bytes(buf)) 151 | } 152 | 153 | const HEX_TABLE: &[u8; 256] = &{ 154 | let mut buf = [0; 256]; 155 | let mut i: u8 = 0; 156 | 157 | loop { 158 | buf[i as usize] = match i { 159 | b'0'..=b'9' => i - b'0', 160 | b'a'..=b'f' => i - b'a' + 10, 161 | b'A'..=b'F' => i - b'A' + 10, 162 | _ => 0xff, 163 | }; 164 | 165 | if i == 255 { 166 | break buf; 167 | } 168 | 169 | i += 1; 170 | } 171 | }; 172 | 173 | const SHL4_TABLE: &[u8; 256] = &{ 174 | let mut buf = [0; 256]; 175 | let mut i: u8 = 0; 176 | 177 | loop { 178 | buf[i as usize] = i.wrapping_shl(4); 179 | 180 | if i == 255 { 181 | break buf; 182 | } 183 | 184 | i += 1; 185 | } 186 | }; 187 | -------------------------------------------------------------------------------- /tracing-forest/src/layer/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::fail; 2 | use crate::printer::{PrettyPrinter, TestCapturePrinter}; 3 | use crate::processor::{Processor, Sink}; 4 | use crate::tag::{NoTag, Tag, TagParser}; 5 | use crate::tree::{self, FieldSet, Tree}; 6 | #[cfg(feature = "chrono")] 7 | use chrono::Utc; 8 | use std::fmt; 9 | use std::io::{self, Write}; 10 | use std::time::Instant; 11 | use tracing::field::{Field, Visit}; 12 | use tracing::span::{Attributes, Id}; 13 | use tracing::{Event, Subscriber}; 14 | use tracing_subscriber::layer::{Context, Layer, SubscriberExt}; 15 | use tracing_subscriber::registry::{LookupSpan, Registry, SpanRef}; 16 | use tracing_subscriber::util::SubscriberInitExt; 17 | use tracing_subscriber::util::TryInitError; 18 | #[cfg(feature = "uuid")] 19 | use uuid::Uuid; 20 | #[cfg(feature = "uuid")] 21 | pub(crate) mod id; 22 | 23 | pub(crate) struct OpenedSpan { 24 | span: tree::Span, 25 | start: Instant, 26 | } 27 | 28 | impl OpenedSpan { 29 | fn new(attrs: &Attributes, _ctx: &Context) -> Self 30 | where 31 | S: Subscriber + for<'a> LookupSpan<'a>, 32 | { 33 | let mut fields = FieldSet::default(); 34 | #[cfg(feature = "uuid")] 35 | let mut maybe_uuid = None; 36 | 37 | attrs.record(&mut |field: &Field, value: &dyn fmt::Debug| { 38 | #[cfg(feature = "uuid")] 39 | if field.name() == "uuid" && maybe_uuid.is_none() { 40 | const LENGTH: usize = 45; 41 | let mut buf = [0u8; LENGTH]; 42 | let mut remaining = &mut buf[..]; 43 | 44 | if let Ok(()) = write!(remaining, "{:?}", value) { 45 | let len = LENGTH - remaining.len(); 46 | if let Some(parsed) = id::try_parse(&buf[..len]) { 47 | maybe_uuid = Some(parsed); 48 | } 49 | } 50 | return; 51 | } 52 | 53 | let value = format!("{:?}", value); 54 | fields.push(tree::Field::new(field.name(), value)); 55 | }); 56 | 57 | let shared = tree::Shared { 58 | #[cfg(feature = "chrono")] 59 | timestamp: Utc::now(), 60 | level: *attrs.metadata().level(), 61 | fields, 62 | #[cfg(feature = "uuid")] 63 | uuid: maybe_uuid.unwrap_or_else(|| match _ctx.lookup_current() { 64 | Some(parent) => parent 65 | .extensions() 66 | .get::() 67 | .expect(fail::OPENED_SPAN_NOT_IN_EXTENSIONS) 68 | .span 69 | .uuid(), 70 | None => Uuid::new_v4(), 71 | }), 72 | }; 73 | 74 | OpenedSpan { 75 | span: tree::Span::new(shared, attrs.metadata().name()), 76 | start: Instant::now(), 77 | } 78 | } 79 | 80 | fn enter(&mut self) { 81 | self.start = Instant::now(); 82 | } 83 | 84 | fn exit(&mut self) { 85 | self.span.total_duration += self.start.elapsed(); 86 | } 87 | 88 | fn close(self) -> tree::Span { 89 | self.span 90 | } 91 | 92 | fn record_event(&mut self, event: tree::Event) { 93 | #[cfg(feature = "uuid")] 94 | let event = { 95 | let mut event = event; 96 | event.shared.uuid = self.span.uuid(); 97 | event 98 | }; 99 | 100 | self.span.nodes.push(Tree::Event(event)); 101 | } 102 | 103 | fn record_span(&mut self, span: tree::Span) { 104 | self.span.inner_duration += span.total_duration(); 105 | self.span.nodes.push(Tree::Span(span)); 106 | } 107 | 108 | #[cfg(feature = "uuid")] 109 | pub(crate) fn uuid(&self) -> Uuid { 110 | self.span.uuid() 111 | } 112 | } 113 | 114 | /// A [`Layer`] that collects and processes trace data while preserving 115 | /// contextual coherence. 116 | #[derive(Clone, Debug)] 117 | pub struct ForestLayer { 118 | processor: P, 119 | tag: T, 120 | } 121 | 122 | impl ForestLayer { 123 | /// Create a new `ForestLayer` from a [`Processor`] and a [`TagParser`]. 124 | pub fn new(processor: P, tag: T) -> Self { 125 | ForestLayer { processor, tag } 126 | } 127 | } 128 | 129 | impl From

for ForestLayer { 130 | fn from(processor: P) -> Self { 131 | ForestLayer::new(processor, NoTag) 132 | } 133 | } 134 | 135 | impl ForestLayer { 136 | /// Create a new `ForestLayer` that does nothing with collected trace data. 137 | pub fn sink() -> Self { 138 | ForestLayer::from(Sink) 139 | } 140 | } 141 | 142 | impl Default for ForestLayer { 143 | fn default() -> Self { 144 | ForestLayer { 145 | processor: PrettyPrinter::new(), 146 | tag: NoTag, 147 | } 148 | } 149 | } 150 | 151 | impl Layer for ForestLayer 152 | where 153 | P: Processor, 154 | T: TagParser, 155 | S: Subscriber + for<'a> LookupSpan<'a>, 156 | { 157 | fn on_new_span(&self, attrs: &Attributes, id: &Id, ctx: Context) { 158 | let span = ctx.span(id).expect(fail::SPAN_NOT_IN_CONTEXT); 159 | let opened = OpenedSpan::new(attrs, &ctx); 160 | 161 | let mut extensions = span.extensions_mut(); 162 | extensions.insert(opened); 163 | } 164 | 165 | fn on_event(&self, event: &Event, ctx: Context) { 166 | struct Visitor { 167 | message: Option, 168 | fields: FieldSet, 169 | immediate: bool, 170 | } 171 | 172 | impl Visit for Visitor { 173 | fn record_bool(&mut self, field: &Field, value: bool) { 174 | match field.name() { 175 | "immediate" => self.immediate |= value, 176 | _ => self.record_debug(field, &value), 177 | } 178 | } 179 | 180 | fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { 181 | let value = format!("{:?}", value); 182 | match field.name() { 183 | "message" if self.message.is_none() => self.message = Some(value), 184 | key => self.fields.push(tree::Field::new(key, value)), 185 | } 186 | } 187 | } 188 | 189 | let mut visitor = Visitor { 190 | message: None, 191 | fields: FieldSet::default(), 192 | immediate: false, 193 | }; 194 | 195 | event.record(&mut visitor); 196 | 197 | let shared = tree::Shared { 198 | #[cfg(feature = "uuid")] 199 | uuid: Uuid::nil(), 200 | #[cfg(feature = "chrono")] 201 | timestamp: Utc::now(), 202 | level: *event.metadata().level(), 203 | fields: visitor.fields, 204 | }; 205 | 206 | let tree_event = tree::Event { 207 | shared, 208 | message: visitor.message, 209 | tag: self.tag.parse(event), 210 | }; 211 | 212 | let current_span = ctx.event_span(event); 213 | 214 | if visitor.immediate { 215 | write_immediate(&tree_event, current_span.as_ref()).expect("writing urgent failed"); 216 | } 217 | 218 | match current_span.as_ref() { 219 | Some(parent) => parent 220 | .extensions_mut() 221 | .get_mut::() 222 | .expect(fail::OPENED_SPAN_NOT_IN_EXTENSIONS) 223 | .record_event(tree_event), 224 | None => self 225 | .processor 226 | .process(Tree::Event(tree_event)) 227 | .expect(fail::PROCESSING_ERROR), 228 | } 229 | } 230 | 231 | fn on_enter(&self, id: &Id, ctx: Context) { 232 | ctx.span(id) 233 | .expect(fail::SPAN_NOT_IN_CONTEXT) 234 | .extensions_mut() 235 | .get_mut::() 236 | .expect(fail::OPENED_SPAN_NOT_IN_EXTENSIONS) 237 | .enter(); 238 | } 239 | 240 | fn on_exit(&self, id: &Id, ctx: Context) { 241 | ctx.span(id) 242 | .expect(fail::SPAN_NOT_IN_CONTEXT) 243 | .extensions_mut() 244 | .get_mut::() 245 | .expect(fail::OPENED_SPAN_NOT_IN_EXTENSIONS) 246 | .exit(); 247 | } 248 | 249 | fn on_close(&self, id: Id, ctx: Context) { 250 | let span_ref = ctx.span(&id).expect(fail::SPAN_NOT_IN_CONTEXT); 251 | 252 | let mut span = span_ref 253 | .extensions_mut() 254 | .remove::() 255 | .expect(fail::OPENED_SPAN_NOT_IN_EXTENSIONS) 256 | .close(); 257 | 258 | // Ensure that the total duration is at least as much as the inner 259 | // duration. This is caused by when a child span is manually passed 260 | // a parent span and then enters without entering the parent span. Also 261 | // when a child span is created within a parent, and then stored and 262 | // entered again when the parent isn't opened. 263 | // 264 | // Issue: https://github.com/QnnOkabayashi/tracing-forest/issues/11 265 | if span.total_duration < span.inner_duration { 266 | span.total_duration = span.inner_duration; 267 | } 268 | 269 | match span_ref.parent() { 270 | Some(parent) => parent 271 | .extensions_mut() 272 | .get_mut::() 273 | .expect(fail::OPENED_SPAN_NOT_IN_EXTENSIONS) 274 | .record_span(span), 275 | None => self 276 | .processor 277 | .process(Tree::Span(span)) 278 | .expect(fail::PROCESSING_ERROR), 279 | } 280 | } 281 | } 282 | 283 | fn write_immediate(event: &tree::Event, current: Option<&SpanRef>) -> io::Result<()> 284 | where 285 | S: for<'a> LookupSpan<'a>, 286 | { 287 | // uuid timestamp LEVEL root > inner > leaf > my message here | key: val 288 | #[cfg(feature = "smallvec")] 289 | let mut writer = smallvec::SmallVec::<[u8; 256]>::new(); 290 | #[cfg(not(feature = "smallvec"))] 291 | let mut writer = Vec::with_capacity(256); 292 | 293 | #[cfg(feature = "uuid")] 294 | if let Some(span) = current { 295 | let uuid = span 296 | .extensions() 297 | .get::() 298 | .expect(fail::OPENED_SPAN_NOT_IN_EXTENSIONS) 299 | .span 300 | .uuid(); 301 | write!(writer, "{} ", uuid)?; 302 | } 303 | 304 | #[cfg(feature = "chrono")] 305 | write!(writer, "{} ", event.timestamp().to_rfc3339())?; 306 | 307 | write!(writer, "{:<8} ", event.level())?; 308 | 309 | let tag = event.tag().unwrap_or_else(|| Tag::from(event.level())); 310 | 311 | write!(writer, "{icon} IMMEDIATE {icon} ", icon = tag.icon())?; 312 | 313 | if let Some(span) = current { 314 | for ancestor in span.scope().from_root() { 315 | write!(writer, "{} > ", ancestor.name())?; 316 | } 317 | } 318 | 319 | // we should just do pretty printing here. 320 | 321 | if let Some(message) = event.message() { 322 | write!(writer, "{}", message)?; 323 | } 324 | 325 | for field in event.fields().iter() { 326 | write!(writer, " | {}: {}", field.key(), field.value())?; 327 | } 328 | 329 | writeln!(writer)?; 330 | 331 | io::stderr().write_all(&writer) 332 | } 333 | 334 | /// Initializes a global subscriber with a [`ForestLayer`] using the default configuration. 335 | /// 336 | /// This function is intended for quick initialization and processes log trees "inline", 337 | /// meaning it doesn't take advantage of a worker task for formatting and writing. 338 | /// To use a worker task, consider using the [`worker_task`] function. Alternatively, 339 | /// configure a `Subscriber` manually using a `ForestLayer`. 340 | /// 341 | /// [`worker_task`]: crate::runtime::worker_task 342 | /// 343 | /// # Examples 344 | /// ``` 345 | /// use tracing::{info, info_span}; 346 | /// 347 | /// tracing_forest::init(); 348 | /// 349 | /// info!("Hello, world!"); 350 | /// info_span!("my_span").in_scope(|| { 351 | /// info!("Relevant information"); 352 | /// }); 353 | /// ``` 354 | /// Produces the the output: 355 | /// ```log 356 | /// INFO i [info]: Hello, world! 357 | /// INFO my_span [ 26.0µs | 100.000% ] 358 | /// INFO ┕━ i [info]: Relevant information 359 | /// ``` 360 | pub fn init() { 361 | Registry::default().with(ForestLayer::default()).init(); 362 | } 363 | 364 | /// Initializes a global subscriber for cargo tests with a [`ForestLayer`] using the default 365 | /// configuration. 366 | /// 367 | /// This function is intended for test case initialization and processes log trees "inline", 368 | /// meaning it doesn't take advantage of a worker task for formatting and writing. 369 | /// 370 | /// [`worker_task`]: crate::runtime::worker_task 371 | /// 372 | /// # Examples 373 | /// ``` 374 | /// use tracing::{info, info_span}; 375 | /// 376 | /// let _ = tracing_forest::test_init(); 377 | /// 378 | /// info!("Hello, world!"); 379 | /// info_span!("my_span").in_scope(|| { 380 | /// info!("Relevant information"); 381 | /// }); 382 | /// ``` 383 | pub fn test_init() -> Result<(), TryInitError> { 384 | Registry::default() 385 | .with(ForestLayer::new(TestCapturePrinter::new(), NoTag)) 386 | .try_init() 387 | } 388 | -------------------------------------------------------------------------------- /tracing-forest/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [![github-img]][github-url] [![crates-img]][crates-url] [![docs-img]][docs-url] 2 | //! 3 | //! [github-url]: https://github.com/QnnOkabayashi/tracing-forest 4 | //! [crates-url]: https://crates.io/crates/tracing-forest 5 | //! [docs-url]: crate 6 | //! [github-img]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github 7 | //! [crates-img]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust 8 | //! [docs-img]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K 9 | //! 10 | //! Preserve contextual coherence among trace data from concurrent tasks. 11 | //! 12 | //! # Overview 13 | //! 14 | //! [`tracing`] is a framework for instrumenting programs to collect structured 15 | //! and async-aware diagnostics via the [`Subscriber`] trait. The 16 | //! [`tracing-subscriber`] crate provides tools for composing [`Subscriber`]s 17 | //! from smaller units. This crate extends [`tracing-subscriber`] by providing 18 | //! [`ForestLayer`], a [`Layer`] that preserves contextual coherence of trace 19 | //! data from concurrent tasks when logging. 20 | //! 21 | //! This crate is intended for programs running many nontrivial and disjoint 22 | //! tasks concurrently, like server backends. Unlike [other `Subscriber`s][crate#contextual-coherence-in-action] 23 | //! which simply keep track of the context of an event, `tracing-forest` preserves 24 | //! the contextual coherence when writing logs even in parallel contexts, allowing 25 | //! readers to easily trace a sequence of events from the same task. 26 | //! 27 | //! `tracing-forest` is intended for authoring applications. 28 | //! 29 | //! [`tracing-subscriber`]: tracing_subscriber 30 | //! [`Layer`]: tracing_subscriber::layer::Layer 31 | //! [`Subscriber`]: tracing::subscriber::Subscriber 32 | //! 33 | //! # Getting started 34 | //! 35 | //! The easiest way to get started is to enable all features. Do this by 36 | //! adding the following to your `Cargo.toml` file: 37 | //! ```toml 38 | //! tracing-forest = { version = "0.1.6", features = ["full"] } 39 | //! ``` 40 | //! Then, add [`tracing_forest::init`](crate::init) to your main function: 41 | //! ``` 42 | //! fn main() { 43 | //! // Initialize a default `ForestLayer` subscriber 44 | //! tracing_forest::init(); 45 | //! // ... 46 | //! } 47 | //! ``` 48 | //! This crate also provides tools for much more advanced configurations: 49 | //! ``` 50 | //! use tracing_forest::{traits::*, util::*}; 51 | //! 52 | //! #[tokio::main] 53 | //! async fn main() { 54 | //! tracing_forest::worker_task() 55 | //! .set_global(true) 56 | //! .map_sender(|sender| sender.or_stderr()) 57 | //! .build_on(|subscriber| subscriber 58 | //! .with(EnvFilter::from_default_env()) 59 | //! .with(LevelFilter::INFO) 60 | //! ) 61 | //! .on(async { 62 | //! // -- snip -- 63 | //! }) 64 | //! .await; 65 | //! } 66 | //! ``` 67 | //! For useful configuration abstractions, see the [`runtime` module documentation][runtime]. 68 | //! 69 | //! # Contextual coherence in action 70 | //! 71 | //! Similar to this crate, the [`tracing-tree`] crate collects and writes trace 72 | //! data as a tree. Unlike this crate, it doesn't maintain contextual coherence 73 | //! in parallel contexts. 74 | //! 75 | //! Observe the below program, which simulates serving multiple clients concurrently. 76 | //! ``` 77 | //! # async fn some_expensive_operation() {} 78 | //! # mod tracing_tree { 79 | //! # #[derive(Default)] 80 | //! # pub struct HierarchicalLayer; 81 | //! # impl tracing_subscriber::Layer for HierarchicalLayer {} 82 | //! # } 83 | //! use tracing::info; 84 | //! use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Registry}; 85 | //! use tracing_tree::HierarchicalLayer; 86 | //! 87 | //! #[tracing::instrument] 88 | //! async fn conn(id: u32) { 89 | //! for i in 0..3 { 90 | //! some_expensive_operation().await; 91 | //! info!(id, "step {}", i); 92 | //! } 93 | //! } 94 | //! 95 | //! #[tokio::main(flavor = "multi_thread")] 96 | //! async fn main() { 97 | //! // Use a `tracing-tree` subscriber 98 | //! Registry::default() 99 | //! .with(HierarchicalLayer::default()) 100 | //! .init(); 101 | //! 102 | //! let connections: Vec<_> = (0..3) 103 | //! .map(|id| tokio::spawn(conn(id))) 104 | //! .collect(); 105 | //! 106 | //! for conn in connections { 107 | //! conn.await.unwrap(); 108 | //! } 109 | //! } 110 | //! ``` 111 | //! `tracing-tree` isn't intended for concurrent use, and this is demonstrated 112 | //! by the output of the program: 113 | //! ```log 114 | //! conn id=2 115 | //! conn id=0 116 | //! conn id=1 117 | //! 23ms INFO step 0, id=2 118 | //! 84ms INFO step 0, id=1 119 | //! 94ms INFO step 1, id=2 120 | //! 118ms INFO step 0, id=0 121 | //! 130ms INFO step 1, id=1 122 | //! 193ms INFO step 2, id=2 123 | //! 124 | //! 217ms INFO step 1, id=0 125 | //! 301ms INFO step 2, id=1 126 | //! 127 | //! 326ms INFO step 2, id=0 128 | //! 129 | //! ``` 130 | //! We can instead use `tracing-forest` as a drop-in replacement for `tracing-tree`. 131 | //! ``` 132 | //! use tracing::info; 133 | //! use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Registry}; 134 | //! use tracing_forest::ForestLayer; 135 | //! 136 | //! #[tracing::instrument] 137 | //! async fn conn(id: u32) { 138 | //! // -- snip -- 139 | //! } 140 | //! 141 | //! #[tokio::main(flavor = "multi_thread")] 142 | //! async fn main() { 143 | //! // Use a `tracing-forest` subscriber 144 | //! Registry::default() 145 | //! .with(ForestLayer::default()) 146 | //! .init(); 147 | //! 148 | //! // -- snip -- 149 | //! } 150 | //! ``` 151 | //! Now we can easily trace what happened: 152 | //! ```log 153 | //! INFO conn [ 150µs | 100.00% ] id: 1 154 | //! INFO ┝━ i [info]: step 0 | id: 1 155 | //! INFO ┝━ i [info]: step 1 | id: 1 156 | //! INFO ┕━ i [info]: step 2 | id: 1 157 | //! INFO conn [ 343µs | 100.00% ] id: 0 158 | //! INFO ┝━ i [info]: step 0 | id: 0 159 | //! INFO ┝━ i [info]: step 1 | id: 0 160 | //! INFO ┕━ i [info]: step 2 | id: 0 161 | //! INFO conn [ 233µs | 100.00% ] id: 2 162 | //! INFO ┝━ i [info]: step 0 | id: 2 163 | //! INFO ┝━ i [info]: step 1 | id: 2 164 | //! INFO ┕━ i [info]: step 2 | id: 2 165 | //! ``` 166 | //! 167 | //! [`tracing-tree`]: https://crates.io/crates/tracing-tree 168 | //! 169 | //! # Categorizing events with tags 170 | //! 171 | //! This crate allows attaching supplemental categorical information to events with [`Tag`]s. 172 | //! 173 | //! Without tags, it's difficult to distinguish where events are occurring in a system. 174 | //! ```log 175 | //! INFO i [info]: some info for the admin 176 | //! ERROR 🚨 [error]: the request timed out 177 | //! ERROR 🚨 [error]: the db has been breached 178 | //! ``` 179 | //! 180 | //! Tags help make this distinction more visible. 181 | //! ```log 182 | //! INFO i [admin.info]: some info for the admin 183 | //! ERROR 🚨 [request.error]: the request timed out 184 | //! ERROR 🔐 [security.critical]: the db has been breached 185 | //! ``` 186 | //! 187 | //! See the [`tag` module-level documentation](mod@crate::tag) for details. 188 | //! 189 | //! # Attaching `Uuid`s to trace data 190 | //! 191 | //! When the `uuid` feature is enabled, the `ForestLayer` will automatically attach 192 | //! [`Uuid`]s to trace data. Events will adopt the UUID of their span, or the "nil" 193 | //! UUID at the root level. Spans will adopt the UUID of parent spans, or generate 194 | //! a new UUID at the root level. 195 | //! 196 | //! A span's `Uuid` can also be passed in manually to override adopting the parent's 197 | //! `Uuid` by passing it in as a field named `uuid`: 198 | //! ``` 199 | //! # use tracing::info_span; 200 | //! # use uuid::Uuid; 201 | //! let id = Uuid::new_v4(); 202 | //! 203 | //! let span = info_span!("my_span", uuid = %id); 204 | //! ``` 205 | //! 206 | //! It can also be retreived from the most recently entered span with 207 | //! [`tracing_forest::id`](crate::id): 208 | //! ``` 209 | //! # use tracing::info_span; 210 | //! # use uuid::Uuid; 211 | //! # tracing_forest::init(); 212 | //! let id = Uuid::new_v4(); 213 | //! 214 | //! info_span!("my_span", uuid = %id).in_scope(|| { 215 | //! let current_id = tracing_forest::id(); 216 | //! 217 | //! assert!(id == current_id); 218 | //! }); 219 | //! ``` 220 | //! 221 | //! # Immediate logs 222 | //! 223 | //! Since `tracing-forest` stores trace data in memory until the root span finishes, 224 | //! it can be a long time until a log is written. This may not be acceptable in 225 | //! certain use cases. 226 | //! 227 | //! To resolve this, the `immediate` field can be used on an event to print the 228 | //! event and its parent spans to stderr. Unlike `eprintln!`, the event will 229 | //! still appear in the trace tree written once the root span closes. 230 | //! 231 | //! ## Example 232 | //! 233 | //! ``` 234 | //! use tracing::{info, trace_span}; 235 | //! 236 | //! tracing_forest::init(); 237 | //! 238 | //! trace_span!("my_span").in_scope(|| { 239 | //! info!("first"); 240 | //! info!("second"); 241 | //! info!(immediate = true, "third, but immediately"); 242 | //! }); 243 | //! ``` 244 | //! ```log 245 | //! INFO i IMMEDIATE i my_span > third, but immediately 246 | //! TRACE my_span [ 125µs | 100.000% ] 247 | //! INFO ┝━ i [info]: first 248 | //! INFO ┝━ i [info]: second 249 | //! INFO ┕━ i [info]: third, but immediately 250 | //! ``` 251 | //! 252 | //! # Feature flags 253 | //! 254 | //! This crate uses feature flags to reduce dependency bloat. 255 | //! 256 | //! * `full`: Enables all features listed below. 257 | //! * `uuid`: Enables spans to carry operation IDs. 258 | //! * `chrono`: Enables timestamps on trace data. 259 | //! * `ansi`: Enables ANSI terminal colors. 260 | //! * `smallvec`: Enables some performance optimizations. 261 | //! * `tokio`: Enables [`worker_task`] and [`capture`]. 262 | //! * `serde`: Enables log trees to be serialized, which is [useful for formatting][serde_fmt]. 263 | //! * `env-filter`: Re-exports [`EnvFilter`] from the [`util`] module. 264 | //! 265 | //! By default, only `smallvec` in enabled. 266 | //! 267 | //! [`Uuid`]: uuid::Uuid 268 | //! [serde_fmt]: crate::printer::Formatter#examples 269 | //! [`EnvFilter`]: tracing_subscriber::EnvFilter 270 | 271 | #![doc(issue_tracker_base_url = "https://github.com/QnnOkabayashi/tracing-forest/issues")] 272 | #![cfg_attr( 273 | docsrs, 274 | // Allows displaying cfgs/feature flags in the documentation. 275 | feature(doc_cfg), 276 | // Allows adding traits to RustDoc's list of "notable traits" 277 | // feature(doc_notable_trait), 278 | // Fail the docs build if any intra-docs links are broken 279 | deny(rustdoc::broken_intra_doc_links), 280 | )] 281 | #![deny(warnings)] 282 | #![warn(unused_extern_crates)] 283 | #![warn(missing_docs)] 284 | 285 | pub mod printer; 286 | pub mod processor; 287 | pub mod tag; 288 | pub mod tree; 289 | #[macro_use] 290 | mod cfg; 291 | mod fail; 292 | mod layer; 293 | 294 | pub use layer::{init, test_init, ForestLayer}; 295 | pub use printer::{Formatter, PrettyPrinter, Printer}; 296 | pub use processor::Processor; 297 | pub use tag::Tag; 298 | 299 | cfg_tokio! { 300 | pub mod runtime; 301 | pub use runtime::{capture, worker_task}; 302 | } 303 | 304 | cfg_uuid! { 305 | pub use layer::id::id; 306 | } 307 | 308 | /// Bring traits from this crate, `tracing`, and `tracing_subscriber` into scope 309 | /// anonymously. 310 | pub mod traits { 311 | pub use crate::Processor as _; 312 | pub use tracing::Instrument as _; 313 | pub use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _}; 314 | } 315 | 316 | /// Bring Tracing's event and span macros into scope, along with other sensible defaults. 317 | pub mod util { 318 | #[doc(no_inline)] 319 | pub use crate::ForestLayer; 320 | #[doc(no_inline)] 321 | pub use tracing::metadata::LevelFilter; 322 | #[doc(no_inline)] 323 | pub use tracing::{ 324 | debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span, 325 | Event, Level, 326 | }; 327 | #[cfg(feature = "env-filter")] 328 | #[doc(no_inline)] 329 | pub use tracing_subscriber::EnvFilter; 330 | } 331 | -------------------------------------------------------------------------------- /tracing-forest/src/printer/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for formatting and writing trace trees. 2 | use crate::processor::{self, Processor}; 3 | use crate::tree::Tree; 4 | use std::error::Error; 5 | use std::io::{self, Write}; 6 | use tracing_subscriber::fmt::MakeWriter; 7 | 8 | mod pretty; 9 | pub use pretty::Pretty; 10 | 11 | /// Format a [`Tree`] into a `String`. 12 | /// 13 | /// # Examples 14 | /// 15 | /// This trait implements all `Fn(&Tree) -> Result` types, where `E: Error + Send + Sync`. 16 | /// If the `serde` feature is enabled, functions like `serde_json::to_string_pretty` 17 | /// can be used wherever a `Formatter` is required. 18 | /// ``` 19 | /// # use tracing::info; 20 | /// # #[tokio::main(flavor = "current_thread")] 21 | /// # async fn main() { 22 | /// tracing_forest::worker_task() 23 | /// .map_receiver(|receiver| { 24 | /// receiver.formatter(serde_json::to_string_pretty) 25 | /// }) 26 | /// .build() 27 | /// .on(async { 28 | /// info!("write this as json"); 29 | /// }) 30 | /// .await 31 | /// # } 32 | /// ``` 33 | /// Produces the following result: 34 | /// ```json 35 | /// { 36 | /// "Event": { 37 | /// "uuid": "00000000-0000-0000-0000-000000000000", 38 | /// "timestamp": "2022-03-24T16:08:17.761149+00:00", 39 | /// "level": "INFO", 40 | /// "message": "write this as json", 41 | /// "tag": "info", 42 | /// "fields": {} 43 | /// } 44 | /// } 45 | /// ``` 46 | pub trait Formatter { 47 | /// The error type if the `Tree` cannot be stringified. 48 | type Error: Error + Send + Sync; 49 | 50 | /// Stringifies the `Tree`, or returns an error. 51 | /// 52 | /// # Errors 53 | /// 54 | /// If the `Tree` cannot be formatted to a string, an error is returned. 55 | fn fmt(&self, tree: &Tree) -> Result; 56 | } 57 | 58 | impl Formatter for F 59 | where 60 | F: Fn(&Tree) -> Result, 61 | E: Error + Send + Sync, 62 | { 63 | type Error = E; 64 | 65 | #[inline] 66 | fn fmt(&self, tree: &Tree) -> Result { 67 | self(tree) 68 | } 69 | } 70 | 71 | /// A [`Processor`] that formats and writes logs. 72 | #[derive(Clone, Debug)] 73 | pub struct Printer { 74 | formatter: F, 75 | make_writer: W, 76 | } 77 | 78 | /// A [`MakeWriter`] that writes to stdout. 79 | /// 80 | /// This is functionally the same as using [`std::io::stdout`] as a `MakeWriter`, 81 | /// except it has a named type and can therefore be used in type signatures. 82 | #[derive(Debug)] 83 | pub struct MakeStdout; 84 | 85 | /// A [`MakeWriter`] that writes to stderr. 86 | /// 87 | /// This is functionally the same as using [`std::io::stderr`] as a `MakeWriter`, 88 | /// except it has a named type and can therefore be used in type signatures. 89 | #[derive(Debug)] 90 | pub struct MakeStderr; 91 | 92 | impl<'a> MakeWriter<'a> for MakeStdout { 93 | type Writer = io::Stdout; 94 | 95 | fn make_writer(&self) -> Self::Writer { 96 | io::stdout() 97 | } 98 | } 99 | 100 | impl<'a> MakeWriter<'a> for MakeStderr { 101 | type Writer = io::Stderr; 102 | 103 | fn make_writer(&self) -> Self::Writer { 104 | io::stderr() 105 | } 106 | } 107 | 108 | /// A [`Processor`] that pretty-prints to stdout. 109 | pub type PrettyPrinter = Printer; 110 | 111 | impl PrettyPrinter { 112 | /// Returns a new [`PrettyPrinter`] that pretty-prints to stdout. 113 | /// 114 | /// Use [`Printer::formatter`] and [`Printer::writer`] for custom configuration. 115 | pub const fn new() -> Self { 116 | Printer { 117 | formatter: Pretty, 118 | make_writer: MakeStdout, 119 | } 120 | } 121 | } 122 | 123 | impl Printer 124 | where 125 | F: 'static + Formatter, 126 | W: 'static + for<'a> MakeWriter<'a>, 127 | { 128 | /// Set the formatter. 129 | /// 130 | /// See the [`Formatter`] trait for details on possible inputs. 131 | pub fn formatter(self, formatter: F2) -> Printer 132 | where 133 | F2: 'static + Formatter, 134 | { 135 | Printer { 136 | formatter, 137 | make_writer: self.make_writer, 138 | } 139 | } 140 | 141 | /// Set the writer. 142 | pub fn writer(self, make_writer: W2) -> Printer 143 | where 144 | W2: 'static + for<'a> MakeWriter<'a>, 145 | { 146 | Printer { 147 | formatter: self.formatter, 148 | make_writer, 149 | } 150 | } 151 | } 152 | 153 | impl Default for PrettyPrinter { 154 | fn default() -> Self { 155 | PrettyPrinter::new() 156 | } 157 | } 158 | 159 | impl Processor for Printer 160 | where 161 | F: 'static + Formatter, 162 | W: 'static + for<'a> MakeWriter<'a>, 163 | { 164 | fn process(&self, tree: Tree) -> processor::Result { 165 | let string = match self.formatter.fmt(&tree) { 166 | Ok(s) => s, 167 | Err(e) => return Err(processor::error(tree, e.into())), 168 | }; 169 | 170 | match self.make_writer.make_writer().write_all(string.as_bytes()) { 171 | Ok(()) => Ok(()), 172 | Err(e) => Err(processor::error(tree, e.into())), 173 | } 174 | } 175 | } 176 | 177 | /// A [`Processor`] that captures logs during tests and allows them to be presented 178 | /// when --nocapture is used. 179 | #[derive(Clone, Debug)] 180 | pub struct TestCapturePrinter { 181 | formatter: F, 182 | } 183 | 184 | impl TestCapturePrinter { 185 | /// Construct a new test capturing printer with the default `Pretty` formatter. This printer 186 | /// is intented for use in tests only as it works with the default rust stdout capture mechanism 187 | pub const fn new() -> Self { 188 | TestCapturePrinter { formatter: Pretty } 189 | } 190 | } 191 | 192 | impl Processor for TestCapturePrinter 193 | where 194 | F: 'static + Formatter, 195 | { 196 | fn process(&self, tree: Tree) -> processor::Result { 197 | let string = self 198 | .formatter 199 | .fmt(&tree) 200 | .map_err(|e| processor::error(tree, e.into()))?; 201 | 202 | print!("{}", string); 203 | Ok(()) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /tracing-forest/src/printer/pretty.rs: -------------------------------------------------------------------------------- 1 | use crate::printer::Formatter; 2 | use crate::tree::{Event, Shared, Span, Tree}; 3 | use crate::Tag; 4 | use std::fmt::{self, Write}; 5 | 6 | #[cfg(feature = "smallvec")] 7 | type IndentVec = smallvec::SmallVec<[Indent; 32]>; 8 | #[cfg(not(feature = "smallvec"))] 9 | type IndentVec = Vec; 10 | 11 | #[cfg(feature = "ansi")] 12 | use ansi_term::Color; 13 | #[cfg(feature = "ansi")] 14 | use tracing::Level; 15 | 16 | /// Format logs for pretty printing. 17 | /// 18 | /// # Interpreting span times 19 | /// 20 | /// Spans have the following format: 21 | /// ```txt 22 | /// [ | / ] 23 | /// ``` 24 | /// * `DURATION` represents the total time the span was entered for. If the span 25 | /// was used to instrument a `Future` that sleeps, then that time won't be counted 26 | /// since the `Future` won't be polled during that time, and so the span won't enter. 27 | /// * `BODY` represents the percent time the span is entered relative to the root 28 | /// span, *excluding* time that any child spans are entered. 29 | /// * `ROOT` represents the percent time the span is entered relative to the root 30 | /// span, *including* time that any child spans are entered. 31 | /// 32 | /// As a mental model, look at `ROOT` to quickly narrow down which branches are 33 | /// costly, and look at `BASE` to pinpoint exactly which spans are expensive. 34 | /// 35 | /// Spans without any child spans would have the same `BASE` and `ROOT`, so the 36 | /// redundency is omitted. 37 | /// 38 | /// # Examples 39 | /// 40 | /// An arbitrarily complex example: 41 | /// ```log 42 | /// INFO try_from_entry_ro [ 324µs | 8.47% / 100.00% ] 43 | /// INFO ┝━ server::internal_search [ 296µs | 19.02% / 91.53% ] 44 | /// INFO │ ┝━ i [filter.info]: Some filter info... 45 | /// INFO │ ┝━ server::search [ 226µs | 10.11% / 70.01% ] 46 | /// INFO │ │ ┝━ be::search [ 181µs | 6.94% / 55.85% ] 47 | /// INFO │ │ │ ┕━ be::search -> filter2idl [ 158µs | 19.65% / 48.91% ] 48 | /// INFO │ │ │ ┝━ be::idl_arc_sqlite::get_idl [ 20.4µs | 6.30% ] 49 | /// INFO │ │ │ │ ┕━ i [filter.info]: Some filter info... 50 | /// INFO │ │ │ ┕━ be::idl_arc_sqlite::get_idl [ 74.3µs | 22.96% ] 51 | /// ERROR │ │ │ ┝━ 🚨 [admin.error]: On no, an admin error occurred :( 52 | /// DEBUG │ │ │ ┝━ 🐛 [debug]: An untagged debug log 53 | /// INFO │ │ │ ┕━ i [admin.info]: there's been a big mistake | alive: false | status: "very sad" 54 | /// INFO │ │ ┕━ be::idl_arc_sqlite::get_identry [ 13.1µs | 4.04% ] 55 | /// ERROR │ │ ┝━ 🔐 [security.critical]: A security critical log 56 | /// INFO │ │ ┕━ 🔓 [security.access]: A security access log 57 | /// INFO │ ┕━ server::search [ 8.08µs | 2.50% ] 58 | /// WARN │ ┕━ 🚧 [filter.warn]: Some filter warning 59 | /// TRACE ┕━ 📍 [trace]: Finished! 60 | /// ``` 61 | #[derive(Debug)] 62 | pub struct Pretty; 63 | 64 | impl Formatter for Pretty { 65 | type Error = fmt::Error; 66 | 67 | fn fmt(&self, tree: &Tree) -> Result { 68 | let mut writer = String::with_capacity(256); 69 | 70 | Pretty::format_tree(tree, None, &mut IndentVec::new(), &mut writer)?; 71 | 72 | Ok(writer) 73 | } 74 | } 75 | 76 | impl Pretty { 77 | fn format_tree( 78 | tree: &Tree, 79 | duration_root: Option, 80 | indent: &mut IndentVec, 81 | writer: &mut String, 82 | ) -> fmt::Result { 83 | match tree { 84 | Tree::Event(event) => { 85 | Pretty::format_shared(&event.shared, writer)?; 86 | Pretty::format_indent(indent, writer)?; 87 | Pretty::format_event(event, writer) 88 | } 89 | Tree::Span(span) => { 90 | Pretty::format_shared(&span.shared, writer)?; 91 | Pretty::format_indent(indent, writer)?; 92 | Pretty::format_span(span, duration_root, indent, writer) 93 | } 94 | } 95 | } 96 | 97 | fn format_shared(shared: &Shared, writer: &mut String) -> fmt::Result { 98 | #[cfg(feature = "uuid")] 99 | write!(writer, "{} ", shared.uuid)?; 100 | 101 | #[cfg(feature = "chrono")] 102 | write!(writer, "{:<36} ", shared.timestamp.to_rfc3339())?; 103 | 104 | #[cfg(feature = "ansi")] 105 | return write!(writer, "{:<8} ", ColorLevel(shared.level)); 106 | 107 | #[cfg(not(feature = "ansi"))] 108 | return write!(writer, "{:<8} ", shared.level); 109 | } 110 | 111 | fn format_indent(indent: &[Indent], writer: &mut String) -> fmt::Result { 112 | for indent in indent { 113 | writer.write_str(indent.repr())?; 114 | } 115 | Ok(()) 116 | } 117 | 118 | fn format_event(event: &Event, writer: &mut String) -> fmt::Result { 119 | let tag = event.tag().unwrap_or_else(|| Tag::from(event.level())); 120 | 121 | write!(writer, "{} [{}]: ", tag.icon(), tag)?; 122 | 123 | if let Some(message) = event.message() { 124 | writer.write_str(message)?; 125 | } 126 | 127 | for field in event.fields().iter() { 128 | write!(writer, " | {}: {}", field.key(), field.value())?; 129 | } 130 | 131 | writeln!(writer) 132 | } 133 | 134 | fn format_span( 135 | span: &Span, 136 | duration_root: Option, 137 | indent: &mut IndentVec, 138 | writer: &mut String, 139 | ) -> fmt::Result { 140 | let total_duration = span.total_duration().as_nanos() as f64; 141 | let inner_duration = span.inner_duration().as_nanos() as f64; 142 | let root_duration = duration_root.unwrap_or(total_duration); 143 | let percent_total_of_root_duration = 100.0 * total_duration / root_duration; 144 | 145 | write!( 146 | writer, 147 | "{} [ {} | ", 148 | span.name(), 149 | DurationDisplay(total_duration) 150 | )?; 151 | 152 | if inner_duration > 0.0 { 153 | let base_duration = span.base_duration().as_nanos() as f64; 154 | let percent_base_of_root_duration = 100.0 * base_duration / root_duration; 155 | write!(writer, "{:.2}% / ", percent_base_of_root_duration)?; 156 | } 157 | 158 | write!(writer, "{:.2}% ]", percent_total_of_root_duration)?; 159 | 160 | for (n, field) in span.shared.fields.iter().enumerate() { 161 | write!( 162 | writer, 163 | "{} {}: {}", 164 | if n == 0 { "" } else { " |" }, 165 | field.key(), 166 | field.value() 167 | )?; 168 | } 169 | writeln!(writer)?; 170 | 171 | if let Some((last, remaining)) = span.nodes().split_last() { 172 | match indent.last_mut() { 173 | Some(edge @ Indent::Turn) => *edge = Indent::Null, 174 | Some(edge @ Indent::Fork) => *edge = Indent::Line, 175 | _ => {} 176 | } 177 | 178 | indent.push(Indent::Fork); 179 | 180 | for tree in remaining { 181 | if let Some(edge) = indent.last_mut() { 182 | *edge = Indent::Fork; 183 | } 184 | Pretty::format_tree(tree, Some(root_duration), indent, writer)?; 185 | } 186 | 187 | if let Some(edge) = indent.last_mut() { 188 | *edge = Indent::Turn; 189 | } 190 | Pretty::format_tree(last, Some(root_duration), indent, writer)?; 191 | 192 | indent.pop(); 193 | } 194 | 195 | Ok(()) 196 | } 197 | } 198 | 199 | enum Indent { 200 | Null, 201 | Line, 202 | Fork, 203 | Turn, 204 | } 205 | 206 | impl Indent { 207 | fn repr(&self) -> &'static str { 208 | match self { 209 | Self::Null => " ", 210 | Self::Line => "│ ", 211 | Self::Fork => "┝━ ", 212 | Self::Turn => "┕━ ", 213 | } 214 | } 215 | } 216 | 217 | struct DurationDisplay(f64); 218 | 219 | // Taken from chrono 220 | impl fmt::Display for DurationDisplay { 221 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 222 | let mut t = self.0; 223 | for unit in ["ns", "µs", "ms", "s"] { 224 | if t < 10.0 { 225 | return write!(f, "{:.2}{}", t, unit); 226 | } else if t < 100.0 { 227 | return write!(f, "{:.1}{}", t, unit); 228 | } else if t < 1000.0 { 229 | return write!(f, "{:.0}{}", t, unit); 230 | } 231 | t /= 1000.0; 232 | } 233 | write!(f, "{:.0}s", t * 1000.0) 234 | } 235 | } 236 | 237 | // From tracing-tree 238 | #[cfg(feature = "ansi")] 239 | struct ColorLevel(Level); 240 | 241 | #[cfg(feature = "ansi")] 242 | impl fmt::Display for ColorLevel { 243 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 244 | let color = match self.0 { 245 | Level::TRACE => Color::Purple, 246 | Level::DEBUG => Color::Blue, 247 | Level::INFO => Color::Green, 248 | Level::WARN => Color::RGB(252, 234, 160), // orange 249 | Level::ERROR => Color::Red, 250 | }; 251 | let style = color.bold(); 252 | write!(f, "{}", style.prefix())?; 253 | f.pad(self.0.as_str())?; 254 | write!(f, "{}", style.suffix()) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /tracing-forest/src/processor.rs: -------------------------------------------------------------------------------- 1 | //! Trait for processing log trees on completion. 2 | //! 3 | //! See [`Processor`] for more details. 4 | use crate::printer::{MakeStderr, MakeStdout, Pretty, Printer}; 5 | use crate::tree::Tree; 6 | use std::error; 7 | use std::sync::Arc; 8 | use thiserror::Error; 9 | 10 | /// Error type returned if a [`Processor`] fails. 11 | #[derive(Error, Debug)] 12 | #[error("{source}")] 13 | pub struct Error { 14 | /// The recoverable [`Tree`] type that couldn't be processed. 15 | pub tree: Tree, 16 | 17 | source: Box, 18 | } 19 | 20 | /// Create an error for when a [`Processor`] fails to process a [`Tree`]. 21 | pub fn error(tree: Tree, source: Box) -> Error { 22 | Error { tree, source } 23 | } 24 | 25 | /// The result type of [`Processor::process`]. 26 | pub type Result = std::result::Result<(), Error>; 27 | 28 | /// A trait for processing completed [`Tree`]s. 29 | /// 30 | /// `Processor`s are responsible for both formatting and writing logs to their 31 | /// intended destinations. This is typically implemented using 32 | /// [`Formatter`], [`MakeWriter`], and [`io::Write`]. 33 | /// 34 | /// While this trait may be implemented on downstream types, [`from_fn`] 35 | /// provides a convenient interface for creating `Processor`s without having to 36 | /// explicitly define new types. 37 | /// 38 | /// [trace trees]: crate::tree::Tree 39 | /// [`Formatter`]: crate::printer::Formatter 40 | /// [`MakeWriter`]: tracing_subscriber::fmt::MakeWriter 41 | /// [`io::Write`]: std::io::Write 42 | pub trait Processor: 'static + Sized { 43 | /// Process a [`Tree`]. This can mean many things, such as writing to 44 | /// stdout or a file, sending over a network, storing in memory, ignoring, 45 | /// or anything else. 46 | /// 47 | /// # Errors 48 | /// 49 | /// If the `Tree` cannot be processed, then it is returned along with a 50 | /// `Box`. If the processor is configured with a 51 | /// fallback processor from [`Processor::or`], then the `Tree` is deferred 52 | /// to that processor. 53 | fn process(&self, tree: Tree) -> Result; 54 | 55 | /// Returns a `Processor` that first attempts processing with `self`, and 56 | /// resorts to processing with `fallback` on failure. 57 | /// 58 | /// Note that [`or_stdout`], [`or_stderr`], and [`or_none`] can be used as 59 | /// shortcuts for pretty printing or dropping the `Tree` entirely. 60 | /// 61 | /// [`or_stdout`]: Processor::or_stdout 62 | /// [`or_stderr`]: Processor::or_stderr 63 | /// [`or_none`]: Processor::or_none 64 | fn or(self, processor: P) -> WithFallback { 65 | WithFallback { 66 | primary: self, 67 | fallback: processor, 68 | } 69 | } 70 | 71 | /// Returns a `Processor` that first attempts processing with `self`, and 72 | /// resorts to pretty-printing to stdout on failure. 73 | fn or_stdout(self) -> WithFallback> { 74 | self.or(Printer::new().writer(MakeStdout)) 75 | } 76 | 77 | /// Returns a `Processor` that first attempts processing with `self`, and 78 | /// resorts to pretty-printing to stderr on failure. 79 | fn or_stderr(self) -> WithFallback> { 80 | self.or(Printer::new().writer(MakeStderr)) 81 | } 82 | 83 | /// Returns a `Processor` that first attempts processing with `self`, otherwise 84 | /// silently fails. 85 | fn or_none(self) -> WithFallback { 86 | self.or(Sink) 87 | } 88 | } 89 | 90 | /// A [`Processor`] composed of a primary and a fallback `Processor`. 91 | /// 92 | /// This type is returned by [`Processor::or`]. 93 | #[derive(Debug)] 94 | pub struct WithFallback { 95 | primary: P, 96 | fallback: F, 97 | } 98 | 99 | /// A [`Processor`] that ignores any incoming logs. 100 | /// 101 | /// This processor cannot fail. 102 | #[derive(Debug)] 103 | pub struct Sink; 104 | 105 | /// A [`Processor`] that processes incoming logs via a function. 106 | /// 107 | /// Instances of `FromFn` are returned by the [`from_fn`] function. 108 | #[derive(Debug)] 109 | pub struct FromFn(F); 110 | 111 | /// Create a processor that processes incoming logs via a function. 112 | /// 113 | /// # Examples 114 | /// 115 | /// Internally, [`worker_task`] uses `from_fn` to allow the subscriber to send 116 | /// trace data across a channel to a processing task. 117 | /// ``` 118 | /// use tokio::sync::mpsc; 119 | /// use tracing_forest::processor; 120 | /// 121 | /// let (tx, rx) = mpsc::unbounded_channel(); 122 | /// 123 | /// let sender_processor = processor::from_fn(move |tree| tx 124 | /// .send(tree) 125 | /// .map_err(|err| { 126 | /// let msg = err.to_string().into(); 127 | /// processor::error(err.0, msg) 128 | /// }) 129 | /// ); 130 | /// 131 | /// // -- snip -- 132 | /// ``` 133 | /// 134 | /// [`worker_task`]: crate::runtime::worker_task 135 | pub fn from_fn(f: F) -> FromFn 136 | where 137 | F: 'static + Fn(Tree) -> Result, 138 | { 139 | FromFn(f) 140 | } 141 | 142 | impl Processor for WithFallback 143 | where 144 | P: Processor, 145 | F: Processor, 146 | { 147 | fn process(&self, tree: Tree) -> Result { 148 | self.primary.process(tree).or_else(|err| { 149 | eprintln!("{}, using fallback processor...", err); 150 | self.fallback.process(err.tree) 151 | }) 152 | } 153 | } 154 | 155 | impl Processor for Sink { 156 | fn process(&self, _tree: Tree) -> Result { 157 | Ok(()) 158 | } 159 | } 160 | 161 | impl Processor for FromFn 162 | where 163 | F: 'static + Fn(Tree) -> Result, 164 | { 165 | fn process(&self, tree: Tree) -> Result { 166 | (self.0)(tree) 167 | } 168 | } 169 | 170 | impl Processor for Box

{ 171 | fn process(&self, tree: Tree) -> Result { 172 | self.as_ref().process(tree) 173 | } 174 | } 175 | 176 | impl Processor for Arc

{ 177 | fn process(&self, tree: Tree) -> Result { 178 | self.as_ref().process(tree) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tracing-forest/src/runtime.rs: -------------------------------------------------------------------------------- 1 | //! Run asynchronous code in the context of a `tracing-forest` subscriber. 2 | //! 3 | //! This module provides useful abstractions for executing async code: 4 | //! [`worker_task`] for `main` functions, and [`capture`] for unit tests, 5 | //! both of which return a configurable [`Builder`] object. 6 | //! 7 | //! # Nonblocking log processing with `worker_task` 8 | //! 9 | //! `tracing-forest` collects trace data into trees, and can sometimes 10 | //! produce large trees that need to be processed. To avoid blocking the main 11 | //! task in these cases, a common strategy is to send this data to a worker 12 | //! task for formatting and writing. 13 | //! 14 | //! The [`worker_task`] function provides this behavior as a first-class feature of this 15 | //! crate, and handles the configuration, initialization, and graceful shutdown 16 | //! of a subscriber with an associated worker task for formatting and writing. 17 | //! 18 | //! Unlike [`tracing-appender`] which uses a writer thread for formatted logs, 19 | //! this module allows for log trees to be sent to a worker task before formatting, 20 | //! allowing more log-related work to be offloaded to the worker task. 21 | //! 22 | //! [`tracing-appender`]: https://crates.io/crates/tracing-appender 23 | //! 24 | //! ## Examples 25 | //! 26 | //! ``` 27 | //! use tracing::{info, info_span}; 28 | //! 29 | //! #[tokio::main] 30 | //! async fn main() { 31 | //! tracing_forest::worker_task() 32 | //! .build() 33 | //! .on(async { 34 | //! info!("Hello, world!"); 35 | //! 36 | //! info_span!("my_span").in_scope(|| { 37 | //! info!("Relevant information"); 38 | //! }) 39 | //! }) 40 | //! .await; 41 | //! } 42 | //! ``` 43 | //! Produces the output: 44 | //! ```log 45 | //! INFO i [info]: Hello, world! 46 | //! INFO my_span [ 26.0µs | 100.000% ] 47 | //! INFO ┕━ i [info]: Relevant information 48 | //! ``` 49 | //! 50 | //! For full configuration options, see the [`Builder`] documentation. 51 | //! 52 | //! # Inspecting trace data in unit tests with `capture` 53 | //! 54 | //! The [`capture`] function offers the ability to programmatically inspect log 55 | //! trees generated by `tracing-forest`. It is the unit testing analog of 56 | //! [`worker_task`], except it returns `Vec` after the future is completed, 57 | //! which can be then be inspected. 58 | //! 59 | //! ## Examples 60 | //! 61 | //! ``` 62 | //! use tracing_forest::tree::{Tree, Event, Span}; 63 | //! use tracing::{info, info_span}; 64 | //! 65 | //! #[tokio::main] 66 | //! async fn main() -> Result<(), Box> { 67 | //! let logs: Vec = tracing_forest::capture() 68 | //! .build() 69 | //! .on(async { 70 | //! info!("Hello, world!"); 71 | //! 72 | //! info_span!("my_span").in_scope(|| { 73 | //! info!("Relevant information"); 74 | //! }) 75 | //! }) 76 | //! .await; 77 | //! 78 | //! // There is one event and one span at the root level 79 | //! assert!(logs.len() == 2); 80 | //! 81 | //! // Inspect the first event 82 | //! let hello_world: &Event = logs[0].event()?; 83 | //! assert!(hello_world.message() == Some("Hello, world!")); 84 | //! 85 | //! // Inspect the span 86 | //! let my_span: &Span = logs[1].span()?; 87 | //! assert!(my_span.name() == "my_span"); 88 | //! 89 | //! // Only the `info` event is recorded 90 | //! assert!(my_span.nodes().len() == 1); 91 | //! 92 | //! let relevant_info: &Event = my_span.nodes()[0].event()?; 93 | //! 94 | //! assert!(relevant_info.message() == Some("Relevant information")); 95 | //! 96 | //! Ok(()) 97 | //! } 98 | //! ``` 99 | //! 100 | //! Additional options for tree inspection can be found in the 101 | //! [`tree` module-level documentation](crate::tree) 102 | //! 103 | //! For full configuration options, see the [`Builder`] documentation. 104 | use crate::layer::ForestLayer; 105 | use crate::printer::PrettyPrinter; 106 | use crate::tree::Tree; 107 | use crate::fail; 108 | use crate::tag::{TagParser, NoTag}; 109 | use crate::processor::{self, Processor, WithFallback}; 110 | use std::future::Future; 111 | use std::iter; 112 | use tokio::sync::mpsc::{self, UnboundedReceiver}; 113 | use tokio::sync::oneshot; 114 | use tracing::Subscriber; 115 | use tracing_subscriber::Registry; 116 | use tracing_subscriber::layer::{Layered, SubscriberExt as _}; 117 | 118 | /// Begins the configuration of a `ForestLayer` subscriber that sends log trees 119 | /// to a processing task for formatting and writing. 120 | /// 121 | /// For full configuration options, see [`Builder`]. 122 | /// 123 | /// For a high-level overview on usage, see the [module-level documentation][nonblocking-processing] 124 | /// for more details. 125 | /// 126 | /// # Note 127 | /// 128 | /// The [`worker_task`] function defaults to setting the global subscriber, which is required 129 | /// to detect logs in multithreading scenarios, but prevents setting other [`Subscriber`]s 130 | /// globally afterwards. This can be disabled via the [`set_global`] method. 131 | /// 132 | /// [nonblocking-processing]: crate::runtime#nonblocking-log-processing-with-worker_task 133 | /// [`set_global`]: Builder::set_global 134 | pub fn worker_task() -> Builder, WorkerTask, NoTag> { 135 | worker_task_inner(WorkerTask(PrettyPrinter::new()), true) 136 | } 137 | 138 | /// Begins the configuration of a `ForestLayer` subscriber that sends log trees 139 | /// to a buffer that can later be inspected programatically. 140 | /// 141 | /// For full configuration options, see [`Builder`]. 142 | /// 143 | /// For a high-level overview on usage, see the [module-level documentation][inspecting-trace-data] 144 | /// for more details. 145 | /// 146 | /// # Note 147 | /// 148 | /// The [`capture`] function defaults to not setting the global subscriber, which 149 | /// allows multiple unit tests in the same file, but prevents trace data from other 150 | /// threads to be collected. This can be enabled via the [`set_global`] method. 151 | /// 152 | /// [inspecting-trace-data]: crate::runtime#inspecting-trace-data-in-unit-tests-with-capture 153 | /// [`set_global`]: Builder::set_global 154 | pub fn capture() -> Builder, Capture, NoTag> { 155 | worker_task_inner(Capture(()), false) 156 | } 157 | 158 | fn worker_task_inner

(worker_processor: P, is_global: bool) -> Builder, P, NoTag> { 159 | let (tx, rx) = mpsc::unbounded_channel(); 160 | 161 | let sender_processor = processor::from_fn(move |tree| tx 162 | .send(tree) 163 | .map_err(|err| { 164 | let msg = err.to_string().into(); 165 | processor::error(err.0, msg) 166 | }) 167 | ); 168 | 169 | Builder { 170 | sender_processor: InnerSender(sender_processor), 171 | worker_processor, 172 | receiver: rx, 173 | tag: NoTag, 174 | is_global, 175 | } 176 | } 177 | 178 | /// Return type of [`worker_task`] and [`capture`]. 179 | /// 180 | /// # Configuring a `Runtime` 181 | /// 182 | /// `Builder` follows the [builder pattern][builder] to configure a [`Runtime`]. 183 | /// 184 | /// Configuration options include: 185 | /// * Setting the [tag][set_tag]. 186 | /// * Installing [globally][set_global]. 187 | /// * Configuring the [internal sender][map_sender] with fallbacks. 188 | /// * Configuring the [processor][map_receiver] in the worker task. 189 | /// 190 | /// To finish the `Runtime`, call the [`build`] method to compose the configured 191 | /// `ForestLayer` onto a [`Registry`]. Alternatively, the [`build_on`] method 192 | /// can be used construct arbitrary `Subscriber`s from the configured `ForestLayer`, 193 | /// which is used in the returned `Runtime`. 194 | /// 195 | /// [builder]: https://rust-lang.github.io/api-guidelines/type-safety.html#builders-enable-construction-of-complex-values-c-builder 196 | /// [set_tag]: Builder::set_tag 197 | /// [set_global]: Builder::set_global 198 | /// [map_sender]: Builder::map_sender 199 | /// [map_receiver]: Builder::map_receiver 200 | /// [`build`]: Builder::build 201 | /// [`build_on`]: Builder::build_on 202 | pub struct Builder { 203 | sender_processor: Tx, 204 | worker_processor: Rx, 205 | receiver: UnboundedReceiver, 206 | tag: T, 207 | is_global: bool, 208 | } 209 | 210 | /// A marker type indicating that trace data should be captured for later use. 211 | pub struct Capture(()); 212 | 213 | /// A marker type indicating that trace data should be processed. 214 | pub struct WorkerTask

(P); 215 | 216 | /// The [`Processor`] used within a `tracing-forest` subscriber for sending logs 217 | /// to a processing task. 218 | /// 219 | /// This type cannot be constructed by downstream users. 220 | #[derive(Debug)] 221 | pub struct InnerSender

(P); 222 | 223 | impl Processor for InnerSender

{ 224 | fn process(&self, tree: Tree) -> processor::Result { 225 | self.0.process(tree) 226 | } 227 | } 228 | 229 | mod sealed { 230 | pub trait Sealed {} 231 | } 232 | 233 | impl

sealed::Sealed for InnerSender

{} 234 | 235 | impl sealed::Sealed for WithFallback {} 236 | 237 | impl Builder, T> 238 | where 239 | P: Processor, 240 | { 241 | /// Configure the processor on the receiving end of the log channel. 242 | /// This is particularly useful for adding fallbacks. 243 | /// 244 | /// This method accepts a closure that accepts the current [`Processor`] on the 245 | /// worker task, and maps it to another [`Processor`]. 246 | /// 247 | /// # Note 248 | /// 249 | /// This method is only available if called after [`worker_task`]. 250 | /// 251 | /// # Examples 252 | /// 253 | /// Configuring the writing task to write to a file, or else fall back to stderr. 254 | /// ```no_run 255 | /// # #[tokio::main] 256 | /// # async fn main() { 257 | /// use tracing_forest::traits::*; 258 | /// use std::fs::File; 259 | /// 260 | /// let out = File::create("out.log").unwrap(); 261 | /// 262 | /// tracing_forest::worker_task() 263 | /// .map_receiver(|printer| printer 264 | /// .writer(out) 265 | /// .or_stderr() 266 | /// ) 267 | /// .build() 268 | /// .on(async { 269 | /// // ... 270 | /// }) 271 | /// .await; 272 | /// # } 273 | /// ``` 274 | pub fn map_receiver(self, f: F) -> Builder, T> 275 | where 276 | F: FnOnce(P) -> P2, 277 | P2: Processor, 278 | { 279 | Builder { 280 | sender_processor: self.sender_processor, 281 | worker_processor: WorkerTask(f(self.worker_processor.0)), 282 | receiver: self.receiver, 283 | tag: self.tag, 284 | is_global: self.is_global, 285 | } 286 | } 287 | } 288 | 289 | impl Builder 290 | where 291 | Tx: Processor + sealed::Sealed, 292 | T: TagParser, 293 | { 294 | /// Configure the processer within the subscriber that sends log trees to 295 | /// a processing task. This allows for dangling tasks to still generate trace 296 | /// data, even after the worker task closes. 297 | /// 298 | /// # Examples 299 | /// 300 | /// Allowing the subscriber to defer to stderr if the worker task finished. 301 | /// ```no_run 302 | /// # #[tokio::main] 303 | /// # async fn main() { 304 | /// use tracing_forest::traits::*; 305 | /// 306 | /// tracing_forest::worker_task() 307 | /// .map_sender(|sender| sender.or_stderr()) 308 | /// .build() 309 | /// .on(async { 310 | /// # mod tokio { 311 | /// # pub async fn spawn(_: T) {} 312 | /// # pub mod signal { 313 | /// # pub async fn ctrl_c() -> Result<(), ()> { Ok(()) } 314 | /// # } 315 | /// # } 316 | /// // The handle is immediately dropped, leaving the task dangling 317 | /// tokio::spawn(async { 318 | /// // Some unending task 319 | /// }); 320 | /// 321 | /// // Wait until the user stops the application 322 | /// tokio::signal::ctrl_c().await.expect("Failed to listen for CTRL-C"); 323 | /// }) 324 | /// .await; 325 | /// // The worker task is completed and the channel is closed at this point. 326 | /// // Any new trace data generated by the dangling task at this point 327 | /// // is deferred to stderr because of the added fallback. 328 | /// # } 329 | /// ``` 330 | /// 331 | /// Since dropping the sender half would make the receiver task useless, this 332 | /// method uses traits to enforce at compile time that the function returns 333 | /// some derivation of the sender. Currently, the only accepted wrapping is 334 | /// through adding a fallback. 335 | /// ```compile_fail 336 | /// use tracing_forest::PrettyPrinter; 337 | /// 338 | /// # #[tokio::main] 339 | /// # async fn main() { 340 | /// tracing_forest::worker_task() 341 | /// .map_sender(|_sender| { 342 | /// // Some variation of the sender isn't returned, so this won't compile. 343 | /// PrettyPrinter::new() 344 | /// }) 345 | /// .build() 346 | /// .on(async { 347 | /// // ... 348 | /// }) 349 | /// .await; 350 | /// # } 351 | /// ``` 352 | pub fn map_sender(self, f: F) -> Builder 353 | where 354 | F: FnOnce(Tx) -> Tx2, 355 | Tx2: Processor + sealed::Sealed, 356 | { 357 | Builder { 358 | sender_processor: f(self.sender_processor), 359 | worker_processor: self.worker_processor, 360 | receiver: self.receiver, 361 | tag: self.tag, 362 | is_global: self.is_global, 363 | 364 | } 365 | } 366 | 367 | /// Set the [`TagParser`]. 368 | /// 369 | /// # Examples 370 | /// 371 | /// ``` 372 | /// use tracing_forest::{util::*, Tag}; 373 | /// 374 | /// fn simple_tag(event: &Event) -> Option { 375 | /// // -- snip -- 376 | /// # None 377 | /// } 378 | /// 379 | /// #[tokio::main] 380 | /// async fn main() { 381 | /// tracing_forest::worker_task() 382 | /// .set_tag(simple_tag) 383 | /// .build() 384 | /// .on(async { 385 | /// // ... 386 | /// }) 387 | /// .await; 388 | /// } 389 | /// ``` 390 | pub fn set_tag(self, tag: T2) -> Builder 391 | where 392 | T2: TagParser, 393 | { 394 | Builder { 395 | sender_processor: self.sender_processor, 396 | worker_processor: self.worker_processor, 397 | receiver: self.receiver, 398 | tag, 399 | is_global: self.is_global, 400 | } 401 | } 402 | 403 | /// Set whether or not the subscriber should be set globally. 404 | /// 405 | /// Setting the subscriber globally is intended for `main` functions, since 406 | /// it allows logs to be be collected across multithreaded environments. Not 407 | /// setting globally is intended for test functions, which need to set a new 408 | /// subscriber multiple times in the same program. 409 | /// 410 | /// # Examples 411 | /// 412 | /// For multithreaded tests, `set_global` can be used so that the subscriber 413 | /// applies to all the threads. However, each function that sets a global 414 | /// subscriber must be in its own compilation unit, like an integration test, 415 | /// otherwise the global subscriber will carry over across tests. 416 | /// ``` 417 | /// #[tokio::test(flavor = "multi_thread")] 418 | /// async fn test_multithreading() { 419 | /// let logs = tracing_forest::capture() 420 | /// .set_global(true) 421 | /// .build() 422 | /// .on(async { 423 | /// // spawn some tasks 424 | /// }) 425 | /// .await; 426 | /// 427 | /// // inspect logs... 428 | /// } 429 | /// ``` 430 | pub fn set_global(mut self, is_global: bool) -> Self { 431 | self.is_global = is_global; 432 | self 433 | } 434 | 435 | /// Finishes the `ForestLayer` by composing it into a [`Registry`], and 436 | /// returns it as a [`Runtime`]. 437 | /// 438 | /// This method is useful for a basic configuration of a `Subscriber`. For 439 | /// a more advanced configuration, see the [`build_on`] and [`build_with`] 440 | /// methods. 441 | /// 442 | /// [`build_on`]: Builder::build_on 443 | /// [`build_with`]: Builder::build_with 444 | /// 445 | /// # Examples 446 | /// 447 | /// ``` 448 | /// #[tokio::main] 449 | /// async fn main() { 450 | /// tracing_forest::worker_task() 451 | /// .build() 452 | /// .on(async { 453 | /// // ... 454 | /// }) 455 | /// .await; 456 | /// } 457 | /// ``` 458 | pub fn build(self) -> Runtime, Registry>, Rx> { 459 | self.build_on(|x| x) 460 | } 461 | 462 | /// Finishes the `ForestLayer` by calling a function to build a `Subscriber`, 463 | /// and returns in as a [`Runtime`]. 464 | /// 465 | /// Unlike [`build_with`], this method composes the layer onto a [`Registry`] 466 | /// prior to passing it into the function. This makes it more convenient for 467 | /// the majority of use cases. 468 | /// 469 | /// This method is useful for advanced configuration of `Subscriber`s as 470 | /// defined in [`tracing-subscriber`s documentation]. For a basic configuration, 471 | /// see the [`build`] method. 472 | /// 473 | /// [`build_with`]: Builder::build_with 474 | /// [`tracing-subscriber`s documentation]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#composing-layers 475 | /// [`build`]: Builder::build 476 | /// 477 | /// # Examples 478 | /// 479 | /// Composing a `Subscriber` with multiple layers: 480 | /// ``` 481 | /// use tracing_forest::{traits::*, util::*}; 482 | /// 483 | /// #[tokio::main] 484 | /// async fn main() { 485 | /// tracing_forest::worker_task() 486 | /// .build_on(|subscriber| subscriber.with(LevelFilter::INFO)) 487 | /// .on(async { 488 | /// // ... 489 | /// }) 490 | /// .await; 491 | /// } 492 | /// ``` 493 | pub fn build_on(self, f: F) -> Runtime 494 | where 495 | F: FnOnce(Layered, Registry>) -> S, 496 | S: Subscriber, 497 | { 498 | self.build_with(|layer| f(Registry::default().with(layer))) 499 | } 500 | 501 | /// Finishes the `ForestLayer` by calling a function to build a `Subscriber`, 502 | /// and returns it as a [`Runtime`]. 503 | /// 504 | /// Unlike [`build_on`], this method passes the `ForestLayer` to the function 505 | /// without presupposing a [`Registry`] base. This makes it the most flexible 506 | /// option for construction. 507 | /// 508 | /// This method is useful for advanced configuration of `Subscriber`s as 509 | /// defined in [`tracing-subscriber`s documentation]. For a basic configuration, 510 | /// see the [`build`] method. 511 | /// 512 | /// [`build_on`]: Builder::build_on 513 | /// [`tracing-subscriber`s documentation]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#composing-layers 514 | /// [`build`]: Builder::build 515 | /// 516 | /// # Examples 517 | /// 518 | /// Composing a `Subscriber` with multiple layers: 519 | /// ``` 520 | /// use tracing_subscriber::Registry; 521 | /// use tracing_forest::{traits::*, util::*}; 522 | /// 523 | /// #[tokio::main] 524 | /// async fn main() { 525 | /// tracing_forest::worker_task() 526 | /// .build_with(|layer: ForestLayer<_, _>| { 527 | /// Registry::default() 528 | /// .with(layer) 529 | /// .with(LevelFilter::INFO) 530 | /// }) 531 | /// .on(async { 532 | /// // ... 533 | /// }) 534 | /// .await; 535 | /// } 536 | /// ``` 537 | pub fn build_with(self, f: F) -> Runtime 538 | where 539 | F: FnOnce(ForestLayer) -> S, 540 | S: Subscriber, 541 | { 542 | let layer = ForestLayer::new(self.sender_processor, self.tag); 543 | let subscriber = f(layer); 544 | 545 | Runtime { 546 | subscriber, 547 | worker_processor: self.worker_processor, 548 | receiver: self.receiver, 549 | is_global: self.is_global, 550 | } 551 | } 552 | } 553 | 554 | /// Execute a `Future` in the context of a subscriber with a `ForestLayer`. 555 | /// 556 | /// This type is returned by [`Builder::build`] and [`Builder::build_with`]. 557 | pub struct Runtime { 558 | subscriber: S, 559 | worker_processor: P, // either `Process<_>` or `Capture` 560 | receiver: UnboundedReceiver, 561 | is_global: bool, 562 | } 563 | 564 | impl Runtime> 565 | where 566 | S: Subscriber + Send + Sync, 567 | P: Processor + Send, 568 | { 569 | /// Execute a future in the context of the configured subscriber. 570 | pub async fn on(self, f: F) -> F::Output { 571 | let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); 572 | let processor = self.worker_processor.0; 573 | let mut receiver = self.receiver; 574 | 575 | let handle = tokio::spawn(async move { 576 | loop { 577 | tokio::select! { 578 | Some(tree) = receiver.recv() => processor.process(tree).expect(fail::PROCESSING_ERROR), 579 | Ok(()) = &mut shutdown_rx => break, 580 | else => break, 581 | } 582 | } 583 | 584 | receiver.close(); 585 | 586 | // Drain any remaining logs in the channel buffer. 587 | while let Ok(tree) = receiver.try_recv() { 588 | processor.process(tree).expect(fail::PROCESSING_ERROR); 589 | } 590 | }); 591 | 592 | let output = { 593 | let _guard = if self.is_global { 594 | tracing::subscriber::set_global_default(self.subscriber) 595 | .expect("global default already set"); 596 | None 597 | } else { 598 | Some(tracing::subscriber::set_default(self.subscriber)) 599 | }; 600 | 601 | f.await 602 | }; 603 | 604 | shutdown_tx.send(()).expect("Shutdown signal couldn't send, this is a bug"); 605 | 606 | handle.await.expect("Failed to join the writing task, this is a bug"); 607 | 608 | output 609 | } 610 | } 611 | 612 | impl Runtime 613 | where 614 | S: Subscriber + Send + Sync, 615 | { 616 | /// Execute a future in the context of the configured subscriber, and return 617 | /// a `Vec` of generated logs. 618 | pub async fn on(self, f: impl Future) -> Vec { 619 | { 620 | let _guard = if self.is_global { 621 | tracing::subscriber::set_global_default(self.subscriber) 622 | .expect("global default already set"); 623 | None 624 | } else { 625 | Some(tracing::subscriber::set_default(self.subscriber)) 626 | }; 627 | 628 | f.await; 629 | } 630 | 631 | let mut receiver = self.receiver; 632 | 633 | receiver.close(); 634 | 635 | iter::from_fn(|| receiver.try_recv().ok()).collect() 636 | } 637 | } 638 | -------------------------------------------------------------------------------- /tracing-forest/src/tag.rs: -------------------------------------------------------------------------------- 1 | //! Supplement events with categorical information. 2 | //! 3 | //! # Use cases for tags 4 | //! 5 | //! Using tags in trace data can improve readability by distinguishing 6 | //! between different kinds of trace data such as requests, internal state, 7 | //! or special operations. An error during a network request could mean a 8 | //! timeout occurred, while an error in the internal state could mean 9 | //! corruption. Both are errors, but one should be treated more seriously than 10 | //! the other, and therefore the two should be easily distinguishable. 11 | //! 12 | //! # How to use tags 13 | //! 14 | //! Every application has its own preferences for how events should be tagged, 15 | //! and this can be set via a custom [`TagParser`] in the [`ForestLayer`]. This 16 | //! works by passing a reference to each incoming [`Event`] to the `TagParser`, 17 | //! which can then be parsed into an `Option` for the `ForestLayer` to use 18 | //! later. 19 | //! 20 | //! Since [`TagParser`] is blanket implemented for all `Fn(&Event) -> Option` 21 | //! the easiest way to create one is to define a top-level function with this type 22 | //! signature. 23 | //! 24 | //! Once the function is defined, it can either be passed directly to [`ForestLayer::new`], 25 | //! or can be passed to [`Builder::set_tag`]. 26 | //! 27 | //! [`ForestLayer`]: crate::layer::ForestLayer 28 | //! [`ForestLayer::new`]: crate::layer::ForestLayer::new 29 | //! [`Builder::set_tag`]: crate::runtime::Builder::set_tag 30 | //! 31 | //! ## Examples 32 | //! 33 | //! Declaring and using a custom `TagParser`. 34 | //! ``` 35 | //! use tracing_forest::{util::*, Tag}; 36 | //! 37 | //! fn simple_tag(event: &Event) -> Option { 38 | //! let target = event.metadata().target(); 39 | //! let level = *event.metadata().level(); 40 | //! 41 | //! Some(match target { 42 | //! "security" if level == Level::ERROR => Tag::builder() 43 | //! .prefix(target) 44 | //! .suffix("critical") 45 | //! .icon('🔐') 46 | //! .build(), 47 | //! "admin" | "request" => Tag::builder().prefix(target).level(level).build(), 48 | //! _ => return None, 49 | //! }) 50 | //! } 51 | //! 52 | //! #[tokio::main] 53 | //! async fn main() { 54 | //! tracing_forest::worker_task() 55 | //! .set_tag(simple_tag) 56 | //! .build() 57 | //! .on(async { 58 | //! // Since `simple_tag` reads from the `target`, we use the target. 59 | //! // If it parsed the event differently, we would reflect that here. 60 | //! info!(target: "admin", "some info for the admin"); 61 | //! error!(target: "request", "the request timed out"); 62 | //! error!(target: "security", "the db has been breached"); 63 | //! info!("no tags here"); 64 | //! }) 65 | //! .await; 66 | //! } 67 | //! ``` 68 | //! ```log 69 | //! INFO i [admin.info]: some info for the admin 70 | //! ERROR 🚨 [request.error]: the request timed out 71 | //! ERROR 🔐 [security.critical]: the db has been breached 72 | //! INFO i [info]: no tags here 73 | //! ``` 74 | use crate::cfg_serde; 75 | use std::fmt; 76 | use tracing::{Event, Level}; 77 | 78 | /// A basic `Copy` type containing information about where an event occurred. 79 | /// 80 | /// See the [module-level documentation](mod@crate::tag) for more details. 81 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] 82 | pub struct Tag { 83 | /// Optional prefix for the tag message 84 | prefix: Option<&'static str>, 85 | 86 | /// Level specifying the importance of the log. 87 | /// 88 | /// This value isn't necessarily "trace", "debug", "info", "warn", or "error", 89 | /// and can be customized. 90 | suffix: &'static str, 91 | 92 | /// An icon, typically emoji, that represents the tag. 93 | icon: char, 94 | } 95 | 96 | impl Tag { 97 | /// Build a new [`Tag`]. 98 | /// 99 | /// # Examples 100 | /// 101 | /// ``` 102 | /// use tracing_forest::Tag; 103 | /// 104 | /// let tag = Tag::builder() 105 | /// .prefix("security") 106 | /// .suffix("critical") 107 | /// .icon('🔐') 108 | /// .build(); 109 | /// ``` 110 | pub fn builder() -> Builder<(), ()> { 111 | Builder { 112 | prefix: None, 113 | suffix: (), 114 | icon: (), 115 | } 116 | } 117 | 118 | /// Returns the prefix, if there is one. 119 | pub const fn prefix(&self) -> Option<&'static str> { 120 | self.prefix 121 | } 122 | 123 | /// Returns the suffix. 124 | pub const fn suffix(&self) -> &'static str { 125 | self.suffix 126 | } 127 | 128 | /// Returns the icon. 129 | pub const fn icon(&self) -> char { 130 | self.icon 131 | } 132 | } 133 | 134 | /// Incrementally construct [`Tag`]s. 135 | /// 136 | /// See [`Tag::builder`] for more details. 137 | #[derive(Copy, Clone, PartialEq, Eq)] 138 | pub struct Builder { 139 | prefix: Option<&'static str>, 140 | suffix: S, 141 | icon: I, 142 | } 143 | 144 | /// A type used by [`Builder`] to indicate that the suffix has been set. 145 | #[derive(Copy, Clone, PartialEq, Eq)] 146 | pub struct Suffix(&'static str); 147 | 148 | /// A type used by [`Builder`] to indicate that the icon has been set. 149 | #[derive(Copy, Clone, PartialEq, Eq)] 150 | pub struct Icon(char); 151 | 152 | impl Builder { 153 | /// Set the prefix. 154 | pub fn prefix(self, prefix: &'static str) -> Builder { 155 | Builder { 156 | prefix: Some(prefix), 157 | ..self 158 | } 159 | } 160 | 161 | /// Set the suffix. 162 | pub fn suffix(self, suffix: &'static str) -> Builder { 163 | Builder { 164 | prefix: self.prefix, 165 | suffix: Suffix(suffix), 166 | icon: self.icon, 167 | } 168 | } 169 | 170 | /// Set the icon. 171 | pub fn icon(self, icon: char) -> Builder { 172 | Builder { 173 | prefix: self.prefix, 174 | suffix: self.suffix, 175 | icon: Icon(icon), 176 | } 177 | } 178 | 179 | /// Set the suffix and icon using defaults for each [`Level`]. 180 | /// 181 | /// If the `Tag` won't have a prefix, then `Tag::from(level)` can be used as 182 | /// a shorter alternative. 183 | pub fn level(self, level: Level) -> Builder { 184 | let (suffix, icon) = match level { 185 | Level::TRACE => ("trace", '📍'), 186 | Level::DEBUG => ("debug", '🐛'), 187 | Level::INFO => ("info", 'i'), 188 | Level::WARN => ("warn", '🚧'), 189 | Level::ERROR => ("error", '🚨'), 190 | }; 191 | 192 | Builder { 193 | prefix: self.prefix, 194 | suffix: Suffix(suffix), 195 | icon: Icon(icon), 196 | } 197 | } 198 | } 199 | 200 | impl Builder { 201 | /// Complete the [`Tag`]. 202 | /// 203 | /// This can only be called once a suffix and an icon have been provided via 204 | /// [`.suffix(...)`](Builder::suffix) and [`.icon(...)`](Builder::icon), or 205 | /// alternatively just [`.level(...)`](Builder::level). 206 | pub fn build(self) -> Tag { 207 | Tag { 208 | prefix: self.prefix, 209 | suffix: self.suffix.0, 210 | icon: self.icon.0, 211 | } 212 | } 213 | } 214 | 215 | impl fmt::Display for Tag { 216 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 217 | if let Some(prefix) = self.prefix { 218 | write!(f, "{}.{}", prefix, self.suffix) 219 | } else { 220 | self.suffix.fmt(f) 221 | } 222 | } 223 | } 224 | 225 | impl From for Tag { 226 | fn from(level: Level) -> Self { 227 | Tag::builder().level(level).build() 228 | } 229 | } 230 | 231 | cfg_serde! { 232 | use serde::{Serialize, Serializer}; 233 | 234 | impl Serialize for Tag { 235 | fn serialize(&self, serializer: S) -> Result { 236 | // This could probably go in a smart string 237 | serializer.serialize_str(&self.to_string()) 238 | } 239 | } 240 | } 241 | 242 | /// A type that can parse [`Tag`]s from Tracing events. 243 | /// 244 | /// This trait is blanket-implemented for all `Fn(&tracing::Event) -> Option`, 245 | /// so top-level `fn`s can be used. 246 | /// 247 | /// See the [module-level documentation](mod@crate::tag) for more details. 248 | pub trait TagParser: 'static { 249 | /// Parse a tag from a [`tracing::Event`] 250 | fn parse(&self, event: &Event) -> Option; 251 | } 252 | 253 | /// A `TagParser` that always returns `None`. 254 | #[derive(Clone, Debug)] 255 | pub struct NoTag; 256 | 257 | impl TagParser for NoTag { 258 | fn parse(&self, _event: &Event) -> Option { 259 | None 260 | } 261 | } 262 | 263 | impl TagParser for F 264 | where 265 | F: 'static + Fn(&Event) -> Option, 266 | { 267 | fn parse(&self, event: &Event) -> Option { 268 | self(event) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /tracing-forest/src/tree/field.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "smallvec")] 2 | pub(crate) type FieldSet = smallvec::SmallVec<[Field; 3]>; 3 | #[cfg(not(feature = "smallvec"))] 4 | pub(crate) type FieldSet = Vec; 5 | 6 | /// A key-value pair recorded from trace data. 7 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 8 | pub struct Field { 9 | key: &'static str, 10 | value: String, 11 | } 12 | 13 | impl Field { 14 | pub(crate) fn new(key: &'static str, value: String) -> Self { 15 | Field { key, value } 16 | } 17 | 18 | /// Returns the field's key. 19 | pub fn key(&self) -> &'static str { 20 | self.key 21 | } 22 | 23 | /// Returns the field's value. 24 | pub fn value(&self) -> &str { 25 | &self.value 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tracing-forest/src/tree/mod.rs: -------------------------------------------------------------------------------- 1 | //! The core tree structure of `tracing-forest`. 2 | //! 3 | //! This module provides methods used for log inspection when using [`capture`]. 4 | //! It consists of three types: [`Tree`], [`Span`], and [`Event`]. 5 | //! 6 | //! [`capture`]: crate::runtime::capture 7 | use crate::tag::Tag; 8 | #[cfg(feature = "chrono")] 9 | use chrono::{DateTime, Utc}; 10 | #[cfg(feature = "serde")] 11 | use serde::Serialize; 12 | use std::time::Duration; 13 | use thiserror::Error; 14 | use tracing::Level; 15 | #[cfg(feature = "uuid")] 16 | use uuid::Uuid; 17 | 18 | mod field; 19 | #[cfg(feature = "serde")] 20 | mod ser; 21 | 22 | pub use field::Field; 23 | pub(crate) use field::FieldSet; 24 | 25 | /// A node in the log tree, consisting of either a [`Span`] or an [`Event`]. 26 | /// 27 | /// The inner types can be extracted through a `match` statement. Alternatively, 28 | /// the [`event`] and [`span`] methods provide a more ergonomic way to access the 29 | /// inner types in unit tests when combined with the [`capture`] function. 30 | /// 31 | /// [`event`]: Tree::event 32 | /// [`span`]: Tree::span 33 | /// [`capture`]: crate::runtime::capture 34 | #[derive(Clone, Debug)] 35 | #[cfg_attr(feature = "serde", derive(Serialize))] 36 | #[allow(clippy::large_enum_variant)] // https://github.com/rust-lang/rust-clippy/issues/9798 37 | pub enum Tree { 38 | /// An [`Event`] leaf node. 39 | Event(Event), 40 | 41 | /// A [`Span`] inner node. 42 | Span(Span), 43 | } 44 | 45 | /// A leaf node in the log tree carrying information about a Tracing event. 46 | #[derive(Clone, Debug)] 47 | #[cfg_attr(feature = "serde", derive(Serialize))] 48 | pub struct Event { 49 | /// Shared fields between events and spans. 50 | #[cfg_attr(feature = "serde", serde(flatten))] 51 | pub(crate) shared: Shared, 52 | 53 | /// The message associated with the event. 54 | pub(crate) message: Option, 55 | 56 | /// The tag that the event was collected with. 57 | pub(crate) tag: Option, 58 | } 59 | 60 | /// An internal node in the log tree carrying information about a Tracing span. 61 | #[derive(Clone, Debug)] 62 | #[cfg_attr(feature = "serde", derive(Serialize))] 63 | pub struct Span { 64 | /// Shared fields between events and spans. 65 | #[cfg_attr(feature = "serde", serde(flatten))] 66 | pub(crate) shared: Shared, 67 | 68 | /// The name of the span. 69 | pub(crate) name: &'static str, 70 | 71 | /// The total duration the span was open for. 72 | #[cfg_attr( 73 | feature = "serde", 74 | serde(rename = "nanos_total", serialize_with = "ser::nanos") 75 | )] 76 | pub(crate) total_duration: Duration, 77 | 78 | /// The total duration inner spans were open for. 79 | #[cfg_attr( 80 | feature = "serde", 81 | serde(rename = "nanos_nested", serialize_with = "ser::nanos") 82 | )] 83 | pub(crate) inner_duration: Duration, 84 | 85 | /// Events and spans collected while the span was open. 86 | pub(crate) nodes: Vec, 87 | } 88 | 89 | #[derive(Clone, Debug)] 90 | #[cfg_attr(feature = "serde", derive(Serialize))] 91 | pub(crate) struct Shared { 92 | /// The ID of the event or span. 93 | #[cfg(feature = "uuid")] 94 | pub(crate) uuid: Uuid, 95 | 96 | /// When the event occurred or when the span opened. 97 | #[cfg(feature = "chrono")] 98 | #[cfg_attr(feature = "serde", serde(serialize_with = "ser::timestamp"))] 99 | pub(crate) timestamp: DateTime, 100 | 101 | /// The level the event or span occurred at. 102 | #[cfg_attr(feature = "serde", serde(serialize_with = "ser::level"))] 103 | pub(crate) level: Level, 104 | 105 | /// Key-value data. 106 | #[cfg_attr(feature = "serde", serde(serialize_with = "ser::fields"))] 107 | pub(crate) fields: FieldSet, 108 | } 109 | 110 | /// Error returned by [`Tree::event`][event]. 111 | /// 112 | /// [event]: crate::tree::Tree::event 113 | #[derive(Error, Debug)] 114 | #[error("Expected an event, found a span")] 115 | pub struct ExpectedEventError(()); 116 | 117 | /// Error returned by [`Tree::span`][span]. 118 | /// 119 | /// [span]: crate::tree::Tree::span 120 | #[derive(Error, Debug)] 121 | #[error("Expected a span, found an event")] 122 | pub struct ExpectedSpanError(()); 123 | 124 | impl Tree { 125 | /// Returns a reference to the inner [`Event`] if the tree is an event. 126 | /// 127 | /// # Errors 128 | /// 129 | /// This function returns an error if the `Tree` contains the `Span` variant. 130 | /// 131 | /// # Examples 132 | /// 133 | /// Inspecting a `Tree` returned from [`capture`]: 134 | /// ``` 135 | /// use tracing::{info, info_span}; 136 | /// use tracing_forest::tree::{Tree, Event}; 137 | /// 138 | /// #[tokio::main] 139 | /// async fn main() -> Result<(), Box> { 140 | /// let logs: Vec = tracing_forest::capture() 141 | /// .build() 142 | /// .on(async { 143 | /// info!("some information"); 144 | /// }) 145 | /// .await; 146 | /// 147 | /// assert!(logs.len() == 1); 148 | /// 149 | /// let event: &Event = logs[0].event()?; 150 | /// assert!(event.message() == Some("some information")); 151 | /// 152 | /// Ok(()) 153 | /// } 154 | /// ``` 155 | /// 156 | /// [`capture`]: crate::runtime::capture 157 | pub fn event(&self) -> Result<&Event, ExpectedEventError> { 158 | match self { 159 | Tree::Event(event) => Ok(event), 160 | Tree::Span(_) => Err(ExpectedEventError(())), 161 | } 162 | } 163 | 164 | /// Returns a reference to the inner [`Span`] if the tree is a span. 165 | /// 166 | /// # Errors 167 | /// 168 | /// This function returns an error if the `Tree` contains the `Event` variant. 169 | /// 170 | /// # Examples 171 | /// 172 | /// Inspecting a `Tree` returned from [`capture`]: 173 | /// ``` 174 | /// use tracing::{info, info_span}; 175 | /// use tracing_forest::tree::{Tree, Span}; 176 | /// 177 | /// #[tokio::main] 178 | /// async fn main() -> Result<(), Box> { 179 | /// let logs: Vec = tracing_forest::capture() 180 | /// .build() 181 | /// .on(async { 182 | /// info_span!("my_span").in_scope(|| { 183 | /// info!("inside the span"); 184 | /// }); 185 | /// }) 186 | /// .await; 187 | /// 188 | /// assert!(logs.len() == 1); 189 | /// 190 | /// let my_span: &Span = logs[0].span()?; 191 | /// assert!(my_span.name() == "my_span"); 192 | /// Ok(()) 193 | /// } 194 | /// ``` 195 | /// 196 | /// [`capture`]: crate::runtime::capture 197 | pub fn span(&self) -> Result<&Span, ExpectedSpanError> { 198 | match self { 199 | Tree::Event(_) => Err(ExpectedSpanError(())), 200 | Tree::Span(span) => Ok(span), 201 | } 202 | } 203 | } 204 | 205 | impl Event { 206 | /// Returns the event's [`Uuid`]. 207 | #[cfg(feature = "uuid")] 208 | pub fn uuid(&self) -> Uuid { 209 | self.shared.uuid 210 | } 211 | 212 | /// Returns the [`DateTime`] that the event occurred at. 213 | #[cfg(feature = "chrono")] 214 | pub fn timestamp(&self) -> DateTime { 215 | self.shared.timestamp 216 | } 217 | 218 | /// Returns the event's [`Level`]. 219 | pub fn level(&self) -> Level { 220 | self.shared.level 221 | } 222 | 223 | /// Returns the event's message, if there is one. 224 | pub fn message(&self) -> Option<&str> { 225 | self.message.as_deref() 226 | } 227 | 228 | /// Returns the event's [`Tag`], if there is one. 229 | pub fn tag(&self) -> Option { 230 | self.tag 231 | } 232 | 233 | /// Returns the event's fields. 234 | pub fn fields(&self) -> &[Field] { 235 | &self.shared.fields 236 | } 237 | } 238 | 239 | impl Span { 240 | pub(crate) fn new(shared: Shared, name: &'static str) -> Self { 241 | Span { 242 | shared, 243 | name, 244 | total_duration: Duration::ZERO, 245 | inner_duration: Duration::ZERO, 246 | nodes: Vec::new(), 247 | } 248 | } 249 | 250 | /// Returns the span's [`Uuid`]. 251 | #[cfg(feature = "uuid")] 252 | pub fn uuid(&self) -> Uuid { 253 | self.shared.uuid 254 | } 255 | 256 | /// Returns the [`DateTime`] that the span occurred at. 257 | #[cfg(feature = "chrono")] 258 | pub fn timestamp(&self) -> DateTime { 259 | self.shared.timestamp 260 | } 261 | 262 | /// Returns the span's [`Level`]. 263 | pub fn level(&self) -> Level { 264 | self.shared.level 265 | } 266 | 267 | /// Returns the span's name. 268 | pub fn name(&self) -> &str { 269 | self.name 270 | } 271 | 272 | /// Returns the span's fields. 273 | pub fn fields(&self) -> &[Field] { 274 | &self.shared.fields 275 | } 276 | 277 | /// Returns the span's child trees. 278 | pub fn nodes(&self) -> &[Tree] { 279 | &self.nodes 280 | } 281 | 282 | /// Returns the total duration the span was entered for. 283 | /// 284 | /// If the span was used to instrument a `Future`, this only accounts for the 285 | /// time spent polling the `Future`. For example, time spent sleeping will 286 | /// not be accounted for. 287 | pub fn total_duration(&self) -> Duration { 288 | self.total_duration 289 | } 290 | 291 | /// Returns the duration that inner spans were opened for. 292 | pub fn inner_duration(&self) -> Duration { 293 | self.inner_duration 294 | } 295 | 296 | /// Returns the duration this span was entered, but not in any child spans. 297 | pub fn base_duration(&self) -> Duration { 298 | self.total_duration - self.inner_duration 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /tracing-forest/src/tree/ser.rs: -------------------------------------------------------------------------------- 1 | use crate::tree::FieldSet; 2 | #[cfg(feature = "chrono")] 3 | use chrono::{DateTime, Utc}; 4 | use serde::ser::{SerializeMap, Serializer}; 5 | use std::time::Duration; 6 | use tracing::Level; 7 | 8 | #[allow(clippy::trivially_copy_pass_by_ref)] 9 | pub(super) fn level(level: &Level, serializer: S) -> Result { 10 | serializer.serialize_str(level.as_str()) 11 | } 12 | 13 | pub(super) fn nanos(duration: &Duration, serializer: S) -> Result { 14 | serializer.serialize_u128(duration.as_nanos()) 15 | } 16 | 17 | pub(super) fn fields(fields: &FieldSet, serializer: S) -> Result { 18 | let mut model = serializer.serialize_map(Some(fields.len()))?; 19 | for field in fields { 20 | model.serialize_entry(field.key(), field.value())?; 21 | } 22 | model.end() 23 | } 24 | 25 | #[cfg(feature = "chrono")] 26 | pub(super) fn timestamp( 27 | timestamp: &DateTime, 28 | serializer: S, 29 | ) -> Result { 30 | serializer.serialize_str(×tamp.to_rfc3339()) 31 | } 32 | -------------------------------------------------------------------------------- /tracing-forest/tests/captured.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "tokio")] 2 | use std::error::Error; 3 | use tokio::time::Duration; 4 | use tracing_forest::{traits::*, util::*}; 5 | 6 | #[tokio::test] 7 | async fn test_filtering() -> Result<(), Box> { 8 | let logs = tracing_forest::capture() 9 | .build_on(|subscriber| subscriber.with(LevelFilter::INFO)) 10 | .on(async { 11 | trace!("unimportant information"); 12 | info!("important information"); 13 | }) 14 | .await; 15 | 16 | assert!(logs.len() == 1); 17 | 18 | let info = logs[0].event()?; 19 | 20 | assert!(info.message() == Some("important information")); 21 | 22 | Ok(()) 23 | } 24 | 25 | #[tokio::test] 26 | async fn duration_checked_sub() -> Result<(), Box> { 27 | let logs = tracing_forest::capture() 28 | .build() 29 | .on(async { 30 | let parent = info_span!("parent"); 31 | info_span!(parent: &parent, "child").in_scope(|| { 32 | // cursed blocking in async lol 33 | std::thread::sleep(Duration::from_millis(100)); 34 | }); 35 | }) 36 | .await; 37 | 38 | assert!(logs.len() == 1); 39 | 40 | let parent = logs[0].span()?; 41 | assert!(parent.total_duration() >= parent.inner_duration()); 42 | 43 | Ok(()) 44 | } 45 | 46 | #[tokio::test] 47 | async fn try_get_wrong_tree_variant() -> Result<(), Box> { 48 | let logs = tracing_forest::capture() 49 | .build() 50 | .on(async { 51 | info!("This is an event"); 52 | info_span!("This is a span").in_scope(|| { 53 | info!("hello again"); 54 | }); 55 | }) 56 | .await; 57 | 58 | assert!(logs.len() == 2); 59 | 60 | assert!(logs[0].span().is_err()); 61 | assert!(logs[0].event().is_ok()); 62 | 63 | assert!(logs[1].event().is_err()); 64 | let span = logs[1].span()?; 65 | 66 | assert!(span.nodes().len() == 1); 67 | let node = &span.nodes()[0]; 68 | 69 | assert!(node.span().is_err()); 70 | assert!(node.event().is_ok()); 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /tracing-forest/tests/demo.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde")] 2 | use tracing_forest::{traits::*, util::*, Printer, Tag}; 3 | use tracing_subscriber::Registry; 4 | 5 | #[test] 6 | #[ignore] 7 | fn test_manual_with_json() { 8 | let processor = Printer::new().formatter(serde_json::to_string_pretty); 9 | let layer = ForestLayer::from(processor); 10 | let subscriber = Registry::default().with(layer); 11 | tracing::subscriber::with_default(subscriber, || { 12 | info!("hello, world!"); 13 | info_span!("my-span", answer = 42).in_scope(|| { 14 | info!("wassup"); 15 | }) 16 | }); 17 | } 18 | 19 | fn pretty_tag(event: &Event) -> Option { 20 | let level = *event.metadata().level(); 21 | let target = event.metadata().target(); 22 | 23 | Some(match target { 24 | "security" if level == Level::ERROR => Tag::builder() 25 | .icon('🔐') 26 | .prefix(target) 27 | .suffix("critical") 28 | .build(), 29 | "security" if level == Level::INFO => Tag::builder() 30 | .icon('🔓') 31 | .prefix(target) 32 | .suffix("access") 33 | .build(), 34 | "admin" | "request" | "filter" => Tag::builder().prefix(target).level(level).build(), 35 | _ => return None, 36 | }) 37 | } 38 | 39 | #[test] 40 | fn pretty_example() { 41 | let _guard = tracing::subscriber::set_default({ 42 | let layer = ForestLayer::new(Printer::new(), pretty_tag); 43 | 44 | Registry::default().with(layer) 45 | }); 46 | 47 | info_span!("try_from_entry_ro").in_scope(|| { 48 | info_span!("server::internal_search").in_scope(|| { 49 | info!(target: "filter", "Some filter info..."); 50 | info_span!("server::search").in_scope(|| { 51 | info_span!("be::search").in_scope(|| { 52 | info_span!("be::search -> filter2idl", term="bobby", verbose=false).in_scope(|| { 53 | info_span!("be::idl_arc_sqlite::get_idl").in_scope(|| { 54 | info!(target: "filter", "Some filter info..."); 55 | }); 56 | info_span!("be::idl_arc_sqlite::get_idl").in_scope(|| { 57 | error!(target: "admin", "On no, an admin error occurred :("); 58 | debug!("An untagged debug log"); 59 | info!(target: "admin", alive = false, status = "sad", "there's been a big mistake") 60 | }); 61 | }); 62 | }); 63 | info_span!("be::idl_arc_sqlite::get_identry").in_scope(|| { 64 | error!(target: "security", "A security critical log"); 65 | info!(target: "security", "A security access log"); 66 | }); 67 | }); 68 | info_span!("server::search").in_scope(|| { 69 | warn!(target: "filter", "Some filter warning"); 70 | }); 71 | 72 | }); 73 | trace!("Finished!"); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /tracing-forest/tests/id.rs: -------------------------------------------------------------------------------- 1 | //! Tests for retrieving the id when there is none. 2 | #![cfg(feature = "tokio")] 3 | use std::panic; 4 | use tracing::dispatcher::DefaultGuard; 5 | use tracing_forest::{traits::*, util::*}; 6 | use tracing_subscriber::Registry; 7 | use uuid::Uuid; 8 | 9 | fn init() -> DefaultGuard { 10 | let layer = ForestLayer::sink(); 11 | let subscriber = Registry::default().with(layer); 12 | tracing::subscriber::set_default(subscriber) 13 | } 14 | 15 | // TODO(Quinn): I think we should switch to using `Result` instead of panicking 16 | 17 | #[test] 18 | fn test_panic_get_id_not_in_span() { 19 | let _guard = init(); 20 | panic::set_hook(Box::new(|_| {})); 21 | assert!(panic::catch_unwind(tracing_forest::id).is_err()); 22 | } 23 | 24 | #[test] 25 | fn test_panic_get_id_not_in_subscriber() { 26 | panic::set_hook(Box::new(|_| {})); 27 | assert!(panic::catch_unwind(tracing_forest::id).is_err()); 28 | } 29 | 30 | #[test] 31 | fn test_panic_get_id_after_close() { 32 | let _guard = init(); 33 | 34 | let uuid = Uuid::new_v4(); 35 | info_span!("in a span", %uuid).in_scope(|| { 36 | let _ = tracing_forest::id(); 37 | }); 38 | panic::set_hook(Box::new(|_| {})); 39 | assert!(panic::catch_unwind(tracing_forest::id).is_err()); 40 | } 41 | 42 | #[test] 43 | fn test_consistent_retrieval() { 44 | let _guard = init(); 45 | info_span!("my_span").in_scope(|| { 46 | let id1 = tracing_forest::id(); 47 | let id2 = tracing_forest::id(); 48 | assert!(id1 == id2); 49 | }); 50 | } 51 | 52 | #[test] 53 | fn test_span_macros() { 54 | let _guard = init(); 55 | let uuid = Uuid::new_v4(); 56 | 57 | trace_span!("my_span", %uuid).in_scope(|| { 58 | assert_eq!(uuid, tracing_forest::id()); 59 | }); 60 | trace_span!("my_span", %uuid, ans = 42).in_scope(|| { 61 | assert_eq!(uuid, tracing_forest::id()); 62 | }); 63 | debug_span!("my_span", %uuid).in_scope(|| { 64 | assert_eq!(uuid, tracing_forest::id()); 65 | }); 66 | debug_span!("my_span", %uuid, ans = 42).in_scope(|| { 67 | assert_eq!(uuid, tracing_forest::id()); 68 | }); 69 | info_span!("my_span", %uuid).in_scope(|| { 70 | assert_eq!(uuid, tracing_forest::id()); 71 | }); 72 | info_span!("my_span", %uuid, ans = 42).in_scope(|| { 73 | assert_eq!(uuid, tracing_forest::id()); 74 | }); 75 | warn_span!("my_span", %uuid).in_scope(|| { 76 | assert_eq!(uuid, tracing_forest::id()); 77 | }); 78 | warn_span!("my_span", %uuid, ans = 42).in_scope(|| { 79 | assert_eq!(uuid, tracing_forest::id()); 80 | }); 81 | error_span!("my_span", %uuid).in_scope(|| { 82 | assert_eq!(uuid, tracing_forest::id()); 83 | }); 84 | error_span!("my_span", %uuid, ans = 42).in_scope(|| { 85 | assert_eq!(uuid, tracing_forest::id()); 86 | }); 87 | } 88 | 89 | #[cfg(feature = "tokio")] 90 | #[tokio::test(flavor = "current_thread")] 91 | async fn test_many_tasks() { 92 | let _guard = init(); 93 | 94 | let mut handles = vec![]; 95 | 96 | for _ in 0..10 { 97 | handles.push(tokio::spawn(async { 98 | let id1 = Uuid::new_v4(); 99 | let id2 = Uuid::new_v4(); 100 | 101 | async { 102 | assert!(id1 == tracing_forest::id()); 103 | async { 104 | assert!(id2 == tracing_forest::id()); 105 | } 106 | .instrument(info_span!("inner", uuid = %id2)) 107 | .await; 108 | assert!(id1 == tracing_forest::id()); 109 | } 110 | .instrument(info_span!("outer", uuid = %id1)) 111 | .await; 112 | })); 113 | } 114 | 115 | for handle in handles { 116 | handle.await.expect("failed to join task"); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tracing-forest/tests/set_global.rs: -------------------------------------------------------------------------------- 1 | //! This test sets a global subscriber so that the sender half of the channel 2 | //! inside the subscriber is never dropped. It tests that the receiver can be closed 3 | //! manually via the shutdown signal that is sent once the `Future` passed to 4 | //! `Runtime::on` finishes. 5 | //! 6 | //! Addresses https://github.com/QnnOkabayashi/tracing-forest/issues/4 7 | #![cfg(feature = "tokio")] 8 | use tokio::time::{timeout, Duration}; 9 | 10 | #[tokio::test(flavor = "multi_thread")] 11 | async fn join_hanging_task() { 12 | let f = tracing_forest::worker_task() 13 | .set_global(true) 14 | .build() 15 | .on(async { 16 | // nothing 17 | }); 18 | 19 | timeout(Duration::from_millis(500), f) 20 | .await 21 | .expect("Shutdown signal wasn't sent"); 22 | } 23 | -------------------------------------------------------------------------------- /tracing-forest/tests/simulate_connections.rs: -------------------------------------------------------------------------------- 1 | //! This test simulates processing many clients on multithreaded task, where 2 | //! each client has many `await` points. 3 | //! 4 | //! It is intended to maximize the amount of concurrent operations to demonstrate 5 | //! that `tracing-forest` does, in fact, keep each one coherent. 6 | #![cfg(feature = "tokio")] 7 | use rand::Rng; 8 | use tokio::time::{sleep, Duration}; 9 | use tracing_forest::{traits::*, util::*}; 10 | 11 | type Result = core::result::Result>; 12 | 13 | async fn sleep_rand() { 14 | let millis = rand::thread_rng().gen_range(10..200); 15 | sleep(Duration::from_millis(millis)).await 16 | } 17 | 18 | #[tokio::test(flavor = "multi_thread")] 19 | async fn test_n_tasks_random_sleeps() -> Result<()> { 20 | let num_clients = 10; 21 | 22 | let logs = tracing_forest::capture() 23 | .set_global(true) 24 | .build() 25 | .on(async { 26 | let mut clients = Vec::with_capacity(10); 27 | for client in 0..num_clients { 28 | let handle = tokio::spawn( 29 | async move { 30 | info!(%client, "new connection"); 31 | 32 | async { 33 | info!(%client, "sent request"); 34 | sleep_rand().await; 35 | info!(%client, "received response"); 36 | } 37 | .instrument(trace_span!("request")) 38 | .await; 39 | 40 | sleep_rand().await; 41 | 42 | async { 43 | info!(%client, "sending response"); 44 | sleep_rand().await; 45 | info!(%client, "response sent"); 46 | } 47 | .instrument(trace_span!("response")) 48 | .await; 49 | } 50 | .instrument(trace_span!("connection")), 51 | ); 52 | 53 | clients.push(handle); 54 | } 55 | 56 | for client in clients { 57 | client.await.unwrap(); 58 | } 59 | }) 60 | .await; 61 | 62 | assert!(logs.len() == num_clients); 63 | 64 | for tree in logs { 65 | let connection = tree.span()?; 66 | assert!(connection.name() == "connection"); 67 | assert!(connection.nodes().len() == 3); 68 | 69 | let client_connect = connection.nodes()[0].event()?; 70 | assert!(client_connect.message() == Some("new connection")); 71 | assert!(client_connect.fields().len() == 1); 72 | 73 | let field = &client_connect.fields()[0]; 74 | assert!(field.key() == "client"); 75 | let client_id = field.value(); 76 | 77 | for (child, action) in connection.nodes()[1..].iter().zip(["request", "response"]) { 78 | let span = child.span()?; 79 | assert!(span.name() == action); 80 | assert!(span.nodes().len() == 2); 81 | 82 | for child in span.nodes() { 83 | let event = child.event()?; 84 | assert!(event.fields().len() == 1); 85 | 86 | let field = &event.fields()[0]; 87 | assert!(field.key() == "client"); 88 | assert!(field.value() == client_id); 89 | } 90 | } 91 | } 92 | 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /tracing-forest/tests/simulate_connections_blocking.rs: -------------------------------------------------------------------------------- 1 | //! This test simulates processing many clients on multithreaded task, where 2 | //! each client has many `await` points. 3 | //! 4 | //! It is intended to maximize the amount of concurrent operations to demonstrate 5 | //! that `tracing-forest` does, in fact, keep each one coherent. 6 | #![cfg(feature = "tokio")] 7 | use tracing_forest::util::*; 8 | 9 | type Result = core::result::Result>; 10 | 11 | #[tokio::test(flavor = "multi_thread")] 12 | async fn test_n_tasks_blocking() -> Result<()> { 13 | let num_clients = 10; 14 | 15 | let logs = tracing_forest::capture() 16 | .set_global(true) 17 | .build() 18 | .on(async { 19 | let mut clients = Vec::with_capacity(10); 20 | for client in 0..num_clients { 21 | let handle = tokio::task::spawn_blocking(move || { 22 | info_span!("connection").in_scope(|| { 23 | info!(%client, "new connection"); 24 | 25 | info_span!("request").in_scope(|| { 26 | info!(%client, "sent request"); 27 | info!(%client, "received response"); 28 | }); 29 | 30 | info_span!("response").in_scope(|| { 31 | info!(%client, "sending response"); 32 | info!(%client, "response sent"); 33 | }); 34 | }) 35 | }); 36 | 37 | clients.push(handle); 38 | } 39 | 40 | for client in clients { 41 | client.await.unwrap(); 42 | } 43 | }) 44 | .await; 45 | 46 | assert!(logs.len() == num_clients); 47 | 48 | for tree in logs { 49 | let connection = tree.span()?; 50 | assert!(connection.name() == "connection"); 51 | assert!(connection.nodes().len() == 3); 52 | 53 | let client_connect = connection.nodes()[0].event()?; 54 | assert!(client_connect.message() == Some("new connection")); 55 | assert!(client_connect.fields().len() == 1); 56 | 57 | let field = &client_connect.fields()[0]; 58 | assert!(field.key() == "client"); 59 | let client_id = field.value(); 60 | 61 | for (child, action) in connection.nodes()[1..].iter().zip(["request", "response"]) { 62 | let span = child.span()?; 63 | assert!(span.name() == action); 64 | assert!(span.nodes().len() == 2); 65 | 66 | for child in span.nodes() { 67 | let event = child.event()?; 68 | assert!(event.fields().len() == 1); 69 | 70 | let field = &event.fields()[0]; 71 | assert!(field.key() == "client"); 72 | assert!(field.value() == client_id); 73 | } 74 | } 75 | } 76 | 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /tracing-forest/tests/spawned_tasks.rs: -------------------------------------------------------------------------------- 1 | //! This test simulates capturing trace data from different threads, and ensures 2 | //! that everything is still captured by the subscriber. 3 | //! 4 | //! Addresses https://github.com/QnnOkabayashi/tracing-forest/issues/3 5 | #![cfg(feature = "tokio")] 6 | use tokio::time::{sleep, Duration}; 7 | use tracing::info; 8 | 9 | type Result = core::result::Result>; 10 | 11 | #[tokio::test(flavor = "multi_thread")] 12 | async fn spawned_tasks() -> Result<()> { 13 | let logs = tracing_forest::capture() 14 | .set_global(true) 15 | .build() 16 | .on(async { 17 | info!("Waiting on signal"); 18 | let handle = tokio::spawn(async { 19 | info!("Test message"); 20 | }); 21 | sleep(Duration::from_millis(100)).await; 22 | handle.await.unwrap(); 23 | info!("Stopping"); 24 | }) 25 | .await; 26 | 27 | assert!(logs.len() == 3); 28 | 29 | let waiting = logs[0].event()?; 30 | assert!(waiting.message() == Some("Waiting on signal")); 31 | 32 | let test = logs[1].event()?; 33 | assert!(test.message() == Some("Test message")); 34 | 35 | let stopping = logs[2].event()?; 36 | assert!(stopping.message() == Some("Stopping")); 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /tracing-forest/tests/tag.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "tokio")] 2 | use tracing_forest::{util::*, Tag}; 3 | 4 | fn kanidm_tag(event: &Event) -> Option { 5 | let target = event.metadata().target(); 6 | let level = *event.metadata().level(); 7 | 8 | Some(match target { 9 | "security" if level == Level::ERROR => Tag::builder() 10 | .prefix(target) 11 | .suffix("critical") 12 | .icon('🔐') 13 | .build(), 14 | "admin" | "request" => Tag::builder().prefix(target).level(level).build(), 15 | _ => return None, 16 | }) 17 | } 18 | 19 | #[tokio::test] 20 | async fn test_kanidm_tag() -> Result<(), Box> { 21 | let logs = tracing_forest::capture() 22 | .set_tag(kanidm_tag) 23 | .build() 24 | .on(async { 25 | info!(target: "admin", "some info for the admin"); 26 | error!(target: "request", "the request timed out"); 27 | error!(target: "security", "the db has been breached"); 28 | info!("no tags here"); 29 | info!(target: "unrecognized", "unrecognizable tag"); 30 | }) 31 | .await; 32 | 33 | assert!(logs.len() == 5); 34 | 35 | let admin_info = logs[0].event()?; 36 | assert!(admin_info.message() == Some("some info for the admin")); 37 | assert!(admin_info.tag().unwrap().to_string() == "admin.info"); 38 | 39 | let request_error = logs[1].event()?; 40 | assert!(request_error.message() == Some("the request timed out")); 41 | assert!(request_error.tag().unwrap().to_string() == "request.error"); 42 | 43 | let security_critical = logs[2].event()?; 44 | assert!(security_critical.message() == Some("the db has been breached")); 45 | assert!(security_critical.tag().unwrap().to_string() == "security.critical"); 46 | 47 | let no_tags = logs[3].event()?; 48 | assert!(no_tags.message() == Some("no tags here")); 49 | assert!(no_tags.tag().is_none()); 50 | 51 | let unrecognized = logs[4].event()?; 52 | assert!(unrecognized.message() == Some("unrecognizable tag")); 53 | assert!(unrecognized.tag().is_none()); 54 | 55 | Ok(()) 56 | } 57 | --------------------------------------------------------------------------------