├── .circleci └── config.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples └── valuable.rs ├── images ├── ConsoleBunyanOutput.png └── ConsoleOutput.png ├── src ├── formatting_layer.rs ├── lib.rs └── storage_layer.rs └── tests ├── e2e.rs └── mock_writer.rs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build-and-test-default-features: 4 | docker: 5 | - image: cimg/rust:1.65 6 | environment: 7 | # Fail the build if there are warnings 8 | RUSTFLAGS: '-D warnings' 9 | steps: 10 | - checkout 11 | - run: 12 | name: Version information 13 | command: rustc --version; cargo --version; rustup --version 14 | - run: 15 | name: Calculate dependencies 16 | command: cargo generate-lockfile 17 | - restore_cache: 18 | keys: 19 | - v1-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} 20 | - run: 21 | name: Build all targets 22 | command: cargo build 23 | - save_cache: 24 | paths: 25 | - /usr/local/cargo/registry 26 | - target/debug/.fingerprint 27 | - target/debug/build 28 | - target/debug/deps 29 | key: v1-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} 30 | - run: 31 | name: Run all tests 32 | command: cargo test 33 | 34 | build-and-test-feature-valuable: 35 | docker: 36 | - image: cimg/rust:1.65 37 | environment: 38 | # Fail the build if there are warnings, and set the rustc cfg 39 | # flag `tracing_unstable` that `tracing` requires to use the 40 | # feature `valuable` 41 | RUSTFLAGS: '-D warnings --cfg tracing_unstable' 42 | steps: 43 | - checkout 44 | - run: 45 | name: Version information 46 | command: rustc --version; cargo --version; rustup --version 47 | - run: 48 | name: Calculate dependencies 49 | command: cargo generate-lockfile 50 | - restore_cache: 51 | keys: 52 | - v1-cargo-cache-{{ arch }}-feature-valuable-{{ checksum "Cargo.lock" }} 53 | - run: 54 | name: Build all targets 55 | command: cargo build --features "valuable valuable/derive" 56 | - save_cache: 57 | paths: 58 | - /usr/local/cargo/registry 59 | - target/debug/.fingerprint 60 | - target/debug/build 61 | - target/debug/deps 62 | key: v1-cargo-cache-{{ arch }}-feature-valuable-{{ checksum "Cargo.lock" }} 63 | - run: 64 | name: Run all tests 65 | command: cargo test --features "valuable valuable/derive" 66 | - run: 67 | # Try to run examples/valuable explicitly. If the features are incorrect 68 | # it will be skipped silently. 69 | name: Run valuable example 70 | command: cargo run --example valuable --features "valuable valuable/derive" 71 | 72 | security: 73 | docker: 74 | - image: cimg/rust:1.65 75 | steps: 76 | - checkout 77 | - run: 78 | name: Version information 79 | command: rustc --version; cargo --version; rustup --version 80 | - run: 81 | name: Install dependency auditing tool 82 | command: cargo install cargo-audit 83 | - run: 84 | name: Check for known security issues in dependencies 85 | command: cargo audit 86 | 87 | format-and-lint: 88 | docker: 89 | - image: cimg/rust:1.65 90 | steps: 91 | - checkout 92 | - run: 93 | name: Version information 94 | command: rustc --version; cargo --version; rustup --version 95 | - run: 96 | name: Install formatter 97 | command: rustup component add rustfmt 98 | - run: 99 | name: Install Clippy 100 | command: rustup component add clippy 101 | - run: 102 | name: Formatting 103 | command: cargo fmt --all -- --check 104 | - run: 105 | name: Linting 106 | command: cargo clippy -- -D warnings 107 | 108 | workflows: 109 | version: 2 110 | build-test: 111 | jobs: 112 | - build-and-test-default-features: 113 | filters: 114 | tags: 115 | only: /.*/ 116 | - build-and-test-feature-valuable: 117 | filters: 118 | tags: 119 | only: /.*/ 120 | - security: 121 | filters: 122 | tags: 123 | only: /.*/ 124 | - format-and-lint: 125 | filters: 126 | tags: 127 | only: /.*/ 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rust@lpalmieri.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tracing-bunyan-formatter" 3 | version = "0.3.10" 4 | authors = ["Luca Palmieri "] 5 | edition = "2018" 6 | 7 | license = "MIT/Apache-2.0" 8 | 9 | repository = "https://github.com/LukeMathWalker/tracing-bunyan-formatter" 10 | documentation = "https://docs.rs/tracing-bunyan-formatter/" 11 | readme = "README.md" 12 | 13 | description = "A Bunyan formatter for the tracing crate" 14 | 15 | keywords = ["logging", "metrics", "tracing", "bunyan", "subscriber"] 16 | categories = ["development-tools::profiling", "development-tools::debugging"] 17 | 18 | [lib] 19 | path = "src/lib.rs" 20 | 21 | [features] 22 | default = ["hostname"] 23 | arbitrary-precision = ["serde_json/arbitrary_precision"] 24 | valuable = ["tracing/valuable", "dep:valuable", "dep:valuable-serde"] 25 | hostname = ["gethostname"] 26 | 27 | [dependencies] 28 | tracing = { version = "0.1.13", default-features = false, features = ["log", "std"] } 29 | tracing-subscriber = { version = "0.3.16", default-features = false, features = ["registry", "fmt"] } 30 | tracing-log = { version = "0.1" } 31 | log = "0.4.8" 32 | serde_json = { version = "1.0.52" } 33 | serde = "1.0.106" 34 | gethostname = { version = "0.2.1", optional = true } 35 | tracing-core = "0.1.10" 36 | time = { version = "0.3", default-features = false, features = ["formatting"] } 37 | ahash = "0.8.2" 38 | valuable = { version = "0.1.0", optional = true } 39 | valuable-serde = { version = "0.1.0", optional = true } 40 | 41 | [dev-dependencies] 42 | claims = "0.6.0" 43 | lazy_static = "1.4.0" 44 | tracing = { version = "0.1.13", default-features = false, features = ["log", "std", "attributes"] } 45 | time = { version = "0.3", default-features = false, features = ["formatting", "parsing", "local-offset"] } 46 | 47 | [[example]] 48 | name = "valuable" 49 | required-features = ["valuable", "valuable/derive"] 50 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

tracing-bunyan-formatter

