├── .github ├── ISSUE_TEMPLATE │ ├── BUG-FORM.yml │ └── FEATURE-FORM.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── relyance-sci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── deny.toml ├── rustfmt.toml ├── telemetry-batteries-macros ├── Cargo.toml ├── examples │ ├── datadog_macros.rs │ └── statsd.rs └── src │ ├── lib.rs │ ├── metrics.rs │ ├── metrics │ └── statsd.rs │ ├── tracing.rs │ └── tracing │ └── datadog.rs └── telemetry-batteries ├── Cargo.toml ├── build.rs ├── examples ├── custom_tracing.rs ├── datadog.rs └── prometheus.rs └── src ├── lib.rs ├── metrics ├── mod.rs ├── prometheus.rs └── statsd.rs └── tracing ├── datadog.rs ├── id_generator.rs ├── layers ├── datadog.rs ├── mod.rs └── stdout.rs ├── mod.rs └── stdout.rs /.github/ISSUE_TEMPLATE/BUG-FORM.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report 3 | labels: ["bug"] 4 | title: "[Bug] " 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please ensure that the bug has not already been filed in the issue tracker. 10 | 11 | Thanks for taking the time to report this bug! 12 | - type: textarea 13 | attributes: 14 | label: Describe the bug 15 | description: Please include code snippets as well if relevant. 16 | validations: 17 | required: true 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE-FORM.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a feature 3 | labels: ["enhancement"] 4 | title: "[Feature] " 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please ensure that the feature has not already been requested in the issue tracker. 10 | - type: textarea 11 | attributes: 12 | label: Describe the feature you would like 13 | description: 14 | Please also describe your goals for the feature. What problems it solves, how it would 15 | be used, etc. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Additional context 21 | description: Add any other context to the feature (screenshots, resources, etc.) 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | ## Motivation 16 | 17 | 22 | 23 | ## Solution 24 | 25 | 29 | 30 | ## PR Checklist 31 | 32 | - [ ] Added Tests 33 | - [ ] Added Documentation 34 | - [ ] Breaking changes 35 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "docker" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | name: test ${{ matrix.rust }} ${{ matrix.flags }} 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 30 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | rust: ["stable"] 21 | flags: [""] 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: dtolnay/rust-toolchain@master 25 | with: 26 | toolchain: ${{ matrix.rust }} 27 | - uses: Swatinem/rust-cache@v2 28 | with: 29 | cache-on-failure: true 30 | - name: build 31 | run: cargo build 32 | - name: test 33 | run: cargo test --workspace 34 | 35 | clippy: 36 | runs-on: ubuntu-latest 37 | timeout-minutes: 30 38 | steps: 39 | - uses: actions/checkout@v3 40 | - uses: dtolnay/rust-toolchain@clippy 41 | - uses: Swatinem/rust-cache@v2 42 | with: 43 | cache-on-failure: true 44 | - run: cargo clippy --workspace --all-targets --all-features 45 | env: 46 | RUSTFLAGS: -Dwarnings 47 | 48 | docs: 49 | runs-on: ubuntu-latest 50 | timeout-minutes: 30 51 | steps: 52 | - uses: actions/checkout@v3 53 | - uses: dtolnay/rust-toolchain@nightly 54 | - uses: Swatinem/rust-cache@v2 55 | with: 56 | cache-on-failure: true 57 | - run: cargo doc --workspace --all-features --no-deps --document-private-items 58 | env: 59 | RUSTDOCFLAGS: "--cfg docsrs -D warnings" 60 | 61 | fmt: 62 | runs-on: ubuntu-latest 63 | timeout-minutes: 30 64 | steps: 65 | - uses: actions/checkout@v3 66 | - uses: dtolnay/rust-toolchain@nightly 67 | with: 68 | components: rustfmt 69 | - run: cargo fmt --all --check 70 | 71 | cargo-deny: 72 | name: Licensing and Advisories 73 | runs-on: ubuntu-24.04 74 | steps: 75 | - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1 76 | - uses: EmbarkStudios/cargo-deny-action@8371184bd11e21dcf8ac82ebf8c9c9f74ebf7268 # pin@v2.0.1 77 | -------------------------------------------------------------------------------- /.github/workflows/relyance-sci.yml: -------------------------------------------------------------------------------- 1 | name: Relyance SCI Scan 2 | 3 | on: 4 | schedule: 5 | - cron: "31 0 * * *" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | execute-relyance-sci: 13 | name: Relyance SCI Job 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | 18 | steps: 19 | - name: Run Relyance SCI 20 | uses: worldcoin/gh-actions-public/relyance@main 21 | # More information: https://github.com/worldcoin/gh-actions-public/tree/main/relyance 22 | with: 23 | secrets-dpp-sci-key: ${{ secrets.DPP_SCI_KEY }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .DS_Store 4 | 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["telemetry-batteries", "telemetry-batteries-macros"] 4 | 5 | [workspace.package] 6 | edition = "2021" 7 | license = "MIT OR (Apache-2.0 WITH LLVM-exception)" 8 | repository = "https://github.com/worldcoin/telemetry-batteries" 9 | -------------------------------------------------------------------------------- /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 2024 Worldcoin Foundation 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 | 203 | 204 | ---- LLVM Exceptions to the Apache 2.0 License ---- 205 | 206 | As an exception, if, as a result of your compiling your source code, portions 207 | of this Software are embedded into an Object form of such source code, you 208 | may redistribute such embedded portions in such Object form without complying 209 | with the conditions of Sections 4(a), 4(b) and 4(d) of the License. 210 | 211 | In addition, if you combine or link compiled forms of this Software with 212 | software that is licensed under the GPLv2 ("Combined Software") and if a 213 | court of competent jurisdiction determines that the patent provision (Section 214 | 3), the indemnity provision (Section 9) or other Section of the License 215 | conflicts with the conditions of the GPLv2, you may retroactively and 216 | prospectively choose to deem waived or otherwise exclude such Section(s) of 217 | the License, but only in their entirety and only with respect to the Combined 218 | Software. 219 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Worldcoin Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # telemetry-batteries 2 | 3 | Batteries included library to configure tracing, logs and metrics 4 | 5 | ## License 6 | 7 | Unless otherwise specified, all code in this repository is dual-licensed under 8 | either: 9 | 10 | - MIT License ([LICENSE-MIT](LICENSE-MIT)) 11 | - Apache License, Version 2.0, with LLVM Exceptions ([LICENSE-APACHE](LICENSE-APACHE)) 12 | 13 | at your option. This means you may select the license you prefer to use. 14 | 15 | Any contribution intentionally submitted for inclusion in the work by you, as 16 | defined in the Apache-2.0 license, shall be dual licensed as above, without any 17 | additional terms or conditions. 18 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | # Cargo deny will check dependencies via `--all-features` 3 | all-features = true 4 | 5 | [advisories] 6 | version = 2 7 | ignore = ["RUSTSEC-2024-0436"] 8 | 9 | [sources] 10 | unknown-registry = "deny" 11 | 12 | [licenses] 13 | version = 2 14 | # We want really high confidence when inferring licenses from text 15 | confidence-threshold = 1.0 16 | 17 | # List of explicitly allowed licenses 18 | # See https://spdx.org/licenses/ for list of possible licenses 19 | allow = [ 20 | "0BSD", 21 | "Apache-2.0 WITH LLVM-exception", 22 | "Apache-2.0", 23 | "BSD-2-Clause", 24 | "BSD-2-Clause-Patent", 25 | "BSD-3-Clause", 26 | "BSL-1.0", 27 | "CC0-1.0", 28 | "CDLA-Permissive-2.0", 29 | "ISC", 30 | "MIT", 31 | "MPL-2.0", # Although this is copyleft, it is scoped to modifying the original files 32 | "OpenSSL", 33 | "Unicode-DFS-2016", 34 | "Unlicense", 35 | "Zlib", 36 | "Unicode-3.0", 37 | ] -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /telemetry-batteries-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "telemetry-batteries-macros" 3 | version = "0.1.0" 4 | publish = false 5 | 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | proc-macro2 = "1.0.60" 15 | quote = "1" 16 | syn = { version = "2.0", features = ["full"] } 17 | 18 | [dev-dependencies] 19 | telemetry-batteries = { path = "../telemetry-batteries" } 20 | tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] } 21 | tracing = "0.1.40" 22 | metrics = "0.24" 23 | eyre = "0.6.12" 24 | tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] } 25 | -------------------------------------------------------------------------------- /telemetry-batteries-macros/examples/datadog_macros.rs: -------------------------------------------------------------------------------- 1 | use telemetry_batteries_macros::datadog; 2 | 3 | #[datadog(service_name = "datadog-example")] 4 | #[tokio::main] 5 | pub async fn main() -> eyre::Result<()> { 6 | tracing::info!("foo"); 7 | tracing::info!("bar"); 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /telemetry-batteries-macros/examples/statsd.rs: -------------------------------------------------------------------------------- 1 | use telemetry_batteries_macros::statsd; 2 | 3 | #[statsd( 4 | host = "localhost", 5 | port = 8125, 6 | buffer_size = 1024, 7 | queue_size = 100, 8 | prefix = "my_service" 9 | )] 10 | #[tokio::main] 11 | pub async fn main() -> eyre::Result<()> { 12 | let _ = metrics::counter!("my_counter"); 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /telemetry-batteries-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | mod metrics; 4 | mod tracing; 5 | 6 | /// Macro to initialize Datadog instrumentation 7 | /// 8 | /// # Parameters 9 | /// 10 | /// - `service_name`: Required string literal that specifies the name of the service. 11 | /// 12 | /// - `endpoint`: Optional string literal that specifies the Datadog agent's endpoint 13 | /// to which telemetry data will be sent. If not specified, this value defaults to http://localhost:8126. 14 | /// 15 | /// - `location`: Optional boolean indicates whether to include the location in traces. Defaults to `false` if not specified. 16 | /// 17 | /// # Usage 18 | /// 19 | /// To use the `datadog` macro, apply it to the main function 20 | /// of your application. You must provide the `service_name` parameter, and you may optionally 21 | /// include `endpoint` and `location` parameters. Due to how the `datadog_layer` from `telemetry-batteries` is configured 22 | /// the `main` function must be asynchronous and use the `tokio::main` macro after the `datadog` macro. 23 | #[proc_macro_attribute] 24 | pub fn datadog(attr: TokenStream, item: TokenStream) -> TokenStream { 25 | tracing::datadog::datadog(attr, item) 26 | } 27 | 28 | /// Macro to initialize Stastd metrics backend 29 | /// 30 | /// # Parameters 31 | /// 32 | /// - `host`: Optional string literal specifying the StatsD server's IP. Defaults to `"localhost"` if not provided. 33 | /// 34 | /// - `port`: Optional u16 specifying the port on which the StatsD server is listening. Defaults to `8125` if not provided. 35 | /// 36 | /// - `buffer_size`: Optional usize specifying the buffer size (in bytes) that should be buffered in StatsdClient's memory before the data is sent to the server. Defaults to 256 if not provided. 37 | /// 38 | /// - `queue_size`: Optional usize specifying the size of the queue for storing metrics 39 | /// before sending to the server. Defaults to 5000 if not provided. 40 | /// 41 | /// - `prefix`: Optional string literal used as a prefix for all metrics sent. No prefix is added if not provided. 42 | /// 43 | /// # Usage 44 | /// 45 | /// To use the `statsd` macro, apply it to the main function 46 | /// of your application. Due to how the `StatsdBattery` from `telemetry-batteries` is configured 47 | /// the `main` function must be asynchronous and use the `tokio::main` macro after the `statsd` macro. 48 | #[proc_macro_attribute] 49 | pub fn statsd(attr: TokenStream, item: TokenStream) -> TokenStream { 50 | metrics::statsd::statsd(attr, item) 51 | } 52 | -------------------------------------------------------------------------------- /telemetry-batteries-macros/src/metrics.rs: -------------------------------------------------------------------------------- 1 | pub mod statsd; 2 | -------------------------------------------------------------------------------- /telemetry-batteries-macros/src/metrics/statsd.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | parse::{Parse, ParseStream}, 5 | parse_macro_input, parse_quote, Ident, ItemFn, LitStr, Token, 6 | }; 7 | 8 | pub const DEFAULT_HOST_ENDPOINT: &str = "localhost"; 9 | pub const DEFAULT_HOST_PORT: u16 = 8125; 10 | pub const DEFAULT_BUFFER_SIZE: usize = 256; 11 | pub const DEFAULT_QUEUE_SIZE: usize = 5000; 12 | 13 | struct StatsdArgs { 14 | host: Option, 15 | port: Option, 16 | queue_size: Option, 17 | buffer_size: Option, 18 | prefix: Option, 19 | } 20 | 21 | impl Parse for StatsdArgs { 22 | fn parse(input: ParseStream) -> syn::Result { 23 | let mut host = None; 24 | let mut port = None; 25 | let mut queue_size = None; 26 | let mut buffer_size = None; 27 | let mut prefix = None; 28 | 29 | while !input.is_empty() { 30 | let ident: Ident = input.parse()?; 31 | let _: Token![=] = input.parse()?; 32 | match ident.to_string().as_str() { 33 | "host" => { 34 | if let Ok(lit_str) = input.parse::() { 35 | host = Some(lit_str.value()); 36 | } 37 | } 38 | "port" => { 39 | if let Ok(lit_int) = input.parse::() { 40 | port = Some(lit_int.base10_parse::()?); 41 | } 42 | } 43 | "queue_size" => { 44 | if let Ok(lit_int) = input.parse::() { 45 | queue_size = Some(lit_int.base10_parse::()?); 46 | } 47 | } 48 | "buffer_size" => { 49 | if let Ok(lit_int) = input.parse::() { 50 | buffer_size = Some(lit_int.base10_parse::()?); 51 | } 52 | } 53 | "prefix" => { 54 | if let Ok(lit_str) = input.parse::() { 55 | prefix = Some(lit_str.value()); 56 | } 57 | } 58 | _ => { 59 | return Err(syn::Error::new( 60 | ident.span(), 61 | "Unexpected argument", 62 | )) 63 | } 64 | } 65 | 66 | if !input.is_empty() { 67 | let _: Option = input.parse()?; 68 | } 69 | } 70 | 71 | Ok(StatsdArgs { 72 | host, 73 | port, 74 | queue_size, 75 | buffer_size, 76 | prefix, 77 | }) 78 | } 79 | } 80 | 81 | pub fn statsd(attr: TokenStream, item: TokenStream) -> TokenStream { 82 | let statsd_args = parse_macro_input!(attr as StatsdArgs); 83 | let mut input_fn = parse_macro_input!(item as ItemFn); 84 | 85 | // Use provided values or defaults 86 | let host = statsd_args 87 | .host 88 | .unwrap_or_else(|| DEFAULT_HOST_ENDPOINT.to_string()); 89 | let port = statsd_args.port.unwrap_or(DEFAULT_HOST_PORT); 90 | let queue_size = statsd_args.queue_size.unwrap_or(DEFAULT_QUEUE_SIZE); 91 | let buffer_size = statsd_args.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE); 92 | let prefix = statsd_args.prefix.unwrap_or_default(); 93 | 94 | let input_block = &input_fn.block; 95 | let new_block: syn::Block = parse_quote!({ 96 | let host = #host; 97 | let prefix = #prefix; 98 | telemetry_batteries::metrics::statsd::StatsdBattery::init( 99 | &host, 100 | #port, 101 | #queue_size, 102 | #buffer_size, 103 | Some(&prefix), 104 | )?; 105 | 106 | #input_block 107 | }); 108 | 109 | input_fn.block = Box::new(new_block); 110 | 111 | let expanded = quote! { 112 | #input_fn 113 | }; 114 | 115 | TokenStream::from(expanded) 116 | } 117 | -------------------------------------------------------------------------------- /telemetry-batteries-macros/src/tracing.rs: -------------------------------------------------------------------------------- 1 | pub mod datadog; 2 | -------------------------------------------------------------------------------- /telemetry-batteries-macros/src/tracing/datadog.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | parse::{Parse, ParseStream}, 5 | parse_macro_input, parse_quote, Ident, ItemFn, LitBool, LitStr, Token, 6 | }; 7 | 8 | pub const DEFAULT_DATADOG_AGENT_ENDPOINT: &str = "http://localhost:8126"; 9 | 10 | struct DatadogArgs { 11 | endpoint: Option, 12 | service_name: String, 13 | location: Option, 14 | } 15 | 16 | impl Parse for DatadogArgs { 17 | fn parse(input: ParseStream) -> syn::Result { 18 | let mut endpoint = None; 19 | let mut service_name = None; 20 | let mut location = None; 21 | 22 | while !input.is_empty() { 23 | let ident: Ident = input.parse()?; 24 | let _: Token![=] = input.parse()?; 25 | match ident.to_string().as_str() { 26 | "endpoint" => { 27 | if let Ok(lit_str) = input.parse::() { 28 | endpoint = Some(lit_str.value()); 29 | } 30 | } 31 | "service_name" => { 32 | if let Ok(lit_str) = input.parse::() { 33 | service_name = Some(lit_str.value()); 34 | } 35 | } 36 | "location" => { 37 | if let Ok(lit_bool) = input.parse::() { 38 | location = Some(lit_bool.value()); 39 | } 40 | } 41 | _ => { 42 | return Err(syn::Error::new( 43 | ident.span(), 44 | "Unexpected argument", 45 | )) 46 | } 47 | } 48 | 49 | if !input.is_empty() { 50 | let _comma: Option = input.parse()?; 51 | } 52 | } 53 | 54 | // Ensure service_name was provided 55 | let service_name = service_name.ok_or_else(|| { 56 | syn::Error::new( 57 | input.span(), 58 | "`service_name` is required for `datadog` attribute", 59 | ) 60 | })?; 61 | 62 | Ok(DatadogArgs { 63 | endpoint, 64 | service_name, 65 | location, 66 | }) 67 | } 68 | } 69 | 70 | pub fn datadog(attr: TokenStream, item: TokenStream) -> TokenStream { 71 | let datadog_args = parse_macro_input!(attr as DatadogArgs); 72 | let mut input_fn = parse_macro_input!(item as ItemFn); 73 | 74 | let endpoint: String = datadog_args 75 | .endpoint 76 | .unwrap_or(DEFAULT_DATADOG_AGENT_ENDPOINT.to_string()); 77 | 78 | let service_name = datadog_args.service_name.as_str(); 79 | let location = datadog_args.location.unwrap_or(false); 80 | 81 | let input_block = &input_fn.block; 82 | let new_block: syn::Block = parse_quote!({ 83 | let endpoint = #endpoint; 84 | let _tracing_shutdown_handle = telemetry_batteries::tracing::datadog::DatadogBattery::init( 85 | Some(&endpoint), 86 | #service_name, 87 | None, 88 | #location, 89 | ); 90 | 91 | 92 | 93 | #input_block 94 | }); 95 | 96 | input_fn.block = Box::new(new_block); 97 | 98 | let expanded = quote! { 99 | #input_fn 100 | }; 101 | 102 | TokenStream::from(expanded) 103 | } 104 | -------------------------------------------------------------------------------- /telemetry-batteries/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "telemetry-batteries" 3 | version = "0.1.1" 4 | publish = false 5 | 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [features] 11 | default = ["metrics-prometheus", "metrics-statsd", "rustls"] 12 | metrics-statsd = ["dep:metrics-exporter-statsd"] 13 | metrics-prometheus = ["dep:metrics-exporter-prometheus"] 14 | rustls = ["reqwest/rustls-tls"] 15 | native-tls = ["reqwest/native-tls"] 16 | 17 | [dependencies] 18 | chrono = "0.4" 19 | dirs = "5" 20 | http = "1" 21 | metrics = "0.24" 22 | metrics-exporter-statsd = { version = "0.9", optional = true } 23 | metrics-exporter-prometheus = { version = "0.16", optional = true } 24 | opentelemetry = { version = "0.26" } 25 | opentelemetry-datadog = { version = "0.14", features = ["reqwest-client"] } 26 | opentelemetry-http = "0.26" 27 | opentelemetry_sdk = { version = "0.26", features = ["rt-tokio"] } 28 | reqwest = { version = "0.12", default-features = false } 29 | serde = { version = "1", features = ["derive"] } 30 | serde_json = "1" 31 | thiserror = "2" 32 | tokio = { version = "1", features = ["macros"] } 33 | tracing = "0.1" 34 | tracing-appender = "0.2" 35 | tracing-opentelemetry = "0.27" 36 | tracing-serde = "0.1" 37 | tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } 38 | rand = "0.8" 39 | 40 | [dev-dependencies] 41 | eyre = "0.6" 42 | -------------------------------------------------------------------------------- /telemetry-batteries/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Ensure a TLS backend is enabled 3 | let rustls = cfg!(feature = "rustls"); 4 | let native_tls = cfg!(feature = "native-tls"); 5 | 6 | if !rustls && !native_tls { 7 | panic!("At least one TLS feature must be enabled: 'rustls' or 'native-tls'"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /telemetry-batteries/examples/custom_tracing.rs: -------------------------------------------------------------------------------- 1 | use telemetry_batteries::tracing::layers::{ 2 | datadog::datadog_layer, stdout_layer, 3 | }; 4 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 5 | 6 | pub fn main() -> eyre::Result<()> { 7 | // Initialize tracing using layers 8 | let datadog_layer = 9 | datadog_layer("datadog-example", "http://localhost:8126", true); 10 | 11 | tracing_subscriber::registry() 12 | .with(stdout_layer()) 13 | .with(datadog_layer) 14 | .init(); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /telemetry-batteries/examples/datadog.rs: -------------------------------------------------------------------------------- 1 | use telemetry_batteries::metrics::statsd::StatsdBattery; 2 | use telemetry_batteries::tracing::datadog::DatadogBattery; 3 | 4 | pub const SERVICE_NAME: &str = "datadog-example"; 5 | 6 | pub fn main() -> eyre::Result<()> { 7 | // Add a new DatadogBattery for tracing/logs 8 | // Tracing providers are gracefully shutdown when shutdown handle is dropped. 9 | let _shutdown_handle = DatadogBattery::init(None, SERVICE_NAME, None, true); 10 | 11 | // Add a new StatsdBattery for metrics 12 | StatsdBattery::init("localhost", 8125, 5000, 1024, None)?; 13 | 14 | // Alternatively you can use a prometheus exporter 15 | // PrometheusBattery::init()?; 16 | 17 | tracing::info!("foo"); 18 | metrics::counter!("foo").increment(1); 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /telemetry-batteries/examples/prometheus.rs: -------------------------------------------------------------------------------- 1 | use telemetry_batteries::metrics::prometheus::{ 2 | PrometheusBattery, PrometheusExporterConfig, 3 | }; 4 | 5 | pub fn main() -> eyre::Result<()> { 6 | // Configure http listener for Prometheus scrape endpoint 7 | let prometheus_exporter_config = PrometheusExporterConfig::HttpListener { 8 | listen_address: "http://0.0.0.0:9998" 9 | .parse::()?, 10 | }; 11 | 12 | // Initialize the Prometheus metrics exporter 13 | PrometheusBattery::init(Some(prometheus_exporter_config))?; 14 | 15 | metrics::counter!("foo").increment(1); 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /telemetry-batteries/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(feature = "metrics-prometheus", feature = "metrics-statsd"))] 2 | pub mod metrics; 3 | pub mod tracing; 4 | 5 | /// Reexports of crates that appear in the public API. 6 | /// 7 | /// Using these directly instead of adding them yourself to Cargo.toml will help avoid 8 | /// errors where types have the same name but actually are distinct types from different 9 | /// crate versions. 10 | pub mod reexports { 11 | #[cfg(any( 12 | feature = "metrics-prometheus", 13 | feature = "metrics-statsd" 14 | ))] 15 | pub use ::metrics; 16 | pub use ::opentelemetry; 17 | } 18 | -------------------------------------------------------------------------------- /telemetry-batteries/src/metrics/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "metrics-prometheus")] 2 | pub mod prometheus; 3 | 4 | #[cfg(feature = "metrics-statsd")] 5 | pub mod statsd; 6 | -------------------------------------------------------------------------------- /telemetry-batteries/src/metrics/prometheus.rs: -------------------------------------------------------------------------------- 1 | use metrics_exporter_prometheus::{BuildError, PrometheusBuilder}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{net::SocketAddr, time::Duration}; 4 | 5 | pub struct PrometheusBattery; 6 | 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | #[serde(rename_all = "snake_case")] 9 | pub enum PrometheusExporterConfig { 10 | // Run an HTTP listener on the given `listen_address`. 11 | HttpListener { 12 | listen_address: SocketAddr, 13 | }, 14 | 15 | // Run a push gateway task sending to the given `endpoint` after `interval` time has elapsed, 16 | // infinitely. 17 | PushGateway { 18 | endpoint: String, 19 | interval: Duration, 20 | username: Option, 21 | password: Option, 22 | }, 23 | 24 | #[allow(dead_code)] 25 | Unconfigured, 26 | } 27 | 28 | impl PrometheusBattery { 29 | pub fn init( 30 | exporter_config: Option, 31 | ) -> Result<(), BuildError> { 32 | let mut builder = PrometheusBuilder::new(); 33 | 34 | builder = match exporter_config { 35 | Some(PrometheusExporterConfig::HttpListener { listen_address }) => { 36 | builder.with_http_listener(listen_address) 37 | } 38 | Some(PrometheusExporterConfig::PushGateway { 39 | endpoint, 40 | interval, 41 | username, 42 | password, 43 | }) => builder 44 | .with_push_gateway(endpoint, interval, username, password)?, 45 | _ => builder, 46 | }; 47 | 48 | builder.install() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /telemetry-batteries/src/metrics/statsd.rs: -------------------------------------------------------------------------------- 1 | use metrics_exporter_statsd::{StatsdBuilder, StatsdError}; 2 | 3 | pub struct StatsdBattery; 4 | 5 | impl StatsdBattery { 6 | pub fn init( 7 | host: &str, 8 | port: u16, 9 | queue_size: usize, 10 | buffer_size: usize, 11 | prefix: Option<&str>, 12 | ) -> Result<(), StatsdError> { 13 | let recorder = StatsdBuilder::from(host, port) 14 | .with_queue_size(queue_size) 15 | .with_buffer_size(buffer_size) 16 | .build(prefix)?; 17 | 18 | metrics::set_global_recorder(recorder)?; 19 | 20 | Ok(()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /telemetry-batteries/src/tracing/datadog.rs: -------------------------------------------------------------------------------- 1 | use crate::tracing::layers::{ 2 | datadog::datadog_layer, non_blocking_writer_layer, 3 | }; 4 | use opentelemetry_datadog::DatadogPropagator; 5 | use tracing_appender::rolling::RollingFileAppender; 6 | use tracing_subscriber::{ 7 | layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, 8 | }; 9 | 10 | use super::TracingShutdownHandle; 11 | 12 | pub const DEFAULT_DATADOG_AGENT_ENDPOINT: &str = "http://localhost:8126"; 13 | 14 | pub struct DatadogBattery; 15 | 16 | impl DatadogBattery { 17 | pub fn init( 18 | endpoint: Option<&str>, 19 | service_name: &str, 20 | file_appender: Option, 21 | location: bool, 22 | ) -> TracingShutdownHandle { 23 | opentelemetry::global::set_text_map_propagator(DatadogPropagator::new()); 24 | 25 | let endpoint = endpoint.unwrap_or(DEFAULT_DATADOG_AGENT_ENDPOINT); 26 | 27 | let datadog_layer = datadog_layer(service_name, endpoint, location); 28 | 29 | if let Some(file_appender) = file_appender { 30 | let file_writer_layer = non_blocking_writer_layer(file_appender); 31 | 32 | let layers = EnvFilter::from_default_env() 33 | .and_then(datadog_layer) 34 | .and_then(file_writer_layer); 35 | 36 | tracing_subscriber::registry().with(layers).init(); 37 | } else { 38 | let layers = EnvFilter::from_default_env().and_then(datadog_layer); 39 | tracing_subscriber::registry().with(layers).init(); 40 | } 41 | 42 | TracingShutdownHandle 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use std::env; 49 | 50 | use super::*; 51 | 52 | #[ignore] 53 | #[tokio::test] 54 | async fn test_init() { 55 | env::set_var("RUST_LOG", "info"); 56 | let service_name = "test_service"; 57 | let _shutdown_handle = 58 | DatadogBattery::init(None, service_name, None, false); 59 | 60 | for _ in 0..10 { 61 | tracing::info!("test"); 62 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /telemetry-batteries/src/tracing/id_generator.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use opentelemetry::trace::{SpanId, TraceId}; 4 | use opentelemetry_sdk::trace::IdGenerator; 5 | use rand::{rngs, Rng}; 6 | 7 | /// Reduced Id Generator 8 | /// 9 | /// Generates trace ids using only 64 bits of randomness to be compatible 10 | /// with other languages. 11 | #[derive(Debug)] 12 | pub struct ReducedIdGenerator; 13 | 14 | impl IdGenerator for ReducedIdGenerator { 15 | fn new_trace_id(&self) -> TraceId { 16 | CURRENT_RNG.with(|rng| { 17 | let trace_id = rng.borrow_mut().gen::(); 18 | 19 | TraceId::from(trace_id as u128) 20 | }) 21 | } 22 | 23 | fn new_span_id(&self) -> SpanId { 24 | CURRENT_RNG.with(|rng| SpanId::from(rng.borrow_mut().gen::())) 25 | } 26 | } 27 | 28 | thread_local! { 29 | /// Store random number generator for each thread 30 | static CURRENT_RNG: RefCell = RefCell::new(rngs::ThreadRng::default()); 31 | } 32 | -------------------------------------------------------------------------------- /telemetry-batteries/src/tracing/layers/datadog.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use chrono::Utc; 4 | use opentelemetry_datadog::ApiVersion; 5 | use opentelemetry_sdk::trace::{Config, Sampler}; 6 | use serde::ser::SerializeMap; 7 | use serde::Serializer; 8 | use tracing::{Event, Subscriber}; 9 | use tracing_serde::AsSerde; 10 | use tracing_subscriber::fmt::format::Writer; 11 | use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; 12 | use tracing_subscriber::registry::LookupSpan; 13 | use tracing_subscriber::{fmt, Layer}; 14 | 15 | use crate::tracing::id_generator::ReducedIdGenerator; 16 | use crate::tracing::{ 17 | opentelemetry_span_id, opentelemetry_trace_id, WriteAdapter, 18 | }; 19 | 20 | pub fn datadog_layer( 21 | service_name: &str, 22 | endpoint: &str, 23 | location: bool, 24 | ) -> impl Layer 25 | where 26 | S: Subscriber + for<'a> LookupSpan<'a>, 27 | { 28 | let tracer_config = Config::default() 29 | .with_id_generator(ReducedIdGenerator) 30 | .with_sampler(Sampler::AlwaysOn); 31 | 32 | // Small hack https://github.com/will-bank/datadog-tracing/blob/30cdfba8d00caa04f6ac8e304f76403a5eb97129/src/tracer.rs#L29 33 | // Until https://github.com/open-telemetry/opentelemetry-rust-contrib/issues/7 is resolved 34 | // seems to prevent client reuse and avoid the errors in question 35 | let dd_http_client = reqwest::ClientBuilder::new() 36 | .pool_idle_timeout(Duration::from_millis(1)) 37 | .build() 38 | .expect("Could not init datadog http_client"); 39 | 40 | let tracer = opentelemetry_datadog::new_pipeline() 41 | .with_http_client(dd_http_client) 42 | .with_agent_endpoint(endpoint) 43 | .with_trace_config(tracer_config) 44 | .with_service_name(service_name) 45 | .with_api_version(ApiVersion::Version05) 46 | .install_batch(opentelemetry_sdk::runtime::Tokio) 47 | .expect("failed to install OpenTelemetry datadog tracer, perhaps check which async runtime is being used"); 48 | 49 | let otel_layer = tracing_opentelemetry::OpenTelemetryLayer::new(tracer); 50 | let dd_format_layer = datadog_format_layer(location); 51 | 52 | dd_format_layer.and_then(otel_layer) 53 | } 54 | 55 | pub fn datadog_format_layer(location: bool) -> impl Layer 56 | where 57 | S: Subscriber + for<'a> LookupSpan<'a>, 58 | { 59 | fmt::Layer::new() 60 | .json() 61 | .event_format(DatadogFormat { location }) 62 | } 63 | 64 | pub struct DatadogFormat { 65 | location: bool, 66 | } 67 | 68 | impl FormatEvent for DatadogFormat 69 | where 70 | S: Subscriber + for<'lookup> LookupSpan<'lookup>, 71 | N: for<'writer> FormatFields<'writer> + 'static, 72 | { 73 | fn format_event( 74 | &self, 75 | ctx: &FmtContext<'_, S, N>, 76 | mut writer: Writer<'_>, 77 | event: &Event<'_>, 78 | ) -> std::fmt::Result 79 | where 80 | S: Subscriber + for<'a> LookupSpan<'a>, 81 | { 82 | let meta = event.metadata(); 83 | 84 | let span_id = opentelemetry_span_id(ctx); 85 | let trace_id = opentelemetry_trace_id(ctx); 86 | 87 | let mut visit = || { 88 | let mut serializer = 89 | serde_json::Serializer::new(WriteAdapter::new(&mut writer)); 90 | let mut serializer = serializer.serialize_map(None)?; 91 | 92 | serializer 93 | .serialize_entry("timestamp", &Utc::now().to_rfc3339())?; 94 | serializer.serialize_entry("level", &meta.level().as_serde())?; 95 | serializer.serialize_entry("target", meta.target())?; 96 | 97 | if self.location { 98 | serializer.serialize_entry("line", &meta.line())?; 99 | serializer.serialize_entry("file", &meta.file())?; 100 | serializer 101 | .serialize_entry("module_path", &meta.module_path())?; 102 | } 103 | 104 | let mut visitor = tracing_serde::SerdeMapVisitor::new(serializer); 105 | event.record(&mut visitor); 106 | serializer = visitor.take_serializer()?; 107 | 108 | if let Some(trace_id) = trace_id { 109 | // The opentelemetry-datadog crate truncates the 128-bit trace-id 110 | // into a u64 before formatting it. 111 | let trace_id = format!("{}", trace_id as u64); 112 | serializer.serialize_entry("dd.trace_id", &trace_id)?; 113 | } 114 | 115 | if let Some(span_id) = span_id { 116 | let span_id = format!("{span_id}"); 117 | serializer.serialize_entry("dd.span_id", &span_id)?; 118 | } 119 | 120 | serializer.end() 121 | }; 122 | 123 | visit().map_err(|_| std::fmt::Error)?; 124 | 125 | writeln!(writer) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /telemetry-batteries/src/tracing/layers/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use tokio::sync::OnceCell; 4 | use tracing::Subscriber; 5 | use tracing_appender::non_blocking::WorkerGuard; 6 | use tracing_subscriber::{fmt, registry::LookupSpan, Layer}; 7 | 8 | pub mod datadog; 9 | pub mod stdout; 10 | 11 | pub fn stdout_layer() -> impl Layer 12 | where 13 | S: Subscriber + for<'a> LookupSpan<'a>, 14 | { 15 | fmt::layer().with_target(false).with_level(true) 16 | } 17 | 18 | static WORKER_GUARD: OnceCell = OnceCell::const_new(); 19 | 20 | pub fn non_blocking_writer_layer(writer: W) -> impl Layer 21 | where 22 | S: Subscriber + for<'a> LookupSpan<'a>, 23 | W: Write + Send + Sync + 'static, 24 | { 25 | let (non_blocking, guard) = tracing_appender::non_blocking(writer); 26 | WORKER_GUARD.set(guard).expect("Could not set worker guard"); 27 | 28 | tracing_subscriber::fmt::layer().with_writer(non_blocking) 29 | } 30 | -------------------------------------------------------------------------------- /telemetry-batteries/src/tracing/layers/stdout.rs: -------------------------------------------------------------------------------- 1 | use tracing::Subscriber; 2 | use tracing_subscriber::{registry::LookupSpan, EnvFilter, Layer}; 3 | 4 | pub fn stdout_layer() -> impl Layer 5 | where 6 | S: Subscriber + for<'a> LookupSpan<'a>, 7 | { 8 | tracing_subscriber::fmt::layer() 9 | .with_writer(std::io::stdout) 10 | .pretty() 11 | .with_target(false) 12 | .with_line_number(true) 13 | .with_file(true) 14 | .with_filter(EnvFilter::from_default_env()) 15 | } 16 | -------------------------------------------------------------------------------- /telemetry-batteries/src/tracing/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod datadog; 2 | pub mod id_generator; 3 | pub mod layers; 4 | pub mod stdout; 5 | 6 | use opentelemetry::trace::{SpanContext, SpanId, TraceContextExt, TraceId}; 7 | use opentelemetry::Context; 8 | 9 | use std::path::PathBuf; 10 | use std::{fs, io}; 11 | use tracing::Subscriber; 12 | use tracing_opentelemetry::OpenTelemetrySpanExt; 13 | use tracing_opentelemetry::OtelData; 14 | use tracing_subscriber::fmt::{FmtContext, FormatFields}; 15 | use tracing_subscriber::registry::{LookupSpan, SpanRef}; 16 | pub use tracing_subscriber::Registry; 17 | 18 | /// `TracingShutdownHandle` ensures the global tracing provider 19 | /// is gracefully shut down when the handle is dropped, preventing loss 20 | /// of any remaining traces not yet exported. 21 | #[must_use] 22 | pub struct TracingShutdownHandle; 23 | 24 | impl Drop for TracingShutdownHandle { 25 | fn drop(&mut self) { 26 | tracing::warn!("Shutting down tracing provider"); 27 | opentelemetry::global::shutdown_tracer_provider(); 28 | } 29 | } 30 | 31 | pub fn trace_from_headers(headers: &http::HeaderMap) { 32 | tracing::Span::current().set_parent( 33 | opentelemetry::global::get_text_map_propagator(|propagator| { 34 | propagator.extract(&opentelemetry_http::HeaderExtractor(headers)) 35 | }), 36 | ); 37 | } 38 | 39 | pub fn trace_to_headers(headers: &mut http::HeaderMap) { 40 | opentelemetry::global::get_text_map_propagator(|propagator| { 41 | propagator.inject_context( 42 | &tracing::Span::current().context(), 43 | &mut opentelemetry_http::HeaderInjector(headers), 44 | ); 45 | }); 46 | } 47 | 48 | /// Finds Otel trace id by going up the span stack until we find a span 49 | /// with a trace id. 50 | pub fn opentelemetry_trace_id(ctx: &FmtContext<'_, S, N>) -> Option 51 | where 52 | S: Subscriber + for<'lookup> LookupSpan<'lookup>, 53 | N: for<'writer> FormatFields<'writer> + 'static, 54 | { 55 | let span_ref = span_from_ctx(ctx)?; 56 | 57 | let extensions = span_ref.extensions(); 58 | 59 | let data = extensions.get::()?; 60 | let parent_trace_id = data.parent_cx.span().span_context().trace_id(); 61 | let parent_trace_id_u128 = u128::from_be_bytes(parent_trace_id.to_bytes()); 62 | 63 | // So parent trace id will usually be zero UNLESS we extract a trace id from 64 | // headers in which case it'll be the trace id from headers. And for some 65 | // reason this logic is not handled with Option 66 | // 67 | // So in case the parent trace id is zero, we should use the builder trace id. 68 | if parent_trace_id_u128 == 0 { 69 | let builder_id = data.builder.trace_id?; 70 | 71 | Some(u128::from_be_bytes(builder_id.to_bytes())) 72 | } else { 73 | Some(parent_trace_id_u128) 74 | } 75 | } 76 | 77 | /// Finds Otel span id 78 | /// 79 | /// BUG: The otel object is not available for span end events. This is 80 | /// because the Otel layer is higher in the stack and removes the 81 | /// extension before we get here. 82 | /// 83 | /// Fallbacks on tracing span id 84 | pub fn opentelemetry_span_id(ctx: &FmtContext<'_, S, N>) -> Option 85 | where 86 | S: Subscriber + for<'lookup> LookupSpan<'lookup>, 87 | N: for<'writer> FormatFields<'writer> + 'static, 88 | { 89 | let span_ref = span_from_ctx(ctx)?; 90 | 91 | let extensions = span_ref.extensions(); 92 | 93 | let data = extensions.get::()?; 94 | let parent_span_id = data.parent_cx.span().span_context().span_id(); 95 | let parent_span_id_u64 = u64::from_be_bytes(parent_span_id.to_bytes()); 96 | 97 | // Same logic as for trace ids 98 | if parent_span_id_u64 == 0 { 99 | let builder_id = data.builder.span_id?; 100 | 101 | Some(u64::from_be_bytes(builder_id.to_bytes())) 102 | } else { 103 | Some(parent_span_id_u64) 104 | } 105 | } 106 | 107 | /// Sets the current span's parent to the specified context 108 | pub fn trace_from_ctx(ctx: SpanContext) { 109 | let parent_ctx = Context::new().with_remote_span_context(ctx); 110 | tracing::Span::current().set_parent(parent_ctx); 111 | } 112 | 113 | // Extracts the trace id and span id from the current span 114 | pub fn extract_span_ids() -> (TraceId, SpanId) { 115 | let current_span = tracing::Span::current(); 116 | let current_context = current_span.context(); 117 | let span_ref = current_context.span(); 118 | 119 | let span_context = span_ref.span_context(); 120 | let trace_id = span_context.trace_id(); 121 | let span_id = span_context.span_id(); 122 | 123 | (trace_id, span_id) 124 | } 125 | 126 | fn span_from_ctx<'a, S, N>( 127 | ctx: &'a FmtContext<'a, S, N>, 128 | ) -> Option> 129 | where 130 | S: Subscriber + for<'lookup> LookupSpan<'lookup>, 131 | N: for<'writer> FormatFields<'writer> + 'static, 132 | { 133 | let span = ctx.lookup_current().or_else(|| ctx.parent_span()); 134 | 135 | span 136 | } 137 | 138 | pub struct WriteAdapter<'a> { 139 | fmt_write: &'a mut dyn std::fmt::Write, 140 | } 141 | 142 | impl<'a> WriteAdapter<'a> { 143 | pub fn new(fmt_write: &'a mut dyn std::fmt::Write) -> Self { 144 | Self { fmt_write } 145 | } 146 | } 147 | 148 | impl io::Write for WriteAdapter<'_> { 149 | fn write(&mut self, buf: &[u8]) -> io::Result { 150 | let s = std::str::from_utf8(buf) 151 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 152 | 153 | self.fmt_write.write_str(s).map_err(io::Error::other)?; 154 | 155 | Ok(s.len()) 156 | } 157 | 158 | fn flush(&mut self) -> io::Result<()> { 159 | Ok(()) 160 | } 161 | } 162 | 163 | /// Platform agnostic function to get the path to the log directory. If the directory does not 164 | /// exist, it will be created. 165 | /// 166 | /// # Returns 167 | /// * `Ok(PathBuf)` containing the path to the `.logs` directory in the user's home directory. 168 | /// * `Err(io::Error)` if the home directory cannot be determined, or the `.logs` directory 169 | /// cannot be created. 170 | /// 171 | /// # Errors 172 | /// This function will return an `Err` if the home directory cannot be found or the `.logs` 173 | /// directory cannot be created. It does not guarantee that the `.logs` directory is writable. 174 | pub fn get_log_directory() -> Result { 175 | let home_dir = dirs::home_dir().ok_or(io::ErrorKind::NotFound)?; 176 | let log_dir = home_dir.join(".logs"); 177 | 178 | // Create the `.logs` directory if it does not exist 179 | if !log_dir.exists() { 180 | fs::create_dir_all(&log_dir)?; 181 | } 182 | 183 | Ok(log_dir) 184 | } 185 | -------------------------------------------------------------------------------- /telemetry-batteries/src/tracing/stdout.rs: -------------------------------------------------------------------------------- 1 | use crate::tracing::layers::stdout::stdout_layer; 2 | use crate::tracing::TracingShutdownHandle; 3 | use tracing_subscriber::{ 4 | layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, 5 | }; 6 | 7 | pub struct StdoutBattery; 8 | 9 | impl StdoutBattery { 10 | pub fn init() -> TracingShutdownHandle { 11 | let stdout_layer = stdout_layer(); 12 | let layers = EnvFilter::from_default_env().and_then(stdout_layer); 13 | tracing_subscriber::registry().with(layers).init(); 14 | 15 | TracingShutdownHandle 16 | } 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | use std::env; 22 | 23 | use super::*; 24 | 25 | #[ignore] 26 | #[tokio::test] 27 | async fn test_init() { 28 | env::set_var("RUST_LOG", "info"); 29 | let _shutdown_handle = StdoutBattery::init(); 30 | 31 | for _ in 0..1000 { 32 | let span = tracing::span!(tracing::Level::INFO, "test_span"); 33 | span.in_scope(|| { 34 | tracing::info!("test_event"); 35 | }); 36 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 37 | } 38 | } 39 | } 40 | --------------------------------------------------------------------------------