2 |
3 | 4 | Bunyan formatting for tokio-rs/tracing. 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | 13 | Crates.io version 15 | 16 | 17 | 18 | Download 20 | 21 | 22 | 23 | docs.rs docs 25 | 26 | 27 | 28 | CircleCI badge 29 | 30 |
31 |
32 | 33 | `tracing-bunyan-formatter` provides two [`Layer`]s implementation to be used on top of 34 | a [`tracing`] [`Subscriber`]: 35 | - [`JsonStorageLayer`], to attach contextual information to spans for ease of consumption by 36 | downstream [`Layer`]s, via [`JsonStorage`] and [`Span`]'s [`extensions`](https://docs.rs/tracing-subscriber/0.2.5/tracing_subscriber/registry/struct.ExtensionsMut.html); 37 | - [`BunyanFormattingLayer`], which emits a [bunyan](https://github.com/trentm/node-bunyan)-compatible formatted record upon entering a span, 38 | exiting a span and event creation. 39 | 40 | **Important**: each span will inherit all fields and properties attached to its parent - this is 41 | currently not the behaviour provided by [`tracing_subscriber::fmt::Layer`](https://docs.rs/tracing-subscriber/0.2.5/tracing_subscriber/fmt/struct.Layer.html). 42 | 43 | ## Example 44 | 45 | ```rust 46 | use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; 47 | use tracing::instrument; 48 | use tracing::info; 49 | use tracing_subscriber::Registry; 50 | use tracing_subscriber::layer::SubscriberExt; 51 | 52 | #[instrument] 53 | pub fn a_unit_of_work(first_parameter: u64) { 54 | for i in 0..2 { 55 | a_sub_unit_of_work(i); 56 | } 57 | info!(excited = "true", "Tracing is quite cool!"); 58 | } 59 | 60 | #[instrument] 61 | pub fn a_sub_unit_of_work(sub_parameter: u64) { 62 | info!("Events have the full context of their parent span!"); 63 | } 64 | 65 | fn main() { 66 | let formatting_layer = BunyanFormattingLayer::new("tracing_demo".into(), std::io::stdout); 67 | let subscriber = Registry::default() 68 | .with(JsonStorageLayer) 69 | .with(formatting_layer); 70 | tracing::subscriber::set_global_default(subscriber).unwrap(); 71 | 72 | info!("Orphan event without a parent span"); 73 | a_unit_of_work(2); 74 | } 75 | ``` 76 | 77 | ## Console output 78 | 79 |
80 | 81 |
82 |
83 | 84 | If you pipe the output in the [`bunyan`](https://github.com/trentm/node-bunyan) CLI: 85 |
86 | 87 |
88 |
89 | 90 | As a pure-Rust alternative check out the [`bunyan` crate](https://crates.io/crates/bunyan). 91 | It includes a CLI binary with similar functionality to the original `bunyan` CLI written in 92 | JavaScript. 93 | 94 | 95 | ## Implementation strategy 96 | 97 | The layered approach we have pursued is not necessarily the most efficient, 98 | but it makes it easier to separate different concerns and re-use common logic across multiple [`Layer`]s. 99 | 100 | While the current crate has no ambition to provide any sort of general purpose framework on top of 101 | [`tracing-subscriber`]'s [`Layer`] trait, the information collected by [`JsonStorageLayer`] can be leveraged via 102 | its public API by other downstream layers outside of this crate whose main concern is formatting. 103 | It significantly lowers the amount of complexity you have to deal with if you are interested 104 | in implementing your own formatter, for whatever reason or purpose. 105 | 106 | You can also add another enrichment layer following the [`JsonStorageLayer`] to collect 107 | additional information about each span and store it in [`JsonStorage`]. 108 | We could have pursued this compositional approach to add `elapsed_milliseconds` to each span 109 | instead of baking it in [`JsonStorage`] itself. 110 | 111 | ## Optional features 112 | 113 | You can enable the `arbitrary_precision` feature to handle numbers of arbitrary size losslessly. Be aware of a [known issue with untagged deserialization](https://github.com/LukeMathWalker/tracing-bunyan-formatter/issues/4). 114 | 115 | ### `valuable` 116 | 117 | The `tracing` crate has an unstable feature `valuable` to enable 118 | recording custom composite types like `struct`s and `enum`s. Custom 119 | types must implement the [`valuable` 120 | crate](https://crates.io/crates/valuable)'s `Valuable` trait, which 121 | can be derived with a macro. 122 | 123 | To use `tracing` and `tracing-bunyan-formatter` with `valuable`, you must set the following configuration in your binary (as of the current crate versions on 2023-03-29): 124 | 125 | 1. Enable the feature flag `valuable` for the `tracing` dependency. 126 | 2. Add the `--cfg tracing_unstable` arguments to your rustc 127 | flags (see [`tracing`'s documentation on this][tracing_unstable]). 128 | This can be done in a few ways: 129 | 1. Adding the arguments to your binary package's 130 | `.cargo/config.toml` under `build.rustflags`. See the 131 | [`cargo` config reference documentation][cargo_build_rustflags]). 132 | 133 | Example: 134 | 135 | ```toml 136 | [build] 137 | rustflags = "--cfg tracing_unstable" 138 | ``` 139 | 140 | 2. Adding the arguments to the `RUSTFLAGS` environment variable when you 141 | run `cargo`. See the [`cargo` environment variable 142 | docs][cargo_env_vars]). 143 | 144 | Example: 145 | ```sh 146 | RUSTFLAGS="--cfg tracing_unstable" cargo build 147 | ``` 148 | 149 | 3. Enable the feature flag `valuable` for the `tracing-bunyan-formatter` dependency. 150 | 4. Add dependency `valuable`. 151 | 5. Optional: if you want to derive the `Valuable` trait for your 152 | custom types, enable the feature flag `derive` for the `valuable` 153 | dependency. 154 | 155 | See more details in the example in [`examples/valuable.rs`](examples/valuable.rs). 156 | 157 | [cargo_build_rustflags]: https://doc.rust-lang.org/cargo/reference/config.html#buildrustflags 158 | [cargo_env_vars]: https://doc.rust-lang.org/cargo/reference/environment-variables.html 159 | [tracing_unstable]: https://docs.rs/tracing/0.1.37/tracing/index.html#unstable-features 160 | 161 | ## Testing 162 | 163 | Just run `cargo test`. 164 | 165 | To run extra tests with the `valuable` feature enabled, run: 166 | 167 | ```sh 168 | RUSTFLAGS='--cfg tracing_unstable' \ 169 | cargo test --target-dir target/debug_valuable --features "valuable valuable/derive" 170 | 171 | RUSTFLAGS='--cfg tracing_unstable' \ 172 | cargo run --example valuable --target-dir target/debug_valuable --features "valuable valuable/derive" 173 | ``` 174 | 175 | [`Layer`]: https://docs.rs/tracing-subscriber/0.2.5/tracing_subscriber/layer/trait.Layer.html 176 | [`JsonStorageLayer`]: https://docs.rs/tracing-bunyan-formatter/0.1.6/tracing_bunyan_formatter/struct.JsonStorageLayer.html 177 | [`JsonStorage`]: https://docs.rs/tracing-bunyan-formatter/0.1.6/tracing_bunyan_formatter/struct.JsonStorage.html 178 | [`BunyanFormattingLayer`]: https://docs.rs/tracing-bunyan-formatter/0.1.6/tracing_bunyan_formatter/struct.BunyanFormattingLayer.html 179 | [`Span`]: https://docs.rs/tracing/0.1.13/tracing/struct.Span.html 180 | [`Subscriber`]: https://docs.rs/tracing-core/0.1.10/tracing_core/subscriber/trait.Subscriber.html 181 | [`tracing`]: https://docs.rs/tracing 182 | [`tracing-subscriber`]: https://docs.rs/tracing-subscriber 183 | -------------------------------------------------------------------------------- /examples/valuable.rs: -------------------------------------------------------------------------------- 1 | use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; 2 | use tracing_subscriber::{layer::SubscriberExt, Registry}; 3 | use valuable::Valuable; 4 | 5 | #[derive(Valuable)] 6 | struct ValuableStruct { 7 | a: u64, 8 | b: String, 9 | c: ValuableEnum, 10 | } 11 | 12 | #[derive(Valuable)] 13 | #[allow(dead_code)] 14 | enum ValuableEnum { 15 | A, 16 | B(u64), 17 | C(String), 18 | } 19 | 20 | pub fn main() -> Result<(), Box> { 21 | let formatting_layer = BunyanFormattingLayer::new("examples_valuable".into(), std::io::stdout); 22 | let subscriber = Registry::default() 23 | .with(JsonStorageLayer) 24 | .with(formatting_layer); 25 | tracing::subscriber::set_global_default(subscriber).unwrap(); 26 | 27 | let s = ValuableStruct { 28 | a: 17, 29 | b: "foo".to_string(), 30 | c: ValuableEnum::B(26), 31 | }; 32 | 33 | tracing::info!(s = s.as_value(), "Test event"); 34 | 35 | // Output example pretty printed: 36 | // 37 | // { 38 | // "v": 0, 39 | // "name": "examples_valuable", 40 | // "msg": "Test event", 41 | // "level": 30, 42 | // "hostname": "foo", 43 | // "pid": 26071, 44 | // "time": "2023-03-29T18:34:38.445454908Z", 45 | // "target": "valuable", 46 | // "line": 36, 47 | // "file": "examples/valuable.rs", 48 | // "s": { 49 | // "a": 17, 50 | // "b": "foo", 51 | // "c": { 52 | // "B": 26 53 | // } 54 | // } 55 | // } 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /images/ConsoleBunyanOutput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeMathWalker/tracing-bunyan-formatter/8c0ed4353f2cca2d2d878f9f1cf173c918b61a42/images/ConsoleBunyanOutput.png -------------------------------------------------------------------------------- /images/ConsoleOutput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LukeMathWalker/tracing-bunyan-formatter/8c0ed4353f2cca2d2d878f9f1cf173c918b61a42/images/ConsoleOutput.png -------------------------------------------------------------------------------- /src/formatting_layer.rs: -------------------------------------------------------------------------------- 1 | use crate::storage_layer::JsonStorage; 2 | use ahash::{HashSet, HashSetExt}; 3 | use serde::ser::{Serialize, SerializeMap, Serializer}; 4 | use serde_json::Value; 5 | use std::collections::HashMap; 6 | use std::fmt; 7 | use std::io::Write; 8 | use time::format_description::well_known::Rfc3339; 9 | use tracing::{Event, Id, Metadata, Subscriber}; 10 | use tracing_core::metadata::Level; 11 | use tracing_core::span::Attributes; 12 | use tracing_log::AsLog; 13 | use tracing_subscriber::fmt::MakeWriter; 14 | use tracing_subscriber::layer::Context; 15 | use tracing_subscriber::registry::SpanRef; 16 | use tracing_subscriber::Layer; 17 | 18 | /// Keys for core fields of the Bunyan format (https://github.com/trentm/node-bunyan#core-fields) 19 | const BUNYAN_VERSION: &str = "v"; 20 | const LEVEL: &str = "level"; 21 | const NAME: &str = "name"; 22 | const HOSTNAME: &str = "hostname"; 23 | const PID: &str = "pid"; 24 | const TIME: &str = "time"; 25 | const MESSAGE: &str = "msg"; 26 | const _SOURCE: &str = "src"; 27 | 28 | const BUNYAN_REQUIRED_FIELDS: [&str; 7] = 29 | [BUNYAN_VERSION, LEVEL, NAME, HOSTNAME, PID, TIME, MESSAGE]; 30 | 31 | /// Convert from log levels to Bunyan's levels. 32 | fn to_bunyan_level(level: &Level) -> u16 { 33 | match level.as_log() { 34 | log::Level::Error => 50, 35 | log::Level::Warn => 40, 36 | log::Level::Info => 30, 37 | log::Level::Debug => 20, 38 | log::Level::Trace => 10, 39 | } 40 | } 41 | 42 | /// This layer is exclusively concerned with formatting information using the [Bunyan format](https://github.com/trentm/node-bunyan). 43 | /// It relies on the upstream `JsonStorageLayer` to get access to the fields attached to 44 | /// each span. 45 | #[derive(Default)] 46 | pub struct BunyanFormattingLayer MakeWriter<'a> + 'static> { 47 | make_writer: W, 48 | pid: u32, 49 | hostname: String, 50 | bunyan_version: u8, 51 | name: String, 52 | default_fields: HashMap, 53 | skip_fields: HashSet, 54 | } 55 | 56 | /// This error will be returned in [`BunyanFormattingLayer::skip_fields`] if trying to skip a core field. 57 | #[non_exhaustive] 58 | #[derive(Debug)] 59 | pub struct SkipFieldError(String); 60 | 61 | impl fmt::Display for SkipFieldError { 62 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 | write!( 64 | f, 65 | "{} is a core field in the bunyan log format, it can't be skipped", 66 | &self.0 67 | ) 68 | } 69 | } 70 | 71 | impl std::error::Error for SkipFieldError {} 72 | 73 | impl MakeWriter<'a> + 'static> BunyanFormattingLayer { 74 | /// Create a new `BunyanFormattingLayer`. 75 | /// 76 | /// You have to specify: 77 | /// - a `name`, which will be attached to all formatted records according to the [Bunyan format](https://github.com/trentm/node-bunyan#log-record-fields); 78 | /// - a `make_writer`, which will be used to get a `Write` instance to write formatted records to. 79 | /// 80 | /// ## Using stdout 81 | /// 82 | /// ```rust 83 | /// use tracing_bunyan_formatter::BunyanFormattingLayer; 84 | /// 85 | /// let formatting_layer = BunyanFormattingLayer::new("tracing_example".into(), std::io::stdout); 86 | /// ``` 87 | /// 88 | /// If you prefer, you can use closure syntax: 89 | /// 90 | /// ```rust 91 | /// use tracing_bunyan_formatter::BunyanFormattingLayer; 92 | /// 93 | /// let formatting_layer = BunyanFormattingLayer::new("tracing_example".into(), || std::io::stdout()); 94 | /// ``` 95 | pub fn new(name: String, make_writer: W) -> Self { 96 | Self::with_default_fields(name, make_writer, HashMap::new()) 97 | } 98 | 99 | /// Add default fields to all formatted records. 100 | /// 101 | /// ```rust 102 | /// use std::collections::HashMap; 103 | /// use serde_json::json; 104 | /// use tracing_bunyan_formatter::BunyanFormattingLayer; 105 | /// 106 | /// let mut default_fields = HashMap::new(); 107 | /// default_fields.insert("custom_field".to_string(), json!("custom_value")); 108 | /// let formatting_layer = BunyanFormattingLayer::with_default_fields( 109 | /// "test".into(), 110 | /// std::io::stdout, 111 | /// default_fields, 112 | /// ); 113 | /// ``` 114 | pub fn with_default_fields( 115 | name: String, 116 | make_writer: W, 117 | default_fields: HashMap, 118 | ) -> Self { 119 | Self { 120 | make_writer, 121 | name, 122 | pid: std::process::id(), 123 | #[cfg(feature = "hostname")] 124 | hostname: gethostname::gethostname().to_string_lossy().into_owned(), 125 | #[cfg(not(feature = "hostname"))] 126 | hostname: Default::default(), 127 | bunyan_version: 0, 128 | default_fields, 129 | skip_fields: HashSet::new(), 130 | } 131 | } 132 | 133 | /// Add fields to skip when formatting with this layer. 134 | /// 135 | /// It returns an error if you try to skip a required core Bunyan field (e.g. `name`). 136 | /// You can skip optional core Bunyan fields (e.g. `line`, `file`, `target`). 137 | /// 138 | /// ```rust 139 | /// use tracing_bunyan_formatter::BunyanFormattingLayer; 140 | /// 141 | /// let skipped_fields = vec!["skipped"]; 142 | /// let formatting_layer = BunyanFormattingLayer::new("test".into(), std::io::stdout) 143 | /// .skip_fields(skipped_fields.into_iter()) 144 | /// .expect("One of the specified fields cannot be skipped"); 145 | /// ``` 146 | pub fn skip_fields(mut self, fields: Fields) -> Result 147 | where 148 | Fields: Iterator, 149 | Field: Into, 150 | { 151 | for field in fields { 152 | let field = field.into(); 153 | if BUNYAN_REQUIRED_FIELDS.contains(&field.as_str()) { 154 | return Err(SkipFieldError(field)); 155 | } 156 | self.skip_fields.insert(field); 157 | } 158 | 159 | Ok(self) 160 | } 161 | 162 | fn serialize_bunyan_core_fields( 163 | &self, 164 | map_serializer: &mut impl SerializeMap, 165 | message: &str, 166 | level: &Level, 167 | ) -> Result<(), std::io::Error> { 168 | map_serializer.serialize_entry(BUNYAN_VERSION, &self.bunyan_version)?; 169 | map_serializer.serialize_entry(NAME, &self.name)?; 170 | map_serializer.serialize_entry(MESSAGE, &message)?; 171 | map_serializer.serialize_entry(LEVEL, &to_bunyan_level(level))?; 172 | map_serializer.serialize_entry(HOSTNAME, &self.hostname)?; 173 | map_serializer.serialize_entry(PID, &self.pid)?; 174 | if let Ok(time) = &time::OffsetDateTime::now_utc().format(&Rfc3339) { 175 | map_serializer.serialize_entry(TIME, time)?; 176 | } 177 | Ok(()) 178 | } 179 | 180 | fn serialize_field( 181 | &self, 182 | map_serializer: &mut impl SerializeMap, 183 | key: &str, 184 | value: &V, 185 | ) -> Result<(), std::io::Error> 186 | where 187 | V: Serialize + ?Sized, 188 | { 189 | if !self.skip_fields.contains(key) { 190 | map_serializer.serialize_entry(key, value)?; 191 | } 192 | 193 | Ok(()) 194 | } 195 | 196 | /// Given a span, it serialised it to a in-memory buffer (vector of bytes). 197 | fn serialize_span tracing_subscriber::registry::LookupSpan<'a>>( 198 | &self, 199 | span: &SpanRef, 200 | ty: Type, 201 | ) -> Result, std::io::Error> { 202 | let mut buffer = Vec::new(); 203 | let mut serializer = serde_json::Serializer::new(&mut buffer); 204 | let mut map_serializer = serializer.serialize_map(None)?; 205 | let message = format_span_context(span, ty); 206 | self.serialize_bunyan_core_fields(&mut map_serializer, &message, span.metadata().level())?; 207 | // Additional metadata useful for debugging 208 | // They should be nested under `src` (see https://github.com/trentm/node-bunyan#src ) 209 | // but `tracing` does not support nested values yet 210 | self.serialize_field(&mut map_serializer, "target", span.metadata().target())?; 211 | self.serialize_field(&mut map_serializer, "line", &span.metadata().line())?; 212 | self.serialize_field(&mut map_serializer, "file", &span.metadata().file())?; 213 | 214 | // Add all default fields 215 | for (key, value) in self.default_fields.iter() { 216 | // Make sure this key isn't reserved. If it is reserved, 217 | // silently ignore 218 | if !BUNYAN_REQUIRED_FIELDS.contains(&key.as_str()) { 219 | self.serialize_field(&mut map_serializer, key, value)?; 220 | } 221 | } 222 | 223 | let extensions = span.extensions(); 224 | if let Some(visitor) = extensions.get::() { 225 | for (key, value) in visitor.values() { 226 | // Make sure this key isn't reserved. If it is reserved, 227 | // silently ignore 228 | if !BUNYAN_REQUIRED_FIELDS.contains(key) { 229 | self.serialize_field(&mut map_serializer, key, value)?; 230 | } 231 | } 232 | } 233 | map_serializer.end()?; 234 | // We add a trailing new line. 235 | buffer.write_all(b"\n")?; 236 | Ok(buffer) 237 | } 238 | 239 | /// Given an in-memory buffer holding a complete serialised record, flush it to the writer 240 | /// returned by self.make_writer. 241 | /// 242 | /// If we write directly to the writer returned by self.make_writer in more than one go 243 | /// we can end up with broken/incoherent bits and pieces of those records when 244 | /// running multi-threaded/concurrent programs. 245 | fn emit(&self, buffer: &[u8], meta: &Metadata<'_>) -> Result<(), std::io::Error> { 246 | self.make_writer.make_writer_for(meta).write_all(buffer) 247 | } 248 | } 249 | 250 | /// The type of record we are dealing with: entering a span, exiting a span, an event. 251 | #[derive(Clone, Debug)] 252 | pub enum Type { 253 | EnterSpan, 254 | ExitSpan, 255 | Event, 256 | } 257 | 258 | impl fmt::Display for Type { 259 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 260 | let repr = match self { 261 | Type::EnterSpan => "START", 262 | Type::ExitSpan => "END", 263 | Type::Event => "EVENT", 264 | }; 265 | write!(f, "{}", repr) 266 | } 267 | } 268 | 269 | /// Ensure consistent formatting of the span context. 270 | /// 271 | /// Example: "[AN_INTERESTING_SPAN - START]" 272 | fn format_span_context tracing_subscriber::registry::LookupSpan<'a>>( 273 | span: &SpanRef, 274 | ty: Type, 275 | ) -> String { 276 | format!("[{} - {}]", span.metadata().name().to_uppercase(), ty) 277 | } 278 | 279 | /// Ensure consistent formatting of event message. 280 | /// 281 | /// Examples: 282 | /// - "[AN_INTERESTING_SPAN - EVENT] My event message" (for an event with a parent span) 283 | /// - "My event message" (for an event without a parent span) 284 | fn format_event_message tracing_subscriber::registry::LookupSpan<'a>>( 285 | current_span: &Option>, 286 | event: &Event, 287 | event_visitor: &JsonStorage<'_>, 288 | ) -> String { 289 | // Extract the "message" field, if provided. Fallback to the target, if missing. 290 | let mut message = event_visitor 291 | .values() 292 | .get("message") 293 | .and_then(|v| match v { 294 | Value::String(s) => Some(s.as_str()), 295 | _ => None, 296 | }) 297 | .unwrap_or_else(|| event.metadata().target()) 298 | .to_owned(); 299 | 300 | // If the event is in the context of a span, prepend the span name to the message. 301 | if let Some(span) = ¤t_span { 302 | message = format!("{} {}", format_span_context(span, Type::Event), message); 303 | } 304 | 305 | message 306 | } 307 | 308 | impl Layer for BunyanFormattingLayer 309 | where 310 | S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, 311 | W: for<'a> MakeWriter<'a> + 'static, 312 | { 313 | fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { 314 | // Events do not necessarily happen in the context of a span, hence lookup_current 315 | // returns an `Option>` instead of a `SpanRef<_>`. 316 | let current_span = ctx.lookup_current(); 317 | 318 | let mut event_visitor = JsonStorage::default(); 319 | event.record(&mut event_visitor); 320 | 321 | // Opting for a closure to use the ? operator and get more linear code. 322 | let format = || { 323 | let mut buffer = Vec::new(); 324 | 325 | let mut serializer = serde_json::Serializer::new(&mut buffer); 326 | let mut map_serializer = serializer.serialize_map(None)?; 327 | 328 | let message = format_event_message(¤t_span, event, &event_visitor); 329 | self.serialize_bunyan_core_fields( 330 | &mut map_serializer, 331 | &message, 332 | event.metadata().level(), 333 | )?; 334 | // Additional metadata useful for debugging 335 | // They should be nested under `src` (see https://github.com/trentm/node-bunyan#src ) 336 | // but `tracing` does not support nested values yet 337 | self.serialize_field(&mut map_serializer, "target", event.metadata().target())?; 338 | self.serialize_field(&mut map_serializer, "line", &event.metadata().line())?; 339 | self.serialize_field(&mut map_serializer, "file", &event.metadata().file())?; 340 | 341 | // Add all default fields 342 | for (key, value) in self.default_fields.iter().filter(|(key, _)| { 343 | key.as_str() != "message" && !BUNYAN_REQUIRED_FIELDS.contains(&key.as_str()) 344 | }) { 345 | self.serialize_field(&mut map_serializer, key, value)?; 346 | } 347 | 348 | // Add all the other fields associated with the event, expect the message we already used. 349 | for (key, value) in event_visitor 350 | .values() 351 | .iter() 352 | .filter(|(&key, _)| key != "message" && !BUNYAN_REQUIRED_FIELDS.contains(&key)) 353 | { 354 | self.serialize_field(&mut map_serializer, key, value)?; 355 | } 356 | 357 | // Add all the fields from the current span, if we have one. 358 | if let Some(span) = ¤t_span { 359 | let extensions = span.extensions(); 360 | if let Some(visitor) = extensions.get::() { 361 | for (key, value) in visitor.values() { 362 | // Make sure this key isn't reserved. If it is reserved, 363 | // silently ignore 364 | if !BUNYAN_REQUIRED_FIELDS.contains(key) { 365 | self.serialize_field(&mut map_serializer, key, value)?; 366 | } 367 | } 368 | } 369 | } 370 | map_serializer.end()?; 371 | // We add a trailing new line. 372 | buffer.write_all(b"\n")?; 373 | 374 | Ok(buffer) 375 | }; 376 | 377 | let result: std::io::Result> = format(); 378 | if let Ok(formatted) = result { 379 | let _ = self.emit(&formatted, event.metadata()); 380 | } 381 | } 382 | 383 | fn on_new_span(&self, _attrs: &Attributes, id: &Id, ctx: Context<'_, S>) { 384 | let span = ctx.span(id).expect("Span not found, this is a bug"); 385 | if let Ok(serialized) = self.serialize_span(&span, Type::EnterSpan) { 386 | let _ = self.emit(&serialized, span.metadata()); 387 | } 388 | } 389 | 390 | fn on_close(&self, id: Id, ctx: Context<'_, S>) { 391 | let span = ctx.span(&id).expect("Span not found, this is a bug"); 392 | if let Ok(serialized) = self.serialize_span(&span, Type::ExitSpan) { 393 | let _ = self.emit(&serialized, span.metadata()); 394 | } 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_doctest_main)] 2 | #![doc = include_str!("../README.md")] 3 | 4 | mod formatting_layer; 5 | mod storage_layer; 6 | 7 | pub use formatting_layer::*; 8 | pub use storage_layer::*; 9 | -------------------------------------------------------------------------------- /src/storage_layer.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt; 3 | use std::time::Instant; 4 | use tracing::field::{Field, Visit}; 5 | use tracing::span::{Attributes, Record}; 6 | use tracing::{Id, Subscriber}; 7 | use tracing_subscriber::layer::Context; 8 | use tracing_subscriber::Layer; 9 | 10 | /// This layer is only concerned with information storage, it does not do any formatting or provide any output. 11 | /// 12 | /// It's purpose is to store the fields associated to spans in an easy-to-consume format 13 | /// for downstream layers concerned with emitting a formatted representation of 14 | /// spans or events. 15 | #[derive(Clone, Debug)] 16 | pub struct JsonStorageLayer; 17 | 18 | /// `JsonStorage` will collect information about a span when it's created (`new_span` handler) 19 | /// or when new records are attached to it (`on_record` handler) and store it in its `extensions` 20 | /// for future retrieval from other layers interested in formatting or further enrichment. 21 | /// 22 | /// We are re-implementing (well, copy-pasting, apart from using an HashMap instead of a BTreeMap) 23 | /// `JsonVisitor` from `tracing-subscriber` given that we can't access/insert/iterate over 24 | /// the underlying BTreeMap using its public API. 25 | /// 26 | /// For spans, we also store the duration of each span with the `elapsed_milliseconds` key using 27 | /// the `on_exit`/`on_enter` handlers. 28 | #[derive(Clone, Debug)] 29 | pub struct JsonStorage<'a> { 30 | values: HashMap<&'a str, serde_json::Value>, 31 | } 32 | 33 | impl<'a> JsonStorage<'a> { 34 | /// Get the set of stored values, as a set of keys and JSON values. 35 | pub fn values(&self) -> &HashMap<&'a str, serde_json::Value> { 36 | &self.values 37 | } 38 | } 39 | 40 | /// Get a new visitor, with an empty bag of key-value pairs. 41 | impl Default for JsonStorage<'_> { 42 | fn default() -> Self { 43 | Self { 44 | values: HashMap::new(), 45 | } 46 | } 47 | } 48 | 49 | /// Taken verbatim from tracing-subscriber 50 | impl Visit for JsonStorage<'_> { 51 | /// Visit a signed 64-bit integer value. 52 | fn record_i64(&mut self, field: &Field, value: i64) { 53 | self.values 54 | .insert(field.name(), serde_json::Value::from(value)); 55 | } 56 | 57 | /// Visit an unsigned 64-bit integer value. 58 | fn record_u64(&mut self, field: &Field, value: u64) { 59 | self.values 60 | .insert(field.name(), serde_json::Value::from(value)); 61 | } 62 | 63 | /// Visit a 64-bit floating point value. 64 | fn record_f64(&mut self, field: &Field, value: f64) { 65 | self.values 66 | .insert(field.name(), serde_json::Value::from(value)); 67 | } 68 | 69 | /// Visit a boolean value. 70 | fn record_bool(&mut self, field: &Field, value: bool) { 71 | self.values 72 | .insert(field.name(), serde_json::Value::from(value)); 73 | } 74 | 75 | /// Visit a string value. 76 | fn record_str(&mut self, field: &Field, value: &str) { 77 | self.values 78 | .insert(field.name(), serde_json::Value::from(value)); 79 | } 80 | 81 | fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { 82 | match field.name() { 83 | // Skip fields that are actually log metadata that have already been handled 84 | name if name.starts_with("log.") => (), 85 | name if name.starts_with("r#") => { 86 | self.values 87 | .insert(&name[2..], serde_json::Value::from(format!("{:?}", value))); 88 | } 89 | name => { 90 | self.values 91 | .insert(name, serde_json::Value::from(format!("{:?}", value))); 92 | } 93 | }; 94 | } 95 | 96 | #[cfg(all(tracing_unstable, feature = "valuable"))] 97 | #[cfg_attr(docsrs, doc(cfg(all(tracing_unstable, feature = "valuable"))))] 98 | fn record_value(&mut self, field: &Field, value: valuable::Value<'_>) { 99 | let serializable = valuable_serde::Serializable::new(value); 100 | 101 | match serde_json::to_value(serializable) { 102 | Ok(json_value) => { 103 | self.values.insert(field.name(), json_value); 104 | } 105 | Err(error) => { 106 | tracing::debug!( 107 | // The parent span may be the one with this 108 | // unserializable field value. If so logging an event 109 | // under this parent span might trigger it field value 110 | // to be serialized again, causing an infinite loop. 111 | // Avoid this by explicitly setting the parent span to `None`. 112 | parent: None, 113 | ?error, 114 | field_name = field.name(), 115 | "serde_json serialization error while recording valuable field." 116 | ); 117 | } 118 | } 119 | } 120 | } 121 | 122 | impl tracing_subscriber::registry::LookupSpan<'a>> Layer 123 | for JsonStorageLayer 124 | { 125 | /// Span creation. 126 | /// This is the only occasion we have to store the fields attached to the span 127 | /// given that they might have been borrowed from the surrounding context. 128 | fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { 129 | let span = ctx.span(id).expect("Span not found, this is a bug"); 130 | 131 | // We want to inherit the fields from the parent span, if there is one. 132 | let mut visitor = if let Some(parent_span) = span.parent() { 133 | // Extensions can be used to associate arbitrary data to a span. 134 | // We'll use it to store our representation of its fields. 135 | // We create a copy of the parent visitor! 136 | let mut extensions = parent_span.extensions_mut(); 137 | extensions 138 | .get_mut::() 139 | .map(|v| v.to_owned()) 140 | .unwrap_or_default() 141 | } else { 142 | JsonStorage::default() 143 | }; 144 | 145 | let mut extensions = span.extensions_mut(); 146 | 147 | // Register all fields. 148 | // Fields on the new span should override fields on the parent span if there is a conflict. 149 | attrs.record(&mut visitor); 150 | // Associate the visitor with the Span for future usage via the Span's extensions 151 | extensions.insert(visitor); 152 | } 153 | 154 | fn on_record(&self, span: &Id, values: &Record<'_>, ctx: Context<'_, S>) { 155 | let span = ctx.span(span).expect("Span not found, this is a bug"); 156 | 157 | // Before you can associate a record to an existing Span, well, that Span has to be created! 158 | // We can thus rely on the invariant that we always associate a JsonVisitor with a Span 159 | // on creation (`new_span` method), hence it's safe to unwrap the Option. 160 | let mut extensions = span.extensions_mut(); 161 | let visitor = extensions 162 | .get_mut::() 163 | .expect("Visitor not found on 'record', this is a bug"); 164 | // Register all new fields 165 | values.record(visitor); 166 | } 167 | 168 | /// When we enter a span **for the first time** save the timestamp in its extensions. 169 | fn on_enter(&self, span: &Id, ctx: Context<'_, S>) { 170 | let span = ctx.span(span).expect("Span not found, this is a bug"); 171 | 172 | let mut extensions = span.extensions_mut(); 173 | if extensions.get_mut::().is_none() { 174 | extensions.insert(Instant::now()); 175 | } 176 | } 177 | 178 | /// When we close a span, register how long it took in milliseconds. 179 | fn on_close(&self, span: Id, ctx: Context<'_, S>) { 180 | let span = ctx.span(&span).expect("Span not found, this is a bug"); 181 | 182 | // Using a block to drop the immutable reference to extensions 183 | // given that we want to borrow it mutably just below 184 | let elapsed_milliseconds = { 185 | let extensions = span.extensions(); 186 | extensions 187 | .get::() 188 | .map(|i| i.elapsed().as_millis()) 189 | // If `Instant` is not in the span extensions it means that the span was never 190 | // entered into. 191 | .unwrap_or(0) 192 | }; 193 | 194 | #[cfg(not(feature = "arbitrary-precision"))] 195 | // without the arbitrary_precision feature u128 values are not supported, 196 | // but u64 is still more than enough for our purposes 197 | let elapsed_milliseconds: u64 = { 198 | use std::convert::TryInto; 199 | 200 | elapsed_milliseconds.try_into().unwrap_or_default() 201 | }; 202 | 203 | let mut extensions_mut = span.extensions_mut(); 204 | let visitor = extensions_mut 205 | .get_mut::() 206 | .expect("Visitor not found on 'record', this is a bug"); 207 | 208 | if let Ok(elapsed) = serde_json::to_value(elapsed_milliseconds) { 209 | visitor.values.insert("elapsed_milliseconds", elapsed); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /tests/e2e.rs: -------------------------------------------------------------------------------- 1 | use crate::mock_writer::MockWriter; 2 | use claims::assert_some_eq; 3 | use serde_json::{json, Value}; 4 | use std::collections::HashMap; 5 | use std::sync::{Arc, Mutex}; 6 | use time::format_description::well_known::Rfc3339; 7 | use tracing::{info, span, Level}; 8 | use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; 9 | use tracing_subscriber::layer::SubscriberExt; 10 | use tracing_subscriber::Registry; 11 | 12 | mod mock_writer; 13 | 14 | // Run a closure and collect the output emitted by the tracing instrumentation using an in-memory buffer. 15 | fn run_and_get_raw_output(action: F) -> String { 16 | let buffer = Arc::new(Mutex::new(vec![])); 17 | let buffer_clone = buffer.clone(); 18 | 19 | let mut default_fields = HashMap::new(); 20 | default_fields.insert("custom_field".to_string(), json!("custom_value")); 21 | let skipped_fields = vec!["skipped"]; 22 | let formatting_layer = BunyanFormattingLayer::with_default_fields( 23 | "test".into(), 24 | move || MockWriter::new(buffer_clone.clone()), 25 | default_fields, 26 | ) 27 | .skip_fields(skipped_fields.into_iter()) 28 | .unwrap(); 29 | let subscriber = Registry::default() 30 | .with(JsonStorageLayer) 31 | .with(formatting_layer); 32 | tracing::subscriber::with_default(subscriber, action); 33 | 34 | // Return the formatted output as a string to make assertions against 35 | let buffer_guard = buffer.lock().unwrap(); 36 | let output = buffer_guard.to_vec(); 37 | String::from_utf8(output).unwrap() 38 | } 39 | 40 | // Run a closure and collect the output emitted by the tracing instrumentation using 41 | // an in-memory buffer as structured new-line-delimited JSON. 42 | fn run_and_get_output(action: F) -> Vec { 43 | run_and_get_raw_output(action) 44 | .lines() 45 | .filter(|&l| !l.trim().is_empty()) 46 | .inspect(|l| println!("{}", l)) 47 | .map(|line| serde_json::from_str::(line).unwrap()) 48 | .collect() 49 | } 50 | 51 | // Instrumented code to be run to test the behaviour of the tracing instrumentation. 52 | fn test_action() { 53 | let a = 2; 54 | let span = span!(Level::DEBUG, "shaving_yaks", a); 55 | let _enter = span.enter(); 56 | 57 | info!("pre-shaving yaks"); 58 | let b = 3; 59 | let skipped = false; 60 | let new_span = span!(Level::DEBUG, "inner shaving", b, skipped); 61 | let _enter2 = new_span.enter(); 62 | 63 | info!("shaving yaks"); 64 | } 65 | 66 | #[test] 67 | fn each_line_is_valid_json() { 68 | let tracing_output = run_and_get_raw_output(test_action); 69 | 70 | // Each line is valid JSON 71 | for line in tracing_output.lines().filter(|&l| !l.is_empty()) { 72 | assert!(serde_json::from_str::(line).is_ok()); 73 | } 74 | } 75 | 76 | #[test] 77 | fn each_line_has_the_mandatory_bunyan_fields() { 78 | let tracing_output = run_and_get_output(test_action); 79 | 80 | for record in tracing_output { 81 | assert!(record.get("name").is_some()); 82 | assert!(record.get("level").is_some()); 83 | assert!(record.get("time").is_some()); 84 | assert!(record.get("msg").is_some()); 85 | assert!(record.get("v").is_some()); 86 | assert!(record.get("pid").is_some()); 87 | assert!(record.get("hostname").is_some()); 88 | assert!(record.get("custom_field").is_some()); 89 | } 90 | } 91 | 92 | #[test] 93 | fn time_is_formatted_according_to_rfc_3339() { 94 | let tracing_output = run_and_get_output(test_action); 95 | 96 | for record in tracing_output { 97 | let time = record.get("time").unwrap().as_str().unwrap(); 98 | let parsed = time::OffsetDateTime::parse(time, &Rfc3339); 99 | assert!(parsed.is_ok()); 100 | let parsed = parsed.unwrap(); 101 | assert!(parsed.offset().is_utc()); 102 | } 103 | } 104 | 105 | #[test] 106 | fn encode_f64_as_numbers() { 107 | let f64_value: f64 = 0.5; 108 | let action = || { 109 | let span = span!( 110 | Level::DEBUG, 111 | "parent_span_f64", 112 | f64_field = tracing::field::Empty 113 | ); 114 | let _enter = span.enter(); 115 | span.record("f64_field", f64_value); 116 | info!("testing f64"); 117 | }; 118 | let tracing_output = run_and_get_output(action); 119 | 120 | for record in tracing_output { 121 | if record 122 | .get("msg") 123 | .and_then(Value::as_str) 124 | .map_or(false, |msg| msg.contains("testing f64")) 125 | { 126 | let observed_value = record.get("f64_field").and_then(|v| v.as_f64()); 127 | assert_some_eq!(observed_value, f64_value); 128 | } 129 | } 130 | } 131 | 132 | #[test] 133 | fn parent_properties_are_propagated() { 134 | let action = || { 135 | let span = span!(Level::DEBUG, "parent_span", parent_property = 2); 136 | let _enter = span.enter(); 137 | 138 | let child_span = span!(Level::DEBUG, "child_span"); 139 | let _enter_child = child_span.enter(); 140 | 141 | info!("shaving yaks"); 142 | }; 143 | let tracing_output = run_and_get_output(action); 144 | 145 | for record in tracing_output { 146 | assert!(record.get("parent_property").is_some()); 147 | } 148 | } 149 | 150 | #[test] 151 | fn elapsed_milliseconds_are_present_on_exit_span() { 152 | let tracing_output = run_and_get_output(test_action); 153 | 154 | for record in tracing_output { 155 | if record 156 | .get("msg") 157 | .and_then(Value::as_str) 158 | .map_or(false, |msg| msg.ends_with("END]")) 159 | { 160 | assert!(record.get("elapsed_milliseconds").is_some()); 161 | } 162 | } 163 | } 164 | 165 | #[test] 166 | fn skip_fields() { 167 | let tracing_output = run_and_get_output(test_action); 168 | 169 | for record in tracing_output { 170 | assert!(record.get("skipped").is_none()); 171 | } 172 | } 173 | 174 | #[test] 175 | fn skipping_core_fields_is_not_allowed() { 176 | let skipped_fields = vec!["level"]; 177 | 178 | let result = BunyanFormattingLayer::new("test".into(), || vec![]) 179 | .skip_fields(skipped_fields.into_iter()); 180 | 181 | match result { 182 | Err(err) => { 183 | assert_eq!( 184 | "level is a core field in the bunyan log format, it can't be skipped", 185 | err.to_string() 186 | ); 187 | } 188 | Ok(_) => panic!("skipping core fields shouldn't work"), 189 | } 190 | } 191 | 192 | #[cfg(feature = "valuable")] 193 | mod valuable_tests { 194 | use super::run_and_get_output; 195 | use serde_json::json; 196 | use valuable::Valuable; 197 | 198 | #[derive(Valuable)] 199 | struct ValuableStruct { 200 | a: u64, 201 | b: String, 202 | c: ValuableEnum, 203 | } 204 | 205 | #[derive(Valuable)] 206 | #[allow(dead_code)] 207 | enum ValuableEnum { 208 | A, 209 | B(u64), 210 | C(String), 211 | } 212 | 213 | #[test] 214 | fn encode_valuable_composite_types_as_json() { 215 | let out = run_and_get_output(|| { 216 | let s = ValuableStruct { 217 | a: 17, 218 | b: "Hello, world!".to_string(), 219 | c: ValuableEnum::B(27), 220 | }; 221 | 222 | tracing::info!(s = s.as_value(), "Test info event"); 223 | }); 224 | 225 | assert_eq!(out.len(), 1); 226 | let entry = &out[0]; 227 | 228 | let s_json = entry 229 | .as_object() 230 | .expect("expect entry is object") 231 | .get("s") 232 | .expect("expect entry.s is present"); 233 | 234 | assert_eq!( 235 | json!({ 236 | "a": 17, 237 | "b": "Hello, world!", 238 | "c": { 239 | "B": 27, 240 | }, 241 | }), 242 | *s_json 243 | ); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /tests/mock_writer.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::sync::{Arc, Mutex, MutexGuard, TryLockError}; 3 | 4 | /// Use a vector of bytes behind a Arc as writer in order to inspect the tracing output 5 | /// for testing purposes. 6 | /// Stolen directly from the test suite of tracing-subscriber. 7 | pub struct MockWriter { 8 | buf: Arc>>, 9 | } 10 | 11 | impl MockWriter { 12 | pub fn new(buf: Arc>>) -> Self { 13 | Self { buf } 14 | } 15 | 16 | pub fn map_error(err: TryLockError) -> io::Error { 17 | match err { 18 | TryLockError::WouldBlock => io::Error::from(io::ErrorKind::WouldBlock), 19 | TryLockError::Poisoned(_) => io::Error::from(io::ErrorKind::Other), 20 | } 21 | } 22 | 23 | pub fn buf(&self) -> io::Result>> { 24 | self.buf.try_lock().map_err(Self::map_error) 25 | } 26 | } 27 | 28 | impl io::Write for MockWriter { 29 | fn write(&mut self, buf: &[u8]) -> io::Result { 30 | self.buf()?.write(buf) 31 | } 32 | 33 | fn flush(&mut self) -> io::Result<()> { 34 | self.buf()?.flush() 35 | } 36 | } 37 | --------------------------------------------------------------------------------