├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .gitpod.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── autometrics-cli ├── Cargo.toml ├── README.md └── src │ ├── main.rs │ └── sloth.rs ├── autometrics-macros ├── Cargo.toml └── src │ ├── lib.rs │ ├── parse.rs │ └── result_labels.rs ├── autometrics ├── Cargo.toml ├── README.md ├── benches │ └── basic_benchmark.rs ├── build.rs ├── src │ ├── README.md │ ├── constants.rs │ ├── exemplars │ │ ├── mod.rs │ │ ├── tracing.rs │ │ └── tracing_opentelemetry.rs │ ├── labels.rs │ ├── lib.rs │ ├── objectives.rs │ ├── otel_push_exporter.rs │ ├── prometheus_exporter.rs │ ├── settings.rs │ ├── task_local.rs │ └── tracker │ │ ├── metrics.rs │ │ ├── mod.rs │ │ ├── opentelemetry.rs │ │ ├── prometheus.rs │ │ └── prometheus_client.rs └── tests │ ├── compilation.rs │ ├── compilation │ ├── error_locus │ │ └── fail │ │ │ ├── report_original_line.rs │ │ │ └── report_original_line.stderr │ └── result_labels │ │ ├── fail │ │ ├── wrong_attribute.rs │ │ ├── wrong_attribute.stderr │ │ ├── wrong_kv_attribute.rs │ │ ├── wrong_kv_attribute.stderr │ │ ├── wrong_result_name.rs │ │ ├── wrong_result_name.stderr │ │ ├── wrong_variant.rs │ │ └── wrong_variant.stderr │ │ └── pass │ │ ├── async_trait_support.rs │ │ ├── generics.rs │ │ └── macro.rs │ ├── exemplars_test.rs │ ├── init_to_zero_test.rs │ ├── integration_test.rs │ ├── objectives_test.rs │ ├── settings_custom_registry.rs │ ├── settings_histogram_buckets_test.rs │ └── settings_service_name_test.rs └── examples ├── README.md ├── actix-web ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── axum ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── custom-metrics ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── exemplars-tracing-opentelemetry ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── exemplars-tracing ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── full-api ├── Cargo.toml ├── README.md ├── build.rs └── src │ ├── database.rs │ ├── error.rs │ ├── main.rs │ ├── routes.rs │ └── util.rs ├── grpc-http ├── Cargo.toml ├── README.md ├── build.rs ├── proto │ └── job.proto └── src │ ├── db_manager.rs │ ├── main.rs │ ├── server.rs │ └── shutdown.rs ├── opentelemetry-push-custom ├── Cargo.toml ├── README.md ├── otel-collector-config.yml └── src │ └── main.rs ├── opentelemetry-push ├── Cargo.toml ├── README.md ├── otel-collector-config.yml └── src │ └── main.rs └── util ├── Cargo.toml ├── gitpod └── Dockerfile ├── prometheus.yml └── src └── lib.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.rules.yml linguist-generated 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Checklist 2 | 3 | - [ ] Changelog updated 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: CI 10 | 11 | jobs: 12 | build_and_test: 13 | name: Build and Test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: dtolnay/rust-toolchain@stable 18 | - uses: Swatinem/rust-cache@v2 19 | 20 | # Lint 21 | # Note: GitHub hosted runners using the latest stable version of Rust have Clippy pre-installed. 22 | - run: cargo clippy --features=metrics-0_24,prometheus-exporter 23 | - run: cargo clippy --features=prometheus-0_13 24 | - run: cargo clippy --features=prometheus-client-0_22 25 | - run: cargo clippy --features=opentelemetry-0_24 26 | 27 | # Run the tests with each of the different metrics libraries 28 | - run: cargo test --features=prometheus-exporter 29 | - run: cargo test --features=prometheus-exporter,metrics-0_24 30 | - run: cargo test --features=prometheus-exporter,prometheus-0_13 31 | - run: cargo test --features=prometheus-exporter,prometheus-client-0_22,exemplars-tracing 32 | - run: cargo test --features=prometheus-exporter,prometheus-client-0_22,exemplars-tracing-opentelemetry-0_25 33 | - run: cargo test --features=prometheus-exporter,opentelemetry-0_24 34 | 35 | # Build the crate using the other optional features 36 | - run: cargo build --features=metrics-0_24,custom-objective-percentile,custom-objective-latency 37 | 38 | # Install protoc for the examples 39 | - uses: arduino/setup-protoc@v3 40 | 41 | # Compile the examples 42 | - run: cargo build --package example-actix-web 43 | - run: cargo build --package example-axum 44 | - run: cargo build --package example-custom-metrics 45 | - run: cargo build --package example-exemplars-tracing 46 | - run: cargo build --package example-exemplars-tracing-opentelemetry 47 | - run: cargo build --package example-full-api 48 | - run: cargo build --package example-grpc-http 49 | - run: cargo build --package example-opentelemetry-push 50 | 51 | 52 | # Make sure the docs can be built 53 | - run: cargo doc --all-features 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | data 4 | 5 | # Trybuild compilation error outputs in development phase 6 | wip 7 | 8 | # jetbrains ide config files 9 | .idea 10 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # Configure a custom docker image. Here we install 'cargo watch' 2 | image: 3 | file: examples/util/gitpod/Dockerfile 4 | 5 | # List the start up tasks. You can start them in parallel in multiple terminals. See https://www.gitpod.io/docs/config-start-tasks/ 6 | tasks: 7 | # Run Prometheus 8 | - init: prometheus 9 | command: $(which prometheus) --config.file=./examples/util/prometheus.yml 10 | # Build and run on startup 11 | - init: cargo build 12 | command: cargo run -p example-full-api serve 13 | # In parallel, open a terminal with automatic type-checking for development 14 | - command: cargo watch -x check 15 | 16 | # List the ports you want to expose and what to do when they are served. See https://www.gitpod.io/docs/config-ports/ 17 | ports: 18 | - port: 9090 19 | onOpen: open-preview 20 | - port: 3000 21 | onOpen: open-preview 22 | 23 | 24 | vscode: 25 | extensions: 26 | # "The" Rust extension for VSCode 27 | - rust-lang.rust-analyzer 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | version = "2.0.0" 3 | edition = "2021" 4 | authors = ["Fiberplane ", "Evan Schwartz <3262610+emschwartz@users.noreply.github.com>"] 5 | description = " Easily add metrics to your code that actually help you spot and debug issues in production. Built on Prometheus and OpenTelemetry." 6 | documentation = "https://docs.rs/autometrics" 7 | repository = "https://github.com/autometrics-dev/autometrics-rs" 8 | homepage = "https://autometrics.dev" 9 | license = "MIT OR Apache-2.0" 10 | keywords = ["metrics", "prometheus", "opentelemetry"] 11 | categories = ["development-tools::debugging", "development-tools::profiling"] 12 | 13 | [workspace] 14 | default-members = ["autometrics", "autometrics-cli", "autometrics-macros"] 15 | members = [ 16 | "autometrics", 17 | "autometrics-cli", 18 | "autometrics-macros", 19 | "examples/*" 20 | ] 21 | exclude = ["examples/data", "examples/target"] 22 | 23 | [workspace.dependencies] 24 | autometrics-macros = { version = "2.0.0", path = "autometrics-macros" } 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![GitHub_headerImage](https://user-images.githubusercontent.com/3262610/221191767-73b8a8d9-9f8b-440e-8ab6-75cb3c82f2bc.png) 2 | 3 | ## NOTE: Autometrics is no longer actively maintained. 4 | 5 | [![Documentation](https://docs.rs/autometrics/badge.svg)](https://docs.rs/autometrics) 6 | [![Crates.io](https://img.shields.io/crates/v/autometrics.svg)](https://crates.io/crates/autometrics) 7 | [![Discord Shield](https://discordapp.com/api/guilds/950489382626951178/widget.png?style=shield)](https://discord.gg/kHtwcH8As9) 8 | 9 | Metrics are a powerful and cost-efficient tool for understanding the health and performance of your code in production. But it's hard to decide what metrics to track and even harder to write queries to understand the data. 10 | 11 | Autometrics provides a macro that makes it trivial to instrument any function with the most useful metrics: request rate, error rate, and latency. It standardizes these metrics and then generates powerful Prometheus queries based on your function details to help you quickly identify and debug issues in production. 12 | 13 | ## Benefits 14 | 15 | - [✨ `#[autometrics]`](https://docs.rs/autometrics/latest/autometrics/attr.autometrics.html) macro adds useful metrics to any function or `impl` block, without you thinking about what metrics to collect 16 | - 💡 Generates powerful Prometheus queries to help quickly identify and debug issues in production 17 | - 🔗 Injects links to live Prometheus charts directly into each function's doc comments 18 | - [📊 Grafana dashboards](https://github.com/autometrics-dev/autometrics-shared#dashboards) work without configuration to visualize the performance of functions & [SLOs](https://docs.rs/autometrics/latest/autometrics/objectives/index.html) 19 | - 🔍 Correlates your code's version with metrics to help identify commits that introduced errors or latency 20 | - 📏 Standardizes metrics across services and teams to improve debugging 21 | - ⚖️ Function-level metrics provide useful granularity without exploding cardinality 22 | - [⚡ Minimal runtime overhead](#benchmarks) 23 | 24 | ## Advanced Features 25 | 26 | - [🚨 Define alerts](https://docs.rs/autometrics/latest/autometrics/objectives/index.html) using SLO best practices directly in your source code 27 | - [📍 Attach exemplars](https://docs.rs/autometrics/latest/autometrics/exemplars/index.html) automatically to connect metrics with traces 28 | - [⚙️ Configurable](https://docs.rs/autometrics/latest/autometrics/#metrics-backends) metric collection library ([`opentelemetry`](https://crates.io/crates/opentelemetry), [`prometheus`](https://crates.io/crates/prometheus), [`prometheus-client`](https://crates.io/crates/prometheus-client) or [`metrics`](https://crates.io/crates/metrics)) 29 | 30 | See [autometrics.dev](https://docs.autometrics.dev/) for more details on the ideas behind autometrics. 31 | 32 | # Example + Demo 33 | 34 | ```rust 35 | use autometrics::autometrics; 36 | 37 | #[autometrics] 38 | pub async fn create_user() { 39 | // Now this function produces metrics! 📈 40 | } 41 | ``` 42 | 43 | Here is a demo of jumping from function docs to live Prometheus charts: 44 | 45 | https://github.com/autometrics-dev/autometrics-rs/assets/3262610/966ed140-1d6c-45f3-a607-64797d5f0233 46 | 47 | ## Quickstart 48 | 49 | 1. Add `autometrics` to your project: 50 | ```sh 51 | cargo add autometrics --features=prometheus-exporter 52 | ``` 53 | 2. Instrument your functions with the [`#[autometrics]`](https://docs.rs/autometrics/latest/autometrics/attr.autometrics.html) macro 54 | 55 | ```rust 56 | use autometrics::autometrics; 57 | 58 | // Just add the autometrics annotation to your functions 59 | #[autometrics] 60 | pub async fn my_function() { 61 | // Now this function produces metrics! 62 | } 63 | 64 | struct MyStruct; 65 | 66 | // You can also instrument whole impl blocks 67 | #[autometrics] 68 | impl MyStruct { 69 | pub fn my_method() { 70 | // This method produces metrics too! 71 | } 72 | } 73 | ``` 74 | 75 | 76 |
77 | 78 | Tip: Adding autometrics to all functions using the tracing::instrument macro 79 | 80 |
81 | 82 | You can use a search and replace to add autometrics to all functions instrumented with `tracing::instrument`. 83 | 84 | Replace: 85 | ```rust 86 | #[instrument] 87 | ``` 88 | With: 89 | ```rust 90 | #[instrument] 91 | #[autometrics] 92 | ``` 93 | 94 | And then let Rust Analyzer tell you which files you need to add `use autometrics::autometrics` at the top of. 95 | 96 |
97 |
98 | 99 | Tip: Adding autometrics to all pub functions (not necessarily recommended 😅) 100 | 101 |
102 | 103 | You can use a search and replace to add autometrics to all public functions. Yes, this is a bit nuts. 104 | 105 | Use a regular expression search to replace: 106 | ``` 107 | (pub (?:async)? fn.*) 108 | ``` 109 | 110 | With: 111 | ``` 112 | #[autometrics] 113 | $1 114 | ``` 115 | 116 | And then let Rust Analyzer tell you which files you need to add `use autometrics::autometrics` at the top of. 117 | 118 |
119 | 120 | 3. Export the metrics for Prometheus 121 | 122 |
123 | 124 | 125 | For projects not currently using Prometheus metrics 126 | 127 | 128 |
129 | 130 | Autometrics includes optional functions to help collect and prepare metrics to be collected by Prometheus. 131 | 132 | In your `main` function, initialize the `prometheus_exporter`: 133 | 134 | ```rust 135 | pub fn main() { 136 | prometheus_exporter::init(); 137 | // ... 138 | } 139 | ``` 140 | 141 | And create a route on your API (probably mounted under `/metrics`) that returns the following: 142 | 143 | ```rust 144 | use autometrics::prometheus_exporter::{self, PrometheusResponse}; 145 | 146 | /// Export metrics for Prometheus to scrape 147 | pub fn get_metrics() -> PrometheusResponse { 148 | prometheus_exporter::encode_http_response() 149 | } 150 | ``` 151 | 152 |
153 | 154 |
155 | 156 | 157 | For projects already using custom Prometheus metrics 158 | 159 | 160 |
161 | 162 | [Configure `autometrics`](https://docs.rs/autometrics/latest/autometrics/#metrics-libraries) to use the same underlying metrics library you use with the feature flag corresponding to the crate and version you are using. 163 | 164 | ```toml 165 | [dependencies] 166 | autometrics = { 167 | version = "*", 168 | features = ["prometheus-0_13"], 169 | default-features = false 170 | } 171 | ``` 172 | 173 | The `autometrics` metrics will be produced alongside yours. 174 | 175 | > **Note** 176 | > 177 | > You must ensure that you are using the exact same version of the library as `autometrics`. If not, the `autometrics` metrics will not appear in your exported metrics. 178 | > This is because Cargo will include both versions of the crate and the global statics used for the metrics registry will be different. 179 | 180 | You do not need to use the Prometheus exporter functions this library provides (you can leave out the `prometheus-exporter` feature flag) and you do not need a separate endpoint for autometrics' metrics. 181 | 182 |
183 | 184 | 4. Run Prometheus locally with the [Autometrics CLI](https://docs.autometrics.dev/local-development#getting-started-with-am) or [configure it manually](https://github.com/autometrics-dev#5-configuring-prometheus) to scrape your metrics endpoint 185 | 5. (Optional) If you have Grafana, import the [Autometrics dashboards](https://github.com/autometrics-dev/autometrics-shared#dashboards) for an overview and detailed view of the function metrics 186 | 187 | ## [API Docs](https://docs.rs/autometrics) 188 | 189 | ## [Examples](./examples) 190 | 191 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/autometrics-dev/autometrics-rs) 192 | 193 | To see autometrics in action: 194 | 195 | 1. Install [prometheus](https://prometheus.io/download/) locally or download the [Autometrics CLI](https://docs.autometrics.dev/local-development#getting-started-with-am) which will install and configure Prometheus for you locally. 196 | 2. Run the [complete example](./examples/full-api): 197 | 198 | ```shell 199 | cargo run -p example-full-api 200 | ``` 201 | 202 | 3. Hover over the [function names](./examples/full-api/src/routes.rs#L13) to see the generated query links 203 | (like in the image above) and view the Prometheus charts 204 | 205 | ## Benchmarks 206 | 207 | Using each of the following metrics libraries, tracking metrics with the `autometrics` macro adds approximately: 208 | - `prometheus-0_13`: 140-150 nanoseconds 209 | - `prometheus-client-0_21`: 150-250 nanoseconds 210 | - `metrics-0_21`: 550-650 nanoseconds 211 | - `opentelemetry-0_20`: 1700-2100 nanoseconds 212 | 213 | These were calculated on a 2021 MacBook Pro with the M1 Max chip and 64 GB of RAM. 214 | 215 | To run the benchmarks yourself, run the following command, replacing `BACKEND` with the metrics library of your choice: 216 | ```sh 217 | cargo bench --features prometheus-exporter,BACKEND 218 | ``` 219 | 220 | ## Contributing 221 | 222 | Issues, feature suggestions, and pull requests are very welcome! 223 | 224 | If you are interested in getting involved: 225 | - Join the conversation on [Discord](https://discord.gg/9eqGEs56UB) 226 | - Ask questions and share ideas in the [Github Discussions](https://github.com/orgs/autometrics-dev/discussions) 227 | - Take a look at the overall [Autometrics Project Roadmap](https://github.com/orgs/autometrics-dev/projects/1) 228 | -------------------------------------------------------------------------------- /autometrics-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autometrics-cli" 3 | description = "The command line interface for autometrics" 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | authors = { workspace = true } 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | license = { workspace = true } 10 | keywords = { workspace = true } 11 | categories = { workspace = true } 12 | 13 | [dependencies] 14 | clap = { version = "4.1", features = ["derive"] } 15 | -------------------------------------------------------------------------------- /autometrics-cli/README.md: -------------------------------------------------------------------------------- 1 | # Autometrics CLI 2 | 3 | Currently, the CLI is only used to regenerate the [recording & alerting rules file](https://github.com/autometrics-dev/autometrics-shared#prometheus-recording--alerting-rules). 4 | 5 | You will only need to use this if you want to use objective percentiles other than the default set: 90%, 95%, 99%, 99.9%. 6 | 7 | To generate the rules file: 8 | 1. Clone this repo 9 | ```sh 10 | git clone https://github.com/autometrics-dev/autometrics-rs.git 11 | cd autometrics-rs 12 | ``` 13 | 2. Run the CLI to generate a Sloth YAML file: 14 | ```sh 15 | cargo run -p autometrics-cli generate-sloth-file -- --objectives=90,95,99,99.9 --output sloth.yml 16 | ``` 17 | 3. Run Sloth to generate the Prometheus recording and alerting rules file: 18 | ```sh 19 | docker run -v $(pwd):/data ghcr.io/slok/sloth generate -i /data/sloth.yml -o /data/autometrics.rules.yml 20 | ``` 21 | -------------------------------------------------------------------------------- /autometrics-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | mod sloth; 4 | 5 | #[derive(Parser)] 6 | #[command(name = "autometrics", about)] 7 | enum Cli { 8 | /// Generate an SLO definition file for use with 9 | GenerateSlothFile(sloth::Arguments), 10 | } 11 | 12 | fn main() { 13 | match Cli::parse() { 14 | Cli::GenerateSlothFile(command) => command.run(), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /autometrics-cli/src/sloth.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::{fs::write, path::PathBuf}; 3 | 4 | #[derive(Parser)] 5 | pub struct Arguments { 6 | /// The objective percentages to support. 7 | /// 8 | /// Note that the objective used in autometrics-instrumented code must match 9 | /// one of these values in order for the alert to work. 10 | #[clap(long, default_values = &["90", "95", "99", "99.9"])] 11 | objectives: Vec, 12 | 13 | /// Minimum traffic to trigger alerts, specified as events/minute. 14 | /// 15 | /// Alerts will only trigger for an objective if the total call-rate of functions 16 | /// comprising the objective is greather than this threshold. 17 | /// 18 | /// Defaults to "at least 1 event per minute" 19 | /// 20 | /// Note that the total of calls is made on matching _both_ the "name" 21 | /// attribute and the percentile targets; e.g. a function from an "API, 90%" 22 | /// objective and one from an "API, 99%" objective count for 2 separate 23 | /// low-traffic threshold. 24 | #[clap(short, long, default_value_t = 1.0)] 25 | alerting_traffic_threshold: f64, 26 | 27 | /// Output path where the SLO file should be written. 28 | /// 29 | /// If not specified, the SLO file will be printed to stdout. 30 | #[clap(short, long)] 31 | output: Option, 32 | } 33 | 34 | impl Arguments { 35 | pub fn run(&self) { 36 | let sloth_file = 37 | generate_sloth_file(&self.objectives, self.alerting_traffic_threshold / 60.0); 38 | if let Some(output_path) = &self.output { 39 | write(output_path, sloth_file) 40 | .unwrap_or_else(|err| panic!("Error writing SLO file to {output_path:?}: {err}")); 41 | } else { 42 | println!("{}", sloth_file); 43 | } 44 | } 45 | } 46 | 47 | fn generate_sloth_file(objectives: &[impl AsRef], min_calls_per_second: f64) -> String { 48 | let mut sloth_file = "version: prometheus/v1 49 | service: autometrics 50 | slos: 51 | " 52 | .to_string(); 53 | 54 | for objective in objectives { 55 | sloth_file.push_str(&generate_success_rate_slo( 56 | objective.as_ref(), 57 | min_calls_per_second, 58 | )); 59 | } 60 | for objective in objectives { 61 | sloth_file.push_str(&generate_latency_slo( 62 | objective.as_ref(), 63 | min_calls_per_second, 64 | )); 65 | } 66 | 67 | sloth_file 68 | } 69 | 70 | fn generate_success_rate_slo(objective_percentile: &str, min_calls_per_second: f64) -> String { 71 | let objective_percentile_no_decimal = objective_percentile.replace('.', "_"); 72 | 73 | format!(" - name: success-rate-{objective_percentile_no_decimal} 74 | objective: {objective_percentile} 75 | description: Common SLO based on function success rates 76 | sli: 77 | events: 78 | error_query: sum by (objective_name, objective_percentile, service_name) (rate({{__name__=~\"function_calls(_count)?(_total)?\",objective_percentile=\"{objective_percentile}\",result=\"error\"}}[{{{{.window}}}}])) 79 | total_query: sum by (objective_name, objective_percentile, service_name) (rate({{__name__=~\"function_calls(_count)?(_total)?\",objective_percentile=\"{objective_percentile}\"}}[{{{{.window}}}}])) >= {min_calls_per_second} 80 | alerting: 81 | name: High Error Rate SLO - {objective_percentile}% 82 | labels: 83 | category: success-rate 84 | annotations: 85 | summary: \"High error rate on the `{{{{$labels.objective_name}}}}` SLO for the `{{{{$labels.service_name}}}}` service\" 86 | page_alert: 87 | labels: 88 | severity: page 89 | ticket_alert: 90 | labels: 91 | severity: ticket 92 | ") 93 | } 94 | 95 | fn generate_latency_slo(objective_percentile: &str, min_calls_per_second: f64) -> String { 96 | let objective_percentile_no_decimal = objective_percentile.replace('.', "_"); 97 | 98 | format!(" - name: latency-{objective_percentile_no_decimal} 99 | objective: {objective_percentile} 100 | description: Common SLO based on function latency 101 | sli: 102 | events: 103 | error_query: > 104 | sum by (objective_name, objective_percentile, service_name) (rate({{__name__=~\"function_calls_duration(_seconds)?_count\", objective_percentile=\"{objective_percentile}\"}}[{{{{.window}}}}])) 105 | - 106 | (sum by (objective_name, objective_percentile, service_name) ( 107 | label_join(rate({{__name__=~\"function_calls_duration(_seconds)?_bucket\", objective_percentile=\"{objective_percentile}\"}}[{{{{.window}}}}]), \"autometrics_check_label_equality\", \"\", \"objective_latency_threshold\") 108 | and 109 | label_join(rate({{__name__=~\"function_calls_duration(_seconds)?_bucket\", objective_percentile=\"{objective_percentile}\"}}[{{{{.window}}}}]), \"autometrics_check_label_equality\", \"\", \"le\") 110 | )) 111 | total_query: sum by (objective_name, objective_percentile, service_name) (rate({{__name__=~\"function_calls_duration(_seconds)?_count\", objective_percentile=\"{objective_percentile}\"}}[{{{{.window}}}}])) >= {min_calls_per_second} 112 | alerting: 113 | name: High Latency SLO - {objective_percentile}% 114 | labels: 115 | category: latency 116 | annotations: 117 | summary: \"High latency on the `{{{{$labels.objective_name}}}}` SLO for the `{{{{$labels.service_name}}}}` service\" 118 | page_alert: 119 | labels: 120 | severity: page 121 | ticket_alert: 122 | labels: 123 | severity: ticket 124 | ") 125 | } 126 | -------------------------------------------------------------------------------- /autometrics-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autometrics-macros" 3 | version = { workspace = true } 4 | edition = { workspace = true } 5 | authors = { workspace = true } 6 | description = { workspace = true } 7 | documentation = { workspace = true } 8 | repository = { workspace = true } 9 | homepage = { workspace = true } 10 | license = { workspace = true } 11 | keywords = { workspace = true } 12 | categories = { workspace = true } 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | percent-encoding = "2.2" 19 | proc-macro2 = "1" 20 | quote = "1" 21 | regex = "1.10.2" 22 | syn = { version = "2", features = ["full"] } 23 | -------------------------------------------------------------------------------- /autometrics-macros/src/parse.rs: -------------------------------------------------------------------------------- 1 | use syn::parse::{Parse, ParseStream}; 2 | use syn::{Expr, ItemFn, ItemImpl, LitStr, Result, Token}; 3 | 4 | mod kw { 5 | syn::custom_keyword!(track_concurrency); 6 | syn::custom_keyword!(objective); 7 | syn::custom_keyword!(success_rate); 8 | syn::custom_keyword!(latency); 9 | syn::custom_keyword!(ok_if); 10 | syn::custom_keyword!(error_if); 11 | syn::custom_keyword!(struct_name); 12 | } 13 | 14 | /// Autometrics can be applied to individual functions or to 15 | /// (all of the methods within) impl blocks. 16 | pub(crate) enum Item { 17 | Function(ItemFn), 18 | Impl(ItemImpl), 19 | } 20 | 21 | impl Parse for Item { 22 | fn parse(input: ParseStream) -> Result { 23 | input 24 | .parse() 25 | .map(Item::Function) 26 | .or_else(|_| input.parse().map(Item::Impl)) 27 | } 28 | } 29 | 30 | #[derive(Default)] 31 | pub(crate) struct AutometricsArgs { 32 | pub track_concurrency: bool, 33 | pub ok_if: Option, 34 | pub error_if: Option, 35 | pub objective: Option, 36 | 37 | // Fix for https://github.com/autometrics-dev/autometrics-rs/issues/139. 38 | pub struct_name: Option, 39 | } 40 | 41 | impl Parse for AutometricsArgs { 42 | fn parse(input: ParseStream) -> Result { 43 | let mut args = AutometricsArgs::default(); 44 | while !input.is_empty() { 45 | let lookahead = input.lookahead1(); 46 | if lookahead.peek(kw::track_concurrency) { 47 | let _ = input.parse::()?; 48 | args.track_concurrency = true; 49 | } else if lookahead.peek(kw::ok_if) { 50 | if args.ok_if.is_some() { 51 | return Err(input.error("expected only a single `ok_if` argument")); 52 | } 53 | if args.error_if.is_some() { 54 | return Err(input.error("cannot use both `ok_if` and `error_if`")); 55 | } 56 | let ok_if = input.parse::>()?; 57 | args.ok_if = Some(ok_if.value); 58 | } else if lookahead.peek(kw::error_if) { 59 | if args.error_if.is_some() { 60 | return Err(input.error("expected only a single `error_if` argument")); 61 | } 62 | if args.ok_if.is_some() { 63 | return Err(input.error("cannot use both `ok_if` and `error_if`")); 64 | } 65 | let error_if = input.parse::>()?; 66 | args.error_if = Some(error_if.value); 67 | } else if lookahead.peek(kw::objective) { 68 | let _ = input.parse::()?; 69 | let _ = input.parse::()?; 70 | if args.objective.is_some() { 71 | return Err(input.error("expected only a single `objective` argument")); 72 | } 73 | args.objective = Some(input.parse()?); 74 | } else if lookahead.peek(kw::struct_name) { 75 | let _ = input.parse::()?; 76 | let _ = input.parse::()?; 77 | let struct_name = input.parse::()?.value(); 78 | args.struct_name = Some(struct_name); 79 | } else if lookahead.peek(Token![,]) { 80 | let _ = input.parse::()?; 81 | } else { 82 | return Err(lookahead.error()); 83 | } 84 | } 85 | Ok(args) 86 | } 87 | } 88 | 89 | struct ExprArg { 90 | value: Expr, 91 | _p: std::marker::PhantomData, 92 | } 93 | 94 | impl Parse for ExprArg { 95 | fn parse(input: ParseStream<'_>) -> syn::Result { 96 | let _ = input.parse::()?; 97 | let _ = input.parse::()?; 98 | let value = input.parse()?; 99 | Ok(Self { 100 | value, 101 | _p: std::marker::PhantomData, 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /autometrics-macros/src/result_labels.rs: -------------------------------------------------------------------------------- 1 | //! The definition of the ResultLabels derive macro, see 2 | //! autometrics::ResultLabels for more information. 3 | 4 | use proc_macro2::TokenStream; 5 | use quote::quote; 6 | use syn::{ 7 | punctuated::Punctuated, token::Comma, Attribute, Data, DataEnum, DeriveInput, Error, Expr, 8 | ExprLit, Ident, Lit, LitStr, Result, Variant, 9 | }; 10 | 11 | // These labels must match autometrics::ERROR_KEY and autometrics::OK_KEY, 12 | // to avoid a dependency loop just for 2 constants we recreate these here. 13 | const OK_KEY: &str = "ok"; 14 | const ERROR_KEY: &str = "error"; 15 | const RESULT_KEY: &str = "result"; 16 | const ATTR_LABEL: &str = "label"; 17 | const ACCEPTED_LABELS: [&str; 2] = [ERROR_KEY, OK_KEY]; 18 | 19 | /// Entry point of the ResultLabels macro 20 | pub(crate) fn expand(input: DeriveInput) -> Result { 21 | let variants = match &input.data { 22 | Data::Enum(DataEnum { variants, .. }) => variants, 23 | _ => { 24 | return Err(Error::new_spanned( 25 | input, 26 | "ResultLabels only works with 'Enum's.", 27 | )) 28 | } 29 | }; 30 | let enum_name = &input.ident; 31 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 32 | let conditional_clauses_for_labels = conditional_label_clauses(variants, enum_name)?; 33 | 34 | Ok(quote! { 35 | #[automatically_derived] 36 | impl #impl_generics ::autometrics::__private::GetLabels for #enum_name #ty_generics #where_clause { 37 | fn __autometrics_get_labels(&self) -> Option<&'static str> { 38 | #conditional_clauses_for_labels 39 | } 40 | } 41 | }) 42 | } 43 | 44 | /// Build the list of match clauses for the generated code. 45 | fn conditional_label_clauses( 46 | variants: &Punctuated, 47 | enum_name: &Ident, 48 | ) -> Result { 49 | let clauses: Vec = variants 50 | .iter() 51 | .map(|variant| { 52 | let variant_name = &variant.ident; 53 | let variant_matcher: TokenStream = match variant.fields { 54 | syn::Fields::Named(_) => quote! { #variant_name {..} }, 55 | syn::Fields::Unnamed(_) => quote! { #variant_name (_) }, 56 | syn::Fields::Unit => quote! { #variant_name }, 57 | }; 58 | if let Some(key) = extract_label_attribute(&variant.attrs)? { 59 | Ok(quote! [ 60 | else if ::std::matches!(self, & #enum_name :: #variant_matcher) { 61 | Some(#key) 62 | } 63 | ]) 64 | } else { 65 | // Let the code flow through the last value 66 | Ok(quote! {}) 67 | } 68 | }) 69 | .collect::>>()?; 70 | 71 | Ok(quote! [ 72 | if false { 73 | None 74 | } 75 | #(#clauses)* 76 | else { 77 | None 78 | } 79 | ]) 80 | } 81 | 82 | /// Extract the wanted label from the annotation in the variant, if present. 83 | /// The function looks for `#[label(result = "ok")]` kind of labels. 84 | /// 85 | /// ## Error cases 86 | /// 87 | /// The function will error out with the smallest possible span when: 88 | /// 89 | /// - The attribute on a variant is not a "list" type (so `#[label]` is not allowed), 90 | /// - The key in the key value pair is not "result", as it's the only supported keyword 91 | /// for now (so `#[label(non_existing_label = "ok")]` is not allowed), 92 | /// - The value for the "result" label is not in the autometrics supported set (so 93 | /// `#[label(result = "random label that will break queries")]` is not allowed) 94 | fn extract_label_attribute(attrs: &[Attribute]) -> Result> { 95 | attrs 96 | .iter() 97 | .find_map(|att| match &att.meta { 98 | syn::Meta::List(list) => { 99 | // Ignore attribute if it's not `label(...)` 100 | if list.path.segments.len() != 1 || list.path.segments[0].ident != ATTR_LABEL { 101 | return None; 102 | } 103 | 104 | // Only lists are allowed 105 | let pair = match att.meta.require_list().and_then(|list| list.parse_args::()) { 106 | Ok(pair) => pair, 107 | Err(..) => return Some( 108 | Err( 109 | Error::new_spanned( 110 | &att.meta, 111 | format!("Only `{ATTR_LABEL}({RESULT_KEY} = \"RES\")` (RES can be {OK_KEY:?} or {ERROR_KEY:?}) is supported"), 112 | ), 113 | ), 114 | ), 115 | }; 116 | 117 | // Inside list, only 'result = ...' are allowed 118 | if pair.path.segments.len() != 1 || pair.path.segments[0].ident != RESULT_KEY { 119 | return Some(Err(Error::new_spanned( 120 | pair.path.clone(), 121 | format!("Only `{RESULT_KEY} = \"RES\"` (RES can be {OK_KEY:?} or {ERROR_KEY:?}) is supported"), 122 | ))); 123 | } 124 | 125 | // Inside 'result = val', 'val' must be a string literal 126 | let lit_str = match pair.value { 127 | Expr::Lit(ExprLit { lit: Lit::Str(ref lit_str), .. }) => lit_str, 128 | _ => { 129 | return Some(Err(Error::new_spanned( 130 | &pair.value, 131 | format!("Only {OK_KEY:?} or {ERROR_KEY:?}, as string literals, are accepted as result values"), 132 | ))); 133 | } 134 | }; 135 | 136 | // Inside 'result = val', 'val' must be one of the allowed string literals 137 | if !ACCEPTED_LABELS.contains(&lit_str.value().as_str()) { 138 | return Some(Err(Error::new_spanned( 139 | lit_str, 140 | format!("Only {OK_KEY:?} or {ERROR_KEY:?} are accepted as result values"), 141 | ))); 142 | } 143 | 144 | Some(Ok(lit_str.clone())) 145 | }, 146 | syn::Meta::NameValue(nv) if nv.path.segments.len() == 1 && nv.path.segments[0].ident == ATTR_LABEL => { 147 | Some(Err(Error::new_spanned( 148 | nv, 149 | format!("Only `{ATTR_LABEL}({RESULT_KEY} = \"RES\")` (RES can be {OK_KEY:?} or {ERROR_KEY:?}) is supported"), 150 | ))) 151 | }, 152 | syn::Meta::Path(p) if p.segments.len() == 1 && p.segments[0].ident == ATTR_LABEL => { 153 | Some(Err(Error::new_spanned( 154 | p, 155 | format!("Only `{ATTR_LABEL}({RESULT_KEY} = \"RES\")` (RES can be {OK_KEY:?} or {ERROR_KEY:?}) is supported"), 156 | ))) 157 | }, 158 | _ => None, 159 | }) 160 | .transpose() 161 | } 162 | -------------------------------------------------------------------------------- /autometrics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autometrics" 3 | version = { workspace = true } 4 | edition = { workspace = true } 5 | authors = { workspace = true } 6 | description = { workspace = true } 7 | documentation = { workspace = true } 8 | repository = { workspace = true } 9 | homepage = { workspace = true } 10 | license = { workspace = true } 11 | keywords = { workspace = true } 12 | categories = { workspace = true } 13 | readme = "README.md" 14 | 15 | [features] 16 | # Metrics backends 17 | metrics-0_24 = ["dep:metrics"] 18 | opentelemetry-0_24 = ["opentelemetry/metrics", "dep:prometheus"] 19 | prometheus-0_13 = ["dep:prometheus"] 20 | prometheus-client-0_22 = ["dep:prometheus-client"] 21 | 22 | # Deprecated feature flags 23 | metrics = ["metrics-0_24"] 24 | opentelemetry = ["opentelemetry-0_24"] 25 | prometheus = ["prometheus-0_13"] 26 | prometheus-client = ["prometheus-client-0_22"] 27 | exemplars-tracing-opentelemetry = ["exemplars-tracing-opentelemetry-0_25"] 28 | 29 | # Misc 30 | prometheus-exporter = [ 31 | "http", 32 | "metrics-exporter-prometheus", 33 | "opentelemetry-prometheus", 34 | "opentelemetry_sdk", 35 | "dep:prometheus", 36 | "dep:prometheus-client", 37 | ] 38 | 39 | otel-push-exporter = [ 40 | "opentelemetry_sdk", 41 | "dep:opentelemetry", 42 | "opentelemetry-otlp", 43 | "opentelemetry-otlp/metrics", 44 | "opentelemetry-otlp/tls-roots", 45 | "opentelemetry-otlp/reqwest-rustls" 46 | ] 47 | 48 | otel-push-exporter-http = [ 49 | "otel-push-exporter", 50 | "opentelemetry-otlp/http-proto" 51 | ] 52 | 53 | otel-push-exporter-grpc = [ 54 | "otel-push-exporter", 55 | "opentelemetry-otlp/grpc-tonic" 56 | ] 57 | 58 | otel-push-exporter-tokio = [ 59 | "otel-push-exporter", 60 | "opentelemetry_sdk/rt-tokio" 61 | ] 62 | 63 | otel-push-exporter-tokio-current-thread = [ 64 | "otel-push-exporter", 65 | "opentelemetry_sdk/rt-tokio-current-thread" 66 | ] 67 | 68 | otel-push-exporter-async-std = [ 69 | "otel-push-exporter", 70 | "opentelemetry_sdk/rt-async-std" 71 | ] 72 | 73 | # Exemplars 74 | exemplars-tracing = ["tracing", "tracing-subscriber"] 75 | exemplars-tracing-opentelemetry-0_25 = [ 76 | "dep:opentelemetry", 77 | "opentelemetry_sdk/trace", 78 | "tracing", 79 | "dep:tracing-opentelemetry", 80 | ] 81 | 82 | # Custom objectives 83 | custom-objective-percentile = [] 84 | custom-objective-latency = [] 85 | 86 | [dependencies] 87 | autometrics-macros = { workspace = true } 88 | linkme = "0.3" 89 | once_cell = "1.17" 90 | spez = "0.1.2" 91 | thiserror = "1" 92 | 93 | # Used for opentelemetry feature 94 | opentelemetry = { version = "0.24", default-features = false, optional = true } 95 | 96 | # Use for metrics feature 97 | metrics = { version = "0.24", default-features = false, optional = true } 98 | 99 | # Used for prometheus-exporter feature 100 | http = { version = "1.0.0", optional = true } 101 | metrics-exporter-prometheus = { version = "0.16", default-features = false, optional = true } 102 | opentelemetry-prometheus = { version = "0.17", optional = true } 103 | opentelemetry_sdk = { version = "0.24.1", default-features = false, features = [ 104 | "metrics", 105 | ], optional = true } 106 | opentelemetry-otlp = { version = "0.17", default-features = false, optional = true } 107 | prometheus = { version = "0.13", default-features = false, optional = true } 108 | 109 | # Used for prometheus-client feature 110 | prometheus-client = { version = "0.22", optional = true } 111 | 112 | # Used for exemplars-tracing feature 113 | tracing = { version = "0.1", optional = true } 114 | tracing-subscriber = { version = "0.3", default-features = false, features = [ 115 | "registry", 116 | ], optional = true } 117 | 118 | # Used for exemplars-tracing-opentelemetry feature 119 | tracing-opentelemetry = { version = "0.25", default-features = false, optional = true } 120 | 121 | [dev-dependencies] 122 | async-trait = "0.1.74" 123 | axum = { version = "0.7.2", features = ["tokio"] } 124 | criterion = "0.5" 125 | http = "1.0.0" 126 | opentelemetry = "0.24" 127 | opentelemetry-stdout = { version = "0.5", features = ["trace"] } 128 | prometheus-client = "0.22" 129 | tokio = { version = "1", features = ["full"] } 130 | tracing = "0.1" 131 | tracing-subscriber = "0.3" 132 | trybuild = "1.0" 133 | uuid = { version = "1", features = ["v4"] } 134 | vergen = { version = "8.1", features = ["git", "gitcl"] } 135 | 136 | [build-dependencies] 137 | cfg_aliases = "0.1" 138 | 139 | [package.metadata.docs.rs] 140 | all-features = true 141 | rustdoc-args = ["--cfg", "docsrs"] 142 | 143 | [[bench]] 144 | name = "basic_benchmark" 145 | harness = false 146 | -------------------------------------------------------------------------------- /autometrics/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![GitHub_headerImage](https://user-images.githubusercontent.com/3262610/221191767-73b8a8d9-9f8b-440e-8ab6-75cb3c82f2bc.png) 4 | 5 | [![Documentation](https://docs.rs/autometrics/badge.svg)](https://docs.rs/autometrics) 6 | [![Crates.io](https://img.shields.io/crates/v/autometrics.svg)](https://crates.io/crates/autometrics) 7 | [![Discord Shield](https://discordapp.com/api/guilds/950489382626951178/widget.png?style=shield)](https://discord.gg/kHtwcH8As9) 8 | 9 | Metrics are a powerful and cost-efficient tool for understanding the health and performance of your code in production. But it's hard to decide what metrics to track and even harder to write queries to understand the data. 10 | 11 | Autometrics provides a macro that makes it trivial to instrument any function with the most useful metrics: request rate, error rate, and latency. It standardizes these metrics and then generates powerful Prometheus queries based on your function details to help you quickly identify and debug issues in production. 12 | 13 | ## Benefits 14 | 15 | - [✨ `#[autometrics]`](https://docs.rs/autometrics/latest/autometrics/attr.autometrics.html) macro adds useful metrics to any function or `impl` block, without you thinking about what metrics to collect 16 | - 💡 Generates powerful Prometheus queries to help quickly identify and debug issues in production 17 | - 🔗 Injects links to live Prometheus charts directly into each function's doc comments 18 | - [📊 Grafana dashboards](https://github.com/autometrics-dev/autometrics-shared#dashboards) work without configuration to visualize the performance of functions & [SLOs](https://docs.rs/autometrics/latest/autometrics/objectives/index.html) 19 | - 🔍 Correlates your code's version with metrics to help identify commits that introduced errors or latency 20 | - 📏 Standardizes metrics across services and teams to improve debugging 21 | - ⚖️ Function-level metrics provide useful granularity without exploding cardinality 22 | - [⚡ Minimal runtime overhead](#benchmarks) 23 | 24 | ## Advanced Features 25 | 26 | - [🚨 Define alerts](https://docs.rs/autometrics/latest/autometrics/objectives/index.html) using SLO best practices directly in your source code 27 | - [📍 Attach exemplars](https://docs.rs/autometrics/latest/autometrics/exemplars/index.html) automatically to connect metrics with traces 28 | - [⚙️ Configurable](https://docs.rs/autometrics/latest/autometrics/#metrics-backends) metric collection library ([`opentelemetry`](https://crates.io/crates/opentelemetry), [`prometheus`](https://crates.io/crates/prometheus), [`prometheus-client`](https://crates.io/crates/prometheus-client) or [`metrics`](https://crates.io/crates/metrics)) 29 | 30 | See [autometrics.dev](https://docs.autometrics.dev/) for more details on the ideas behind autometrics. 31 | 32 | ## Example Axum App 33 | 34 | Autometrics isn't tied to any web framework, but this shows how you can use the library in an [Axum](https://github.com/tokio-rs/axum) server. 35 | 36 | ```rust,ignore 37 | use std::error::Error; 38 | use autometrics::{autometrics, prometheus_exporter}; 39 | use axum::{routing::*, Router}; 40 | use std::net::Ipv4Addr; 41 | use tokio::net::TcpListener; 42 | 43 | // Instrument your functions with metrics 44 | #[autometrics] 45 | pub async fn create_user() -> Result<(), ()> { 46 | Ok(()) 47 | } 48 | 49 | // Export the metrics to Prometheus 50 | #[tokio::main] 51 | pub async fn main() -> Result<(), Box> { 52 | prometheus_exporter::init(); 53 | 54 | let app = Router::new() 55 | .route("/users", post(create_user)) 56 | .route( 57 | "/metrics", 58 | get(|| async { prometheus_exporter::encode_http_response() }), 59 | ); 60 | 61 | 62 | let listener = TcpListener::bind((Ipv4Addr::from([127, 0, 0, 1]), 0)).await?; 63 | axum::serve(listener, app).await?; 64 | Ok(()) 65 | } 66 | ``` 67 | 68 | ## Quickstart 69 | 70 | See the [Github repo README](https://github.com/autometrics-dev/autometrics-rs#quickstart) to quickly add `autometrics` to your project. 71 | 72 | ## Contributing 73 | 74 | Issues, feature suggestions, and pull requests are very welcome! 75 | 76 | If you are interested in getting involved: 77 | - Join the conversation on [Discord](https://discord.gg/9eqGEs56UB) 78 | - Ask questions and share ideas in the [Github Discussions](https://github.com/orgs/autometrics-dev/discussions) 79 | - Take a look at the overall [Autometrics Project Roadmap](https://github.com/orgs/autometrics-dev/projects/1) 80 | -------------------------------------------------------------------------------- /autometrics/benches/basic_benchmark.rs: -------------------------------------------------------------------------------- 1 | use autometrics::{autometrics, prometheus_exporter}; 2 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 3 | 4 | #[inline(never)] 5 | pub fn add(a: i32, b: i32) -> i32 { 6 | a + b 7 | } 8 | 9 | #[autometrics] 10 | #[inline(never)] 11 | pub fn instrumented_add(a: i32, b: i32) -> i32 { 12 | a + b 13 | } 14 | 15 | #[inline(never)] 16 | pub fn clone_result(input: String) -> Result { 17 | Ok(input.to_string()) 18 | } 19 | 20 | #[autometrics] 21 | #[inline(never)] 22 | pub fn instrumented_clone_result(input: String) -> Result { 23 | Ok(input.to_string()) 24 | } 25 | 26 | pub fn criterion_benchmark(c: &mut Criterion) { 27 | prometheus_exporter::init(); 28 | 29 | let backend = if cfg!(metrics) { 30 | "metrics" 31 | } else if cfg!(opentelemetry) { 32 | "opentelemetry" 33 | } else if cfg!(prometheus) { 34 | "prometheus" 35 | } else if cfg!(prometheus_client_feature) { 36 | "prometheus-client" 37 | } else { 38 | "unknown" 39 | }; 40 | 41 | let mut add_group = c.benchmark_group("Add"); 42 | add_group.bench_function("baseline", |b| b.iter(|| add(black_box(20), black_box(30)))); 43 | add_group.bench_function(format!("autometrics + {backend}"), |b| { 44 | b.iter(|| instrumented_add(black_box(20), black_box(30))) 45 | }); 46 | add_group.finish(); 47 | 48 | let mut clone_result_group = c.benchmark_group("Clone String and return Result"); 49 | clone_result_group.bench_function("baseline", |b| { 50 | b.iter(|| clone_result(black_box("hello".to_string()))) 51 | }); 52 | clone_result_group.bench_function(format!("autometrics + {backend}"), |b| { 53 | b.iter(|| instrumented_clone_result(black_box("hello".to_string()))); 54 | }); 55 | clone_result_group.finish(); 56 | } 57 | 58 | criterion_group!(benches, criterion_benchmark); 59 | criterion_main!(benches); 60 | -------------------------------------------------------------------------------- /autometrics/build.rs: -------------------------------------------------------------------------------- 1 | use cfg_aliases::cfg_aliases; 2 | 3 | pub fn main() { 4 | println!("cargo:rerun-if-changed=build.rs"); 5 | 6 | #[cfg(feature = "metrics")] 7 | println!("cargo:warning=The `metrics` feature is deprecated and will be removed in the next version. Please use `metrics-0_24` instead."); 8 | #[cfg(feature = "opentelemetry")] 9 | println!("cargo:warning=The `opentelemetry` feature is deprecated and will be removed in the next version. Please use `opentelemetry-0_24` instead."); 10 | #[cfg(feature = "prometheus")] 11 | println!("cargo:warning=The `prometheus` feature is deprecated and will be removed in the next version. Please use `prometheus-0_13` instead."); 12 | #[cfg(feature = "prometheus-client")] 13 | println!("cargo:warning=The `prometheus-client` feature is deprecated and will be removed in the next version. Please use `prometheus-client-0_22` instead."); 14 | #[cfg(feature = "exemplars-tracing-opentelemetry")] 15 | println!("cargo:warning=The `exemplars-tracing-opentelemetry` feature is deprecated and will be removed in the next version. Please use `exemplars-tracing-opentelemetry-0_25` instead."); 16 | 17 | cfg_aliases! { 18 | // Backends 19 | metrics: { any(feature = "metrics", feature = "metrics-0_24") }, 20 | opentelemetry: { any(feature = "opentelemetry", feature = "opentelemetry-0_24") }, 21 | prometheus: { any(feature = "prometheus", feature = "prometheus-0_13") }, 22 | prometheus_client_feature: { any(feature = "prometheus-client", feature = "prometheus-client-0_22") }, 23 | default_backend: { all( 24 | prometheus_exporter, 25 | not(any(metrics, opentelemetry, prometheus, prometheus_client_feature)) 26 | ) }, 27 | prometheus_client: { any(prometheus_client_feature, default_backend) }, 28 | 29 | // Misc 30 | prometheus_exporter: { feature = "prometheus-exporter" }, 31 | 32 | // Exemplars 33 | exemplars: { any(exemplars_tracing, exemplars_tracing_opentelemetry) }, 34 | exemplars_tracing: { feature = "exemplars-tracing" }, 35 | exemplars_tracing_opentelemetry: { any(feature = "exemplars-tracing-opentelemetry-0_25", feature = "exemplars-tracing-opentelemetry") }, 36 | 37 | // Custom objectives 38 | custom_objective_percentile: { feature = "custom-objective-percentile" }, 39 | custom_objective_latency: { feature = "custom-objective-latency" }, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /autometrics/src/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![GitHub_headerImage](https://user-images.githubusercontent.com/3262610/221191767-73b8a8d9-9f8b-440e-8ab6-75cb3c82f2bc.png) 4 | 5 | [![Documentation](https://docs.rs/autometrics/badge.svg)](https://docs.rs/autometrics) 6 | [![Crates.io](https://img.shields.io/crates/v/autometrics.svg)](https://crates.io/crates/autometrics) 7 | [![Discord Shield](https://discordapp.com/api/guilds/950489382626951178/widget.png?style=shield)](https://discord.gg/kHtwcH8As9) 8 | 9 | Metrics are a powerful and cost-efficient tool for understanding the health and performance of your code in production. But it's hard to decide what metrics to track and even harder to write queries to understand the data. 10 | 11 | Autometrics provides a macro that makes it trivial to instrument any function with the most useful metrics: request rate, error rate, and latency. It standardizes these metrics and then generates powerful Prometheus queries based on your function details to help you quickly identify and debug issues in production. 12 | 13 | # Benefits 14 | 15 | - [✨ `#[autometrics]`](autometrics) macro adds useful metrics to any function or `impl` block, without you thinking about what metrics to collect 16 | - 💡 Generates powerful Prometheus queries to help quickly identify and debug issues in production 17 | - 🔗 Injects links to live Prometheus charts directly into each function's doc comments 18 | - [📊 Grafana dashboards](https://github.com/autometrics-dev/autometrics-shared#dashboards) work without configuration to visualize the performance of functions & [SLOs](objectives) 19 | - 🔍 Correlates your code's version with metrics to help identify commits that introduced errors or latency 20 | - 📏 Standardizes metrics across services and teams to improve debugging 21 | - ⚖️ Function-level metrics provide useful granularity without exploding cardinality 22 | - [⚡ Minimal runtime overhead](https://github.com/autometrics-dev/autometrics-rs#benchmarks) 23 | 24 | # Advanced Features 25 | 26 | - [🚨 Define alerts](objectives) using SLO best practices directly in your source code 27 | - [📍 Attach exemplars](exemplars) automatically to connect metrics with traces 28 | - [⚙️ Configurable](#metrics-backends) metric collection library ([`opentelemetry`](https://crates.io/crates/opentelemetry), [`prometheus`](https://crates.io/crates/prometheus), [`prometheus-client`](https://crates.io/crates/prometheus-client) or [`metrics`](https://crates.io/crates/metrics)) 29 | 30 | See [autometrics.dev](https://docs.autometrics.dev/) for more details on the ideas behind autometrics. 31 | 32 | # Example Axum App 33 | 34 | Autometrics isn't tied to any web framework, but this shows how you can use the library in an [Axum](https://github.com/tokio-rs/axum) server. 35 | 36 | ```rust,ignore 37 | use autometrics::{autometrics, prometheus_exporter}; 38 | use axum::{routing::*, Router}; 39 | use std::error::Error; 40 | use std::net::Ipv4Addr; 41 | use tokio::net::TcpListener; 42 | 43 | // Instrument your functions with metrics 44 | #[autometrics] 45 | pub async fn create_user() -> Result<(), ()> { 46 | Ok(()) 47 | } 48 | 49 | // Export the metrics to Prometheus 50 | #[tokio::main] 51 | pub async fn main() -> Result<(), Box> { 52 | prometheus_exporter::init(); 53 | 54 | let app = Router::new() 55 | .route("/users", post(create_user)) 56 | .route( 57 | "/metrics", 58 | get(|| async { prometheus_exporter::encode_http_response() }), 59 | ); 60 | 61 | let listener = TcpListener::bind((Ipv4Addr::from([127, 0, 0, 1]), 0)).await?; 62 | axum::serve(listener, app).await?; 63 | Ok(()) 64 | } 65 | ``` 66 | 67 | # Configuring Autometrics 68 | 69 | Because Autometrics combines a macro and a library, and supports multiple underlying metrics libraries, different settings are configured in different places. 70 | 71 | ## `AutometricsSettings` 72 | 73 | See [`settings`]. 74 | 75 | ## `build.rs` File 76 | 77 | ### Including Git commit details in the metrics 78 | 79 | Autometrics produces a `build_info` metric and writes queries that make it easy to correlate production issues with the commit or version that may have introduced bugs or latency (see [this blog post](https://fiberplane.com/blog/autometrics-rs-0-4-spot-commits-that-introduce-errors-or-slow-down-your-application) for details). 80 | 81 | The `version` label is set automatically based on the version in your crate's `Cargo.toml` file. 82 | 83 | You can set `commit` and `branch` labels using the `AUTOMETRICS_COMMIT` and `AUTOMETRICS_BRANCH` environment variables , or you can use the [`vergen`](https://crates.io/crates/vergen) crate to attach them automatically: 84 | 85 | ```sh 86 | cargo add vergen --features git,gitcl 87 | ``` 88 | 89 | ```rust 90 | // build.rs 91 | 92 | pub fn main() { 93 | vergen::EmitBuilder::builder() 94 | .git_sha(true) 95 | .git_branch() 96 | .emit() 97 | .expect("Unable to generate build info"); 98 | } 99 | ``` 100 | 101 | ### Custom Prometheus URL 102 | 103 | The Autometrics macro inserts Prometheus query links into function documentation. By default, the links point to `http://localhost:9090` but you can configure it to use a custom URL using a compile-time environment variable in your `build.rs` file: 104 | 105 | ```rust 106 | // build.rs 107 | 108 | pub fn main() { 109 | // Reload Rust analyzer after changing the Prometheus URL to regenerate the links 110 | let prometheus_url = "https://your-prometheus-url.example"; 111 | println!("cargo:rustc-env=PROMETHEUS_URL={prometheus_url}"); 112 | } 113 | ``` 114 | 115 | ### Disabling documentation generation 116 | 117 | If you do not want Autometrics to insert Prometheus query links into the function documentation, set the `AUTOMETRICS_DISABLE_DOCS` compile-time environment variable: 118 | 119 | ```rust 120 | // build.rs 121 | 122 | pub fn main() { 123 | println!("cargo:rustc-env=AUTOMETRICS_DISABLE_DOCS=1"); 124 | } 125 | ``` 126 | 127 | ## Feature flags 128 | 129 | ### Exporting metrics 130 | 131 | - `prometheus-exporter` - exports a Prometheus metrics collector and exporter. This is compatible with any of the [Metrics backends](#metrics-backends) and uses `prometheus-client` by default if none are explicitly selected 132 | 133 | ### Pushing metrics 134 | 135 | Easily push collected metrics to a OpenTelemetry collector and compatible software. 136 | Combine one of the transport feature flags together with your runtime feature flag: 137 | 138 | **Transport feature flags**: 139 | 140 | - `otel-push-exporter-http` - metrics sent over HTTP(s) using `hyper` 141 | - `otel-push-exporter-grpc` - metrics sent over gRPC using `tonic` 142 | 143 | **Runtime feature flags**: 144 | 145 | - `otel-push-exporter-tokio` - tokio 146 | - `otel-push-exporter-tokio-current-thread` - tokio with `flavor = "current_thread"` 147 | - `otel-push-exporter-async-std` - async-std 148 | 149 | If you require more customization than these offered feature flags, enable just 150 | `otel-push-exporter` and follow the [example](https://github.com/autometrics-dev/autometrics-rs/tree/main/examples/opentelemetry-push-custom). 151 | 152 | ### Metrics backends 153 | 154 | > If you are exporting metrics yourself rather than using the `prometheus-exporter`, you must ensure that you are using the exact same version of the metrics library as `autometrics` (and it must come from `crates.io` rather than git or another source). If not, the autometrics metrics will not appear in your exported metrics. 155 | 156 | - `opentelemetry-0_24` - use the [opentelemetry](https://crates.io/crates/opentelemetry) crate for producing metrics. 157 | - `metrics-0_24` - use the [metrics](https://crates.io/crates/metrics) crate for producing metrics 158 | - `prometheus-0_13` - use the [prometheus](https://crates.io/crates/prometheus) crate for producing metrics 159 | - `prometheus-client-0_22` - use the official [prometheus-client](https://crates.io/crates/prometheus-client) crate for producing metrics 160 | 161 | ### Exemplars (for integrating metrics with traces) 162 | 163 | See the [exemplars module docs](https://docs.rs/autometrics/latest/autometrics/exemplars/index.html) for details about these features. Currently only supported with the `prometheus-client` backend. 164 | 165 | - `exemplars-tracing` - extract arbitrary fields from `tracing::Span`s 166 | - `exemplars-tracing-opentelemetry-0_25` - extract the `trace_id` and `span_id` from the `opentelemetry::Context`, which is attached to `tracing::Span`s by the `tracing-opentelemetry` crate 167 | 168 | ### Custom objective values 169 | 170 | By default, Autometrics supports a fixed set of percentiles and latency thresholds for [`objectives`]. Use these features to enable custom values: 171 | 172 | - `custom-objective-latency` - enable this to use custom latency thresholds. Note, however, that the custom latency **must** match one of the buckets configured for your histogram or the queries, recording rules, and alerts will not work. 173 | - `custom-objective-percentile` - enable this to use custom objective percentiles. Note, however, that using custom percentiles requires generating a different recording and alerting rules file using the CLI + Sloth (see [here](https://github.com/autometrics-dev/autometrics-rs/tree/main/autometrics-cli)). 174 | -------------------------------------------------------------------------------- /autometrics/src/constants.rs: -------------------------------------------------------------------------------- 1 | // Version of the autometrics spec we are targeting 2 | pub const AUTOMETRICS_SPEC_TARGET: &str = "1.0.0"; 3 | 4 | // Metrics 5 | pub const COUNTER_NAME: &str = "function.calls"; 6 | pub const HISTOGRAM_NAME: &str = "function.calls.duration"; 7 | pub const GAUGE_NAME: &str = "function.calls.concurrent"; 8 | pub const BUILD_INFO_NAME: &str = "build_info"; 9 | 10 | // Prometheus-flavored metric names 11 | pub const COUNTER_NAME_PROMETHEUS: &str = "function_calls_total"; 12 | pub const HISTOGRAM_NAME_PROMETHEUS: &str = "function_calls_duration_seconds"; 13 | pub const GAUGE_NAME_PROMETHEUS: &str = "function_calls_concurrent"; 14 | 15 | // Descriptions 16 | pub const COUNTER_DESCRIPTION: &str = "Autometrics counter for tracking function calls"; 17 | pub const HISTOGRAM_DESCRIPTION: &str = "Autometrics histogram for tracking function call duration"; 18 | pub const GAUGE_DESCRIPTION: &str = "Autometrics gauge for tracking concurrent function calls"; 19 | pub const BUILD_INFO_DESCRIPTION: &str = 20 | "Autometrics info metric for tracking software version and build details"; 21 | 22 | // Labels 23 | pub const FUNCTION_KEY: &str = "function"; 24 | pub const MODULE_KEY: &str = "module"; 25 | pub const CALLER_FUNCTION_KEY: &str = "caller.function"; 26 | pub const CALLER_FUNCTION_PROMETHEUS: &str = "caller_function"; 27 | pub const CALLER_MODULE_KEY: &str = "caller.module"; 28 | pub const CALLER_MODULE_PROMETHEUS: &str = "caller_module"; 29 | pub const RESULT_KEY: &str = "result"; 30 | pub const OK_KEY: &str = "ok"; 31 | pub const ERROR_KEY: &str = "error"; 32 | pub const OBJECTIVE_NAME: &str = "objective.name"; 33 | pub const OBJECTIVE_NAME_PROMETHEUS: &str = "objective_name"; 34 | pub const OBJECTIVE_PERCENTILE: &str = "objective.percentile"; 35 | pub const OBJECTIVE_PERCENTILE_PROMETHEUS: &str = "objective_percentile"; 36 | pub const OBJECTIVE_LATENCY_THRESHOLD: &str = "objective.latency.threshold"; 37 | pub const OBJECTIVE_LATENCY_THRESHOLD_PROMETHEUS: &str = "objective_latency_threshold"; 38 | pub const VERSION_KEY: &str = "version"; 39 | pub const COMMIT_KEY: &str = "commit"; 40 | pub const BRANCH_KEY: &str = "branch"; 41 | pub const SERVICE_NAME_KEY: &str = "service.name"; 42 | pub const SERVICE_NAME_KEY_PROMETHEUS: &str = "service_name"; 43 | pub const REPO_URL_KEY: &str = "repository.url"; 44 | pub const REPO_URL_KEY_PROMETHEUS: &str = "repository_url"; 45 | pub const REPO_PROVIDER_KEY: &str = "repository.provider"; 46 | pub const REPO_PROVIDER_KEY_PROMETHEUS: &str = "repository_provider"; 47 | pub const AUTOMETRICS_VERSION_KEY: &str = "autometrics.version"; 48 | pub const AUTOMETRICS_VERSION_KEY_PROMETHEUS: &str = "autometrics_version"; 49 | -------------------------------------------------------------------------------- /autometrics/src/exemplars/mod.rs: -------------------------------------------------------------------------------- 1 | //! Connect metrics to traces using exemplars. 2 | //! 3 | //! Exemplars are a newer Prometheus / OpenMetrics / OpenTelemetry feature that allows you to associate 4 | //! specific traces or samples with a given metric. This enables you to investigate what caused metrics 5 | //! to change by looking at individual examples that contributed to the metrics. 6 | //! 7 | //! Autometrics integrates with tracing libraries to extract details from the 8 | //! current span context and automatically attach them as exemplars to the generated metrics. 9 | //! 10 | //! # Supported metrics libraries 11 | //! 12 | //! Exemplars are currently only supported with the `prometheus-client` metrics library, 13 | //! because that is the only one that currently supports producing metrics with exemplars. 14 | //! 15 | //! # Exposing metrics to Prometheus with exemplars 16 | //! 17 | //! To enable Prometheus to scrape metrics with exemplars you must: 18 | //! 1. Run Prometheus with the [`--enable-feature=exemplar-storage`](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage) flag 19 | //! 2. Export the metrics to Prometheus using the provided [`prometheus_exporter::encode_http_response`] or 20 | //! make sure to manually set the `Content-Type` header to indicate it is using the the OpenMetrics format, 21 | //! rather than the default Prometheus format: 22 | //! ```http 23 | //! Content-Type: application/openmetrics-text; version=1.0.0; charset=utf-8 24 | //! ``` 25 | //! 26 | //! [`prometheus_exporter::encode_http_response`]: crate::prometheus_exporter::encode_http_response 27 | //! 28 | //! # Tracing libraries 29 | //! 30 | //! ## [`tracing`](https://crates.io/crates/tracing) 31 | //! 32 | //! See the [`tracing` submodule docs](tracing). 33 | //! 34 | //! ## [`tracing-opentelemetry`](https://crates.io/crates/tracing-opentelemetry) 35 | //! 36 | //! Extract exemplars from the OpenTelemetry Context attached to the current tracing Span. 37 | //! 38 | //! This works in the following way: 39 | //! 1. Add the [`tracing_opentelemetry::OpenTelemetryLayer`] to your tracing subscriber 40 | //! 2. That layer ensures that there is an [`opentelemetry::Context`] attached to every [`tracing::Span`] 41 | //! 3. Spans can be manually created or created for every function using the [`tracing::instrument`] macro 42 | //! 4. Autometrics extracts the `trace_id` and `span_id` from the `Context` and attaches them as exemplars to the generated metrics 43 | //! 44 | //! See the `exemplars-tracing-opentelemetry` example for usage details. 45 | //! 46 | //! [`tracing_opentelemetry::OpenTelemetryLayer`]: https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/struct.OpenTelemetryLayer.html 47 | //! [`opentelemetry::Context`]: https://docs.rs/opentelemetry/latest/opentelemetry/struct.Context.html 48 | //! [`tracing::Span`]: https://docs.rs/tracing/latest/tracing/struct.Span.html 49 | //! [`tracing::instrument`]: https://docs.rs/tracing/latest/tracing/attr.instrument.html 50 | 51 | use std::collections::HashMap; 52 | 53 | #[cfg(exemplars_tracing)] 54 | pub mod tracing; 55 | #[cfg(exemplars_tracing_opentelemetry)] 56 | mod tracing_opentelemetry; 57 | 58 | #[cfg(all(not(doc), exemplars_tracing, exemplars_tracing_opentelemetry))] 59 | compile_error!("Only one of the exemplars-tracing and exemplars-tracing-opentelemetry features can be enabled at a time"); 60 | 61 | #[cfg(not(prometheus_client))] 62 | compile_error!("Exemplars can only be used with the `prometheus-client` metrics library because that is the only one that currently supports producing metrics with exemplars"); 63 | 64 | pub(crate) type TraceLabels = HashMap<&'static str, String>; 65 | pub(crate) fn get_exemplar() -> Option { 66 | #[cfg(exemplars_tracing_opentelemetry)] 67 | { 68 | tracing_opentelemetry::get_exemplar() 69 | } 70 | #[cfg(exemplars_tracing)] 71 | { 72 | tracing::get_exemplar() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /autometrics/src/exemplars/tracing.rs: -------------------------------------------------------------------------------- 1 | //! Extract fields from [`tracing::Span`]s as exemplars. 2 | //! 3 | //! This module enables autometrics to use fields from the current [`Span`] as exemplar labels. 4 | //! 5 | //! # Example 6 | //! 7 | //! ```rust 8 | //! use autometrics::autometrics; 9 | //! use autometrics::exemplars::tracing::AutometricsExemplarExtractor; 10 | //! use tracing::{instrument, trace}; 11 | //! use tracing_subscriber::prelude::*; 12 | //! use uuid::Uuid; 13 | //! 14 | //! #[autometrics] 15 | //! #[instrument(fields(trace_id = %Uuid::new_v4()))] 16 | //! fn my_function() { 17 | //! trace!("Hello world!"); 18 | //! } 19 | //! 20 | //! fn main() { 21 | //! tracing_subscriber::fmt::fmt() 22 | //! .finish() 23 | //! .with(AutometricsExemplarExtractor::from_fields(&["trace_id"])) 24 | //! .init(); 25 | //! } 26 | //! ``` 27 | //! 28 | //! [`Span`]: tracing::Span 29 | 30 | use super::TraceLabels; 31 | use tracing::field::{Field, Visit}; 32 | use tracing::{span::Attributes, Id, Subscriber}; 33 | use tracing_subscriber::layer::{Context, Layer}; 34 | use tracing_subscriber::registry::{LookupSpan, Registry}; 35 | 36 | /// Get the exemplar from the current tracing span 37 | pub(crate) fn get_exemplar() -> Option { 38 | let span = tracing::span::Span::current(); 39 | 40 | span.with_subscriber(|(id, sub)| { 41 | sub.downcast_ref::() 42 | .and_then(|reg| reg.span(id)) 43 | .and_then(|span| { 44 | span.scope() 45 | .find_map(|span| span.extensions().get::().cloned()) 46 | }) 47 | }) 48 | .flatten() 49 | } 50 | 51 | /// A [`tracing_subscriber::Layer`] that enables autometrics to use fields from the current span as exemplars for 52 | /// the metrics it produces. 53 | /// 54 | /// # Example 55 | /// ```rust 56 | /// use autometrics::exemplars::tracing::AutometricsExemplarExtractor; 57 | /// use tracing_subscriber::prelude::*; 58 | /// 59 | /// fn main() { 60 | /// tracing_subscriber::fmt::fmt() 61 | /// .finish() 62 | /// .with(AutometricsExemplarExtractor::from_fields(&["trace_id"])) 63 | /// .init(); 64 | /// } 65 | /// ``` 66 | #[derive(Clone)] 67 | pub struct AutometricsExemplarExtractor { 68 | fields: &'static [&'static str], 69 | } 70 | 71 | impl AutometricsExemplarExtractor { 72 | /// Create a new [`AutometricsExemplarExtractor`] that will extract the given fields from the current [`Span`] scope 73 | /// to use as the labels for the exemplars. 74 | /// 75 | /// [`Span`]: tracing::Span 76 | pub fn from_fields(fields: &'static [&'static str]) -> Self { 77 | Self { fields } 78 | } 79 | } 80 | 81 | impl LookupSpan<'lookup>> Layer for AutometricsExemplarExtractor { 82 | fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { 83 | let mut visitor = TraceLabelVisitor::new(self.fields); 84 | attrs.values().record(&mut visitor); 85 | 86 | if !visitor.labels.is_empty() { 87 | if let Some(span) = ctx.span(id) { 88 | let mut ext = span.extensions_mut(); 89 | ext.insert(visitor.labels); 90 | } 91 | } 92 | } 93 | } 94 | 95 | struct TraceLabelVisitor { 96 | fields: &'static [&'static str], 97 | labels: TraceLabels, 98 | } 99 | 100 | impl TraceLabelVisitor { 101 | fn new(fields: &'static [&'static str]) -> Self { 102 | Self { 103 | fields, 104 | labels: TraceLabels::with_capacity(fields.len()), 105 | } 106 | } 107 | } 108 | 109 | impl Visit for TraceLabelVisitor { 110 | fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { 111 | if self.fields.contains(&field.name()) && !self.labels.contains_key(field.name()) { 112 | self.labels.insert(field.name(), format!("{:?}", value)); 113 | } 114 | } 115 | 116 | fn record_str(&mut self, field: &Field, value: &str) { 117 | if self.fields.contains(&field.name()) && !self.labels.contains_key(field.name()) { 118 | self.labels.insert(field.name(), value.to_string()); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /autometrics/src/exemplars/tracing_opentelemetry.rs: -------------------------------------------------------------------------------- 1 | use super::TraceLabels; 2 | use opentelemetry::trace::TraceContextExt as _; 3 | use std::iter::FromIterator; 4 | use tracing::Span; 5 | use tracing_opentelemetry::OpenTelemetrySpanExt; 6 | 7 | pub fn get_exemplar() -> Option { 8 | // Get the OpenTelemetry Context from the tracing span 9 | let context = OpenTelemetrySpanExt::context(&Span::current()); 10 | 11 | // Now get the OpenTelemetry "span" from the Context 12 | // (it's confusing because the word "span" is used by both tracing and OpenTelemetry 13 | // to mean slightly different things) 14 | let span = context.span(); 15 | let span_context = span.span_context(); 16 | 17 | if span_context.is_valid() { 18 | Some(TraceLabels::from_iter([ 19 | ("trace_id", span_context.trace_id().to_string()), 20 | ("span_id", span_context.span_id().to_string()), 21 | ])) 22 | } else { 23 | None 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /autometrics/src/otel_push_exporter.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::metrics::MetricsError; 2 | use opentelemetry_otlp::{ExportConfig, Protocol, WithExportConfig}; 3 | use opentelemetry_otlp::{OtlpMetricPipeline, OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT}; 4 | use opentelemetry_sdk::metrics::SdkMeterProvider; 5 | use std::ops::Deref; 6 | use std::time::Duration; 7 | 8 | /// Newtype struct holding a [`SdkMeterProvider`] with a custom `Drop` implementation to automatically clean up itself 9 | #[repr(transparent)] 10 | #[must_use = "Assign this to a unused variable instead: `let _meter = ...` (NOT `let _ = ...`), as else it will be dropped immediately - which will cause it to be shut down"] 11 | pub struct OtelMeterProvider(SdkMeterProvider); 12 | 13 | impl Deref for OtelMeterProvider { 14 | type Target = SdkMeterProvider; 15 | 16 | fn deref(&self) -> &Self::Target { 17 | &self.0 18 | } 19 | } 20 | 21 | impl Drop for OtelMeterProvider { 22 | fn drop(&mut self) { 23 | // this will only error if `.shutdown` gets called multiple times 24 | let _ = self.0.shutdown(); 25 | } 26 | } 27 | 28 | /// Initialize the OpenTelemetry push exporter using HTTP transport. 29 | /// 30 | /// # Interval and timeout 31 | /// This function uses the environment variables `OTEL_METRIC_EXPORT_TIMEOUT` and `OTEL_METRIC_EXPORT_INTERVAL` 32 | /// to configure the timeout and interval respectively. If you want to customize those 33 | /// from within code, consider using [`init_http_with_timeout_period`]. 34 | #[cfg(feature = "otel-push-exporter-http")] 35 | pub fn init_http(url: impl Into) -> Result { 36 | let (timeout, period) = timeout_and_period_from_env_or_default(); 37 | init_http_with_timeout_period(url, timeout, period) 38 | } 39 | 40 | /// Initialize the OpenTelemetry push exporter using HTTP transport with customized `timeout` and `period`. 41 | #[cfg(feature = "otel-push-exporter-http")] 42 | pub fn init_http_with_timeout_period( 43 | url: impl Into, 44 | timeout: Duration, 45 | period: Duration, 46 | ) -> Result { 47 | runtime() 48 | .with_exporter( 49 | opentelemetry_otlp::new_exporter() 50 | .http() 51 | .with_export_config(ExportConfig { 52 | endpoint: url.into(), 53 | protocol: Protocol::HttpBinary, 54 | timeout, 55 | ..Default::default() 56 | }), 57 | ) 58 | .with_period(period) 59 | .build() 60 | .map(OtelMeterProvider) 61 | } 62 | 63 | /// Initialize the OpenTelemetry push exporter using gRPC transport. 64 | /// 65 | /// # Interval and timeout 66 | /// This function uses the environment variables `OTEL_METRIC_EXPORT_TIMEOUT` and `OTEL_METRIC_EXPORT_INTERVAL` 67 | /// to configure the timeout and interval respectively. If you want to customize those 68 | /// from within code, consider using [`init_grpc_with_timeout_period`]. 69 | #[cfg(feature = "otel-push-exporter-grpc")] 70 | pub fn init_grpc(url: impl Into) -> Result { 71 | let (timeout, period) = timeout_and_period_from_env_or_default(); 72 | init_grpc_with_timeout_period(url, timeout, period) 73 | } 74 | 75 | /// Initialize the OpenTelemetry push exporter using gRPC transport with customized `timeout` and `period`. 76 | #[cfg(feature = "otel-push-exporter-grpc")] 77 | pub fn init_grpc_with_timeout_period( 78 | url: impl Into, 79 | timeout: Duration, 80 | period: Duration, 81 | ) -> Result { 82 | runtime() 83 | .with_exporter( 84 | opentelemetry_otlp::new_exporter() 85 | .tonic() 86 | .with_export_config(ExportConfig { 87 | endpoint: url.into(), 88 | protocol: Protocol::Grpc, 89 | timeout, 90 | ..Default::default() 91 | }), 92 | ) 93 | .with_period(period) 94 | .build() 95 | .map(OtelMeterProvider) 96 | } 97 | 98 | /// returns timeout and period from their respective environment variables 99 | /// or the default, if they are not set or set to an invalid value 100 | fn timeout_and_period_from_env_or_default() -> (Duration, Duration) { 101 | const OTEL_EXPORTER_TIMEOUT_ENV: &str = "OTEL_METRIC_EXPORT_TIMEOUT"; 102 | const OTEL_EXPORTER_INTERVAL_ENV: &str = "OTEL_METRIC_EXPORT_INTERVAL"; 103 | 104 | let timeout = Duration::from_secs( 105 | std::env::var_os(OTEL_EXPORTER_TIMEOUT_ENV) 106 | .and_then(|os_string| os_string.into_string().ok()) 107 | .and_then(|str| str.parse().ok()) 108 | .unwrap_or(OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT), 109 | ); 110 | 111 | let period = Duration::from_secs( 112 | std::env::var_os(OTEL_EXPORTER_INTERVAL_ENV) 113 | .and_then(|os_string| os_string.into_string().ok()) 114 | .and_then(|str| str.parse().ok()) 115 | .unwrap_or(60), 116 | ); 117 | 118 | (timeout, period) 119 | } 120 | 121 | #[cfg(all( 122 | feature = "otel-push-exporter-tokio", 123 | not(any( 124 | feature = "otel-push-exporter-tokio-current-thread", 125 | feature = "otel-push-exporter-async-std" 126 | )) 127 | ))] 128 | fn runtime( 129 | ) -> OtlpMetricPipeline { 130 | return opentelemetry_otlp::new_pipeline().metrics(opentelemetry_sdk::runtime::Tokio); 131 | } 132 | 133 | #[cfg(all( 134 | feature = "otel-push-exporter-tokio-current-thread", 135 | not(any( 136 | feature = "otel-push-exporter-tokio", 137 | feature = "otel-push-exporter-async-std" 138 | )) 139 | ))] 140 | fn runtime() -> OtlpMetricPipeline< 141 | opentelemetry_sdk::runtime::TokioCurrentThread, 142 | opentelemetry_otlp::NoExporterConfig, 143 | > { 144 | return opentelemetry_otlp::new_pipeline() 145 | .metrics(opentelemetry_sdk::runtime::TokioCurrentThread); 146 | } 147 | 148 | #[cfg(all( 149 | feature = "otel-push-exporter-async-std", 150 | not(any( 151 | feature = "otel-push-exporter-tokio", 152 | feature = "otel-push-exporter-tokio-current-thread" 153 | )) 154 | ))] 155 | fn runtime( 156 | ) -> OtlpMetricPipeline 157 | { 158 | return opentelemetry_otlp::new_pipeline().metrics(opentelemetry_sdk::runtime::AsyncStd); 159 | } 160 | 161 | #[cfg(not(any( 162 | feature = "otel-push-exporter-tokio", 163 | feature = "otel-push-exporter-tokio-current-thread", 164 | feature = "otel-push-exporter-async-std" 165 | )))] 166 | fn runtime() -> ! { 167 | compile_error!("select your runtime (`otel-push-exporter-tokio`, `otel-push-exporter-tokio-current-thread` or `otel-push-exporter-async-std`) for the autometrics push exporter or use the custom push exporter if none fit") 168 | } 169 | -------------------------------------------------------------------------------- /autometrics/src/prometheus_exporter.rs: -------------------------------------------------------------------------------- 1 | //! Helper functions for easily collecting and exporting metrics to Prometheus. 2 | //! 3 | //! This module is compatible with any of the metrics backends. It uses 4 | //! the `prometheus-client` by default if you do not specifically enable another backend. 5 | //! 6 | //! You do not need this module if you are already collecting custom metrics and exporting them to Prometheus. 7 | //! 8 | //! # Example 9 | //! ```rust 10 | //! use autometrics::prometheus_exporter::{self, PrometheusResponse}; 11 | //! 12 | //! /// Exports metrics to Prometheus. 13 | //! /// This should be mounted on `/metrics` on your API server 14 | //! pub async fn get_metrics() -> PrometheusResponse { 15 | //! prometheus_exporter::encode_http_response() 16 | //! } 17 | //! 18 | //! pub fn main() { 19 | //! prometheus_exporter::init(); 20 | //! } 21 | //! ``` 22 | 23 | #[cfg(debug_assertions)] 24 | use crate::__private::{AutometricsTracker, TrackMetrics, FUNCTION_DESCRIPTIONS}; 25 | use crate::settings::{get_settings, AutometricsSettings}; 26 | use http::{header::CONTENT_TYPE, Response}; 27 | #[cfg(metrics)] 28 | use metrics_exporter_prometheus::{BuildError, PrometheusBuilder, PrometheusHandle}; 29 | use once_cell::sync::OnceCell; 30 | #[cfg(opentelemetry)] 31 | use opentelemetry::metrics::MetricsError; 32 | #[cfg(opentelemetry)] 33 | use opentelemetry_sdk::metrics::SdkMeterProvider; 34 | #[cfg(any(opentelemetry, prometheus))] 35 | use prometheus::TextEncoder; 36 | use thiserror::Error; 37 | 38 | #[cfg(not(exemplars))] 39 | /// Prometheus text format content type 40 | const RESPONSE_CONTENT_TYPE: &str = "text/plain; version=0.0.4"; 41 | #[cfg(exemplars)] 42 | /// OpenMetrics content type 43 | const RESPONSE_CONTENT_TYPE: &str = "application/openmetrics-text; version=1.0.0; charset=utf-8"; 44 | 45 | static GLOBAL_EXPORTER: OnceCell = OnceCell::new(); 46 | 47 | pub type PrometheusResponse = Response; 48 | 49 | #[derive(Debug, Error)] 50 | pub enum EncodingError { 51 | #[cfg(any(prometheus, opentelemetry))] 52 | #[error(transparent)] 53 | Prometheus(#[from] prometheus::Error), 54 | 55 | #[cfg(prometheus_client)] 56 | #[error(transparent)] 57 | Format(#[from] std::fmt::Error), 58 | 59 | #[error(transparent)] 60 | Initialization(#[from] ExporterInitializationError), 61 | } 62 | 63 | #[derive(Debug, Error)] 64 | pub enum ExporterInitializationError { 65 | #[error("Prometheus exporter has already been initialized")] 66 | AlreadyInitialized, 67 | 68 | #[cfg(opentelemetry)] 69 | #[error(transparent)] 70 | OpenTelemetryExporter(#[from] MetricsError), 71 | 72 | #[cfg(metrics)] 73 | #[error(transparent)] 74 | MetricsExporter(#[from] BuildError), 75 | } 76 | 77 | /// Initialize the global Prometheus metrics collector and exporter. 78 | /// 79 | /// You will need a collector/exporter set up in order to use the metrics 80 | /// generated by autometrics. You can either use this one or configure 81 | /// your own using your metrics backend. 82 | /// 83 | /// In debug builds, this will also set the function call counters to zero. 84 | /// This exposes the names of instrumented functions to Prometheus without 85 | /// affecting the metric values. 86 | /// 87 | /// You should not call this function if you initialize the Autometrics 88 | /// settings via [`AutometricsSettingsBuilder::try_init`]. 89 | /// 90 | /// [`AutometricsSettingsBuilder::try_init`]: crate::settings::AutometricsSettingsBuilder::try_init 91 | pub fn try_init() -> Result<(), ExporterInitializationError> { 92 | // Initialize the global exporter but only if it hasn't already been initialized 93 | let mut newly_initialized = false; 94 | GLOBAL_EXPORTER.get_or_try_init(|| { 95 | newly_initialized = true; 96 | initialize_prometheus_exporter() 97 | })?; 98 | 99 | if !newly_initialized { 100 | return Err(ExporterInitializationError::AlreadyInitialized); 101 | } 102 | 103 | // Set all of the function counters to zero 104 | #[cfg(debug_assertions)] 105 | AutometricsTracker::intitialize_metrics(&FUNCTION_DESCRIPTIONS); 106 | 107 | Ok(()) 108 | } 109 | 110 | /// Initialize the global Prometheus metrics collector and exporter. 111 | /// 112 | /// You will need a collector/exporter set up in order to use the metrics 113 | /// generated by autometrics. You can either use this one or configure 114 | /// your own using your metrics backend. 115 | /// 116 | /// This should be included in your `main.rs`: 117 | /// ``` 118 | /// # fn main() { 119 | /// # #[cfg(feature="prometheus-exporter")] 120 | /// autometrics::prometheus_exporter::init(); 121 | /// # } 122 | /// ``` 123 | /// 124 | /// In debug builds, this will also set the function call counters to zero. 125 | /// This exposes the names of instrumented functions to Prometheus without 126 | /// affecting the metric values. 127 | /// 128 | /// You should not call this function if you initialize the Autometrics 129 | /// settings via [`AutometricsSettingsBuilder::init`]. 130 | /// 131 | /// [`AutometricsSettingsBuilder::init`]: crate::settings::AutometricsSettingsBuilder::init 132 | /// 133 | /// # Panics 134 | /// 135 | /// Panics if the exporter has already been initialized. 136 | pub fn init() { 137 | try_init().unwrap(); 138 | } 139 | 140 | /// Export the collected metrics to the Prometheus format. 141 | /// 142 | /// Create a handler on your API (often, this would be the 143 | /// handler for the route `/metrics`) that returns the result of this function. 144 | /// 145 | /// For example, using Axum, you might have a handler: 146 | /// ```rust 147 | /// # use http::StatusCode; 148 | /// // Mounted at the route `/metrics` 149 | /// pub async fn metrics_get() -> (StatusCode, String) { 150 | /// match autometrics::prometheus_exporter::encode_to_string() { 151 | /// Ok(metrics) => (StatusCode::OK, metrics), 152 | /// Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", err)) 153 | /// } 154 | /// } 155 | /// ``` 156 | pub fn encode_to_string() -> Result { 157 | GLOBAL_EXPORTER 158 | .get_or_try_init(initialize_prometheus_exporter)? 159 | .encode_metrics() 160 | } 161 | 162 | /// Export the collected metrics to the Prometheus or OpenMetrics format and wrap 163 | /// them in an HTTP response. 164 | /// 165 | /// If you are using exemplars, this will automatically use the OpenMetrics 166 | /// content type so that Prometheus can scrape the metrics and exemplars. 167 | pub fn encode_http_response() -> PrometheusResponse { 168 | match encode_to_string() { 169 | Ok(metrics) => http::Response::builder() 170 | .status(200) 171 | .header(CONTENT_TYPE, RESPONSE_CONTENT_TYPE) 172 | .body(metrics) 173 | .expect("Error building response"), 174 | Err(err) => http::Response::builder() 175 | .status(500) 176 | .body(format!("{:?}", err)) 177 | .expect("Error building response"), 178 | } 179 | } 180 | 181 | #[derive(Clone)] 182 | #[doc(hidden)] 183 | struct GlobalPrometheus { 184 | #[allow(dead_code)] 185 | settings: &'static AutometricsSettings, 186 | #[cfg(metrics)] 187 | metrics_exporter: PrometheusHandle, 188 | } 189 | 190 | impl GlobalPrometheus { 191 | fn encode_metrics(&self) -> Result { 192 | let mut output = String::new(); 193 | 194 | #[cfg(metrics)] 195 | output.push_str(&self.metrics_exporter.render()); 196 | 197 | #[cfg(any(prometheus, opentelemetry))] 198 | TextEncoder::new().encode_utf8(&self.settings.prometheus_registry.gather(), &mut output)?; 199 | 200 | #[cfg(prometheus_client)] 201 | prometheus_client::encoding::text::encode( 202 | &mut output, 203 | &self.settings.prometheus_client_registry, 204 | )?; 205 | 206 | Ok(output) 207 | } 208 | } 209 | 210 | fn initialize_prometheus_exporter() -> Result { 211 | let settings = get_settings(); 212 | 213 | #[cfg(opentelemetry)] 214 | { 215 | use opentelemetry::global; 216 | use opentelemetry_prometheus::exporter; 217 | use opentelemetry_sdk::metrics::reader::AggregationSelector; 218 | use opentelemetry_sdk::metrics::{Aggregation, InstrumentKind}; 219 | 220 | /// A custom aggregation selector that uses the configured histogram buckets, 221 | /// along with the other default aggregation settings. 222 | struct AggregationSelectorWithHistogramBuckets { 223 | histogram_buckets: Vec, 224 | } 225 | 226 | impl AggregationSelector for AggregationSelectorWithHistogramBuckets { 227 | fn aggregation(&self, kind: InstrumentKind) -> Aggregation { 228 | match kind { 229 | InstrumentKind::Counter 230 | | InstrumentKind::UpDownCounter 231 | | InstrumentKind::ObservableCounter 232 | | InstrumentKind::ObservableUpDownCounter => Aggregation::Sum, 233 | InstrumentKind::ObservableGauge | InstrumentKind::Gauge => { 234 | Aggregation::LastValue 235 | } 236 | InstrumentKind::Histogram => Aggregation::ExplicitBucketHistogram { 237 | boundaries: self.histogram_buckets.clone(), 238 | record_min_max: false, 239 | }, 240 | } 241 | } 242 | } 243 | 244 | let exporter = exporter() 245 | .with_registry(settings.prometheus_registry.clone()) 246 | .with_aggregation_selector(AggregationSelectorWithHistogramBuckets { 247 | histogram_buckets: settings.histogram_buckets.clone(), 248 | }) 249 | .without_scope_info() 250 | .without_target_info() 251 | .build()?; 252 | 253 | let meter_provider = SdkMeterProvider::builder().with_reader(exporter).build(); 254 | 255 | global::set_meter_provider(meter_provider); 256 | } 257 | 258 | Ok(GlobalPrometheus { 259 | #[cfg(metrics)] 260 | metrics_exporter: PrometheusBuilder::new() 261 | .set_buckets(&settings.histogram_buckets)? 262 | .install_recorder()?, 263 | settings, 264 | }) 265 | } 266 | -------------------------------------------------------------------------------- /autometrics/src/tracker/metrics.rs: -------------------------------------------------------------------------------- 1 | #[cfg(debug_assertions)] 2 | use crate::__private::FunctionDescription; 3 | use crate::constants::*; 4 | use crate::labels::{BuildInfoLabels, CounterLabels, GaugeLabels, HistogramLabels}; 5 | use crate::tracker::TrackMetrics; 6 | use metrics::{ 7 | describe_counter, describe_gauge, describe_histogram, register_counter, register_gauge, 8 | register_histogram, Gauge, Unit, 9 | }; 10 | use std::{sync::Once, time::Instant}; 11 | 12 | static DESCRIBE_METRICS: Once = Once::new(); 13 | static SET_BUILD_INFO: Once = Once::new(); 14 | 15 | fn describe_metrics() { 16 | DESCRIBE_METRICS.call_once(|| { 17 | describe_counter!(COUNTER_NAME_PROMETHEUS, COUNTER_DESCRIPTION); 18 | describe_histogram!( 19 | HISTOGRAM_NAME_PROMETHEUS, 20 | Unit::Seconds, 21 | HISTOGRAM_DESCRIPTION 22 | ); 23 | describe_gauge!(GAUGE_NAME_PROMETHEUS, GAUGE_DESCRIPTION); 24 | describe_gauge!(BUILD_INFO_NAME, BUILD_INFO_DESCRIPTION); 25 | }); 26 | } 27 | 28 | pub struct MetricsTracker { 29 | gauge: Option, 30 | start: Instant, 31 | } 32 | 33 | impl TrackMetrics for MetricsTracker { 34 | fn start(gauge_labels: Option<&GaugeLabels>) -> Self { 35 | describe_metrics(); 36 | 37 | let gauge = if let Some(gauge_labels) = gauge_labels { 38 | let gauge = register_gauge!(GAUGE_NAME, &gauge_labels.to_array()); 39 | gauge.increment(1.0); 40 | Some(gauge) 41 | } else { 42 | None 43 | }; 44 | 45 | Self { 46 | gauge, 47 | start: Instant::now(), 48 | } 49 | } 50 | 51 | fn finish(self, counter_labels: &CounterLabels, histogram_labels: &HistogramLabels) { 52 | let duration = self.start.elapsed().as_secs_f64(); 53 | register_counter!(COUNTER_NAME_PROMETHEUS, &counter_labels.to_vec()).increment(1); 54 | register_histogram!(HISTOGRAM_NAME_PROMETHEUS, &histogram_labels.to_vec()).record(duration); 55 | if let Some(gauge) = self.gauge { 56 | gauge.decrement(1.0); 57 | } 58 | } 59 | 60 | fn set_build_info(build_info_labels: &BuildInfoLabels) { 61 | SET_BUILD_INFO.call_once(|| { 62 | register_gauge!(BUILD_INFO_NAME, &build_info_labels.to_vec()).set(1.0); 63 | }); 64 | } 65 | 66 | #[cfg(debug_assertions)] 67 | fn intitialize_metrics(function_descriptions: &[FunctionDescription]) { 68 | for function in function_descriptions { 69 | let labels = &CounterLabels::from(function).to_vec(); 70 | register_counter!(COUNTER_NAME, labels).increment(0); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /autometrics/src/tracker/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(debug_assertions)] 2 | use crate::__private::FunctionDescription; 3 | use crate::labels::{BuildInfoLabels, CounterLabels, GaugeLabels, HistogramLabels}; 4 | 5 | #[cfg(metrics)] 6 | mod metrics; 7 | #[cfg(opentelemetry)] 8 | mod opentelemetry; 9 | #[cfg(prometheus)] 10 | mod prometheus; 11 | #[cfg(prometheus_client)] 12 | pub(crate) mod prometheus_client; 13 | 14 | #[cfg(metrics)] 15 | pub use self::metrics::MetricsTracker; 16 | #[cfg(opentelemetry)] 17 | pub use self::opentelemetry::OpenTelemetryTracker; 18 | #[cfg(prometheus)] 19 | pub use self::prometheus::PrometheusTracker; 20 | #[cfg(prometheus_client)] 21 | pub use self::prometheus_client::PrometheusClientTracker; 22 | 23 | #[cfg(all( 24 | not(doc), 25 | any( 26 | all(metrics, any(opentelemetry, prometheus, prometheus_client)), 27 | all(opentelemetry, any(prometheus, prometheus_client)), 28 | all(prometheus, prometheus_client) 29 | ) 30 | ))] 31 | compile_error!("Only one of the metrics, opentelemetry, prometheus, or prometheus-client features can be enabled at a time"); 32 | 33 | pub trait TrackMetrics { 34 | fn set_build_info(build_info_labels: &BuildInfoLabels); 35 | fn start(gauge_labels: Option<&GaugeLabels>) -> Self; 36 | fn finish(self, counter_labels: &CounterLabels, histogram_labels: &HistogramLabels); 37 | #[cfg(debug_assertions)] 38 | fn intitialize_metrics(function_descriptions: &[FunctionDescription]); 39 | } 40 | 41 | pub struct AutometricsTracker { 42 | #[cfg(metrics)] 43 | metrics_tracker: MetricsTracker, 44 | #[cfg(opentelemetry)] 45 | opentelemetry_tracker: OpenTelemetryTracker, 46 | #[cfg(prometheus)] 47 | prometheus_tracker: PrometheusTracker, 48 | #[cfg(prometheus_client)] 49 | prometheus_client_tracker: PrometheusClientTracker, 50 | } 51 | 52 | impl TrackMetrics for AutometricsTracker { 53 | #[allow(unused_variables)] 54 | fn set_build_info(build_info_labels: &BuildInfoLabels) { 55 | #[cfg(metrics)] 56 | MetricsTracker::set_build_info(build_info_labels); 57 | #[cfg(opentelemetry)] 58 | OpenTelemetryTracker::set_build_info(build_info_labels); 59 | #[cfg(prometheus)] 60 | PrometheusTracker::set_build_info(build_info_labels); 61 | #[cfg(prometheus_client)] 62 | PrometheusClientTracker::set_build_info(build_info_labels); 63 | } 64 | 65 | #[allow(unused_variables)] 66 | fn start(gauge_labels: Option<&GaugeLabels>) -> Self { 67 | Self { 68 | #[cfg(metrics)] 69 | metrics_tracker: MetricsTracker::start(gauge_labels), 70 | #[cfg(opentelemetry)] 71 | opentelemetry_tracker: OpenTelemetryTracker::start(gauge_labels), 72 | #[cfg(prometheus)] 73 | prometheus_tracker: PrometheusTracker::start(gauge_labels), 74 | #[cfg(prometheus_client)] 75 | prometheus_client_tracker: PrometheusClientTracker::start(gauge_labels), 76 | } 77 | } 78 | 79 | #[allow(unused_variables)] 80 | fn finish(self, counter_labels: &CounterLabels, histogram_labels: &HistogramLabels) { 81 | #[cfg(metrics)] 82 | self.metrics_tracker 83 | .finish(counter_labels, histogram_labels); 84 | #[cfg(opentelemetry)] 85 | self.opentelemetry_tracker 86 | .finish(counter_labels, histogram_labels); 87 | #[cfg(prometheus)] 88 | self.prometheus_tracker 89 | .finish(counter_labels, histogram_labels); 90 | #[cfg(prometheus_client)] 91 | self.prometheus_client_tracker 92 | .finish(counter_labels, histogram_labels); 93 | } 94 | 95 | #[cfg(debug_assertions)] 96 | #[allow(unused_variables)] 97 | fn intitialize_metrics(function_descriptions: &[FunctionDescription]) { 98 | #[cfg(metrics)] 99 | MetricsTracker::intitialize_metrics(function_descriptions); 100 | #[cfg(opentelemetry)] 101 | OpenTelemetryTracker::intitialize_metrics(function_descriptions); 102 | #[cfg(prometheus)] 103 | PrometheusTracker::intitialize_metrics(function_descriptions); 104 | #[cfg(prometheus_client)] 105 | PrometheusClientTracker::intitialize_metrics(function_descriptions); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /autometrics/src/tracker/opentelemetry.rs: -------------------------------------------------------------------------------- 1 | #[cfg(debug_assertions)] 2 | use crate::__private::FunctionDescription; 3 | use crate::labels::{BuildInfoLabels, CounterLabels, GaugeLabels, HistogramLabels, Label}; 4 | use crate::{constants::*, tracker::TrackMetrics}; 5 | use once_cell::sync::Lazy; 6 | use opentelemetry::metrics::{Counter, Histogram, UpDownCounter}; 7 | use opentelemetry::{global, KeyValue}; 8 | use std::{sync::Once, time::Instant}; 9 | 10 | static SET_BUILD_INFO: Once = Once::new(); 11 | const METER_NAME: &str = "autometrics"; 12 | static COUNTER: Lazy> = Lazy::new(|| { 13 | global::meter(METER_NAME) 14 | .u64_counter(COUNTER_NAME) 15 | .with_description(COUNTER_DESCRIPTION) 16 | .init() 17 | }); 18 | static HISTOGRAM: Lazy> = Lazy::new(|| { 19 | // Note that the unit needs to be written as "s" rather than "seconds" 20 | // or it will not be included in the metric name 21 | // https://github.com/open-telemetry/opentelemetry-rust/issues/1173 22 | global::meter(METER_NAME) 23 | .f64_histogram(HISTOGRAM_NAME) 24 | .with_unit("s") 25 | .with_description(HISTOGRAM_DESCRIPTION) 26 | .init() 27 | }); 28 | static GAUGE: Lazy> = Lazy::new(|| { 29 | global::meter(METER_NAME) 30 | .i64_up_down_counter(GAUGE_NAME) 31 | .with_description(GAUGE_DESCRIPTION) 32 | .init() 33 | }); 34 | 35 | /// Tracks the number of function calls, concurrent calls, and latency 36 | pub struct OpenTelemetryTracker { 37 | gauge_labels: Option>, 38 | start: Instant, 39 | } 40 | 41 | impl TrackMetrics for OpenTelemetryTracker { 42 | fn start(gauge_labels: Option<&GaugeLabels>) -> Self { 43 | let gauge_labels = if let Some(gauge_labels) = gauge_labels { 44 | let gauge_labels = to_key_values(gauge_labels.to_array()); 45 | // Increase the number of concurrent requests 46 | GAUGE.add(1, &gauge_labels); 47 | Some(gauge_labels) 48 | } else { 49 | None 50 | }; 51 | 52 | Self { 53 | gauge_labels, 54 | start: Instant::now(), 55 | } 56 | } 57 | 58 | fn finish<'a>(self, counter_labels: &CounterLabels, histogram_labels: &HistogramLabels) { 59 | let duration = self.start.elapsed().as_secs_f64(); 60 | 61 | // Track the function calls 62 | let counter_labels = to_key_values(counter_labels.to_vec()); 63 | COUNTER.add(1, &counter_labels); 64 | 65 | // Track the latency 66 | let histogram_labels = to_key_values(histogram_labels.to_vec()); 67 | HISTOGRAM.record(duration, &histogram_labels); 68 | 69 | // Decrease the number of concurrent requests 70 | if let Some(gauge_labels) = self.gauge_labels { 71 | GAUGE.add(-1, &gauge_labels); 72 | } 73 | } 74 | 75 | fn set_build_info(build_info_labels: &BuildInfoLabels) { 76 | SET_BUILD_INFO.call_once(|| { 77 | let build_info_labels = to_key_values(build_info_labels.to_vec()); 78 | let build_info = global::meter(METER_NAME) 79 | .f64_up_down_counter(BUILD_INFO_NAME) 80 | .with_description(BUILD_INFO_DESCRIPTION) 81 | .init(); 82 | build_info.add(1.0, &build_info_labels); 83 | }); 84 | } 85 | 86 | #[cfg(debug_assertions)] 87 | fn intitialize_metrics(function_descriptions: &[FunctionDescription]) { 88 | for function in function_descriptions { 89 | let labels = &to_key_values(CounterLabels::from(function).to_vec()); 90 | COUNTER.add(0, labels); 91 | } 92 | } 93 | } 94 | 95 | fn to_key_values(labels: impl IntoIterator) -> Vec { 96 | labels 97 | .into_iter() 98 | .map(|(k, v)| KeyValue::new(k, v)) 99 | .collect() 100 | } 101 | -------------------------------------------------------------------------------- /autometrics/src/tracker/prometheus.rs: -------------------------------------------------------------------------------- 1 | #[cfg(debug_assertions)] 2 | use crate::__private::FunctionDescription; 3 | use crate::labels::{BuildInfoLabels, CounterLabels, GaugeLabels, HistogramLabels, ResultLabel}; 4 | use crate::{constants::*, settings::get_settings, tracker::TrackMetrics}; 5 | use once_cell::sync::Lazy; 6 | use prometheus::core::{AtomicI64, GenericGauge}; 7 | use prometheus::{ 8 | histogram_opts, register_histogram_vec_with_registry, register_int_counter_vec_with_registry, 9 | register_int_gauge_vec_with_registry, HistogramVec, IntCounterVec, IntGaugeVec, 10 | }; 11 | use std::{sync::Once, time::Instant}; 12 | 13 | static SET_BUILD_INFO: Once = Once::new(); 14 | 15 | static COUNTER: Lazy = Lazy::new(|| { 16 | register_int_counter_vec_with_registry!( 17 | COUNTER_NAME_PROMETHEUS, 18 | COUNTER_DESCRIPTION, 19 | &[ 20 | FUNCTION_KEY, 21 | MODULE_KEY, 22 | SERVICE_NAME_KEY_PROMETHEUS, 23 | CALLER_FUNCTION_PROMETHEUS, 24 | CALLER_MODULE_PROMETHEUS, 25 | RESULT_KEY, 26 | OK_KEY, 27 | ERROR_KEY, 28 | OBJECTIVE_NAME_PROMETHEUS, 29 | OBJECTIVE_PERCENTILE_PROMETHEUS, 30 | ], 31 | get_settings().prometheus_registry.clone() 32 | ) 33 | .expect("Failed to register function_calls_count_total counter") 34 | }); 35 | static HISTOGRAM: Lazy = Lazy::new(|| { 36 | let opts = histogram_opts!( 37 | HISTOGRAM_NAME_PROMETHEUS, 38 | HISTOGRAM_DESCRIPTION, 39 | // The Prometheus crate uses different histogram buckets by default 40 | // (and these are configured when creating a histogram rather than 41 | // when configuring the registry or exporter, like in the other crates) 42 | // so we need to pass these in here 43 | get_settings().histogram_buckets.clone() 44 | ); 45 | register_histogram_vec_with_registry!( 46 | opts, 47 | &[ 48 | FUNCTION_KEY, 49 | MODULE_KEY, 50 | SERVICE_NAME_KEY_PROMETHEUS, 51 | OBJECTIVE_NAME_PROMETHEUS, 52 | OBJECTIVE_PERCENTILE_PROMETHEUS, 53 | OBJECTIVE_LATENCY_THRESHOLD_PROMETHEUS 54 | ], 55 | get_settings().prometheus_registry.clone() 56 | ) 57 | .expect("Failed to register function_calls_duration histogram") 58 | }); 59 | static GAUGE: Lazy = Lazy::new(|| { 60 | register_int_gauge_vec_with_registry!( 61 | GAUGE_NAME_PROMETHEUS, 62 | GAUGE_DESCRIPTION, 63 | &[FUNCTION_KEY, MODULE_KEY, SERVICE_NAME_KEY_PROMETHEUS], 64 | get_settings().prometheus_registry.clone() 65 | ) 66 | .expect("Failed to register function_calls_concurrent gauge") 67 | }); 68 | static BUILD_INFO: Lazy = Lazy::new(|| { 69 | register_int_gauge_vec_with_registry!( 70 | BUILD_INFO_NAME, 71 | BUILD_INFO_DESCRIPTION, 72 | &[ 73 | COMMIT_KEY, 74 | VERSION_KEY, 75 | BRANCH_KEY, 76 | SERVICE_NAME_KEY_PROMETHEUS, 77 | REPO_URL_KEY_PROMETHEUS, 78 | REPO_PROVIDER_KEY_PROMETHEUS, 79 | AUTOMETRICS_VERSION_KEY_PROMETHEUS, 80 | ], 81 | get_settings().prometheus_registry.clone() 82 | ) 83 | .expect("Failed to register build_info counter") 84 | }); 85 | 86 | pub struct PrometheusTracker { 87 | start: Instant, 88 | gauge: Option>, 89 | } 90 | 91 | impl TrackMetrics for PrometheusTracker { 92 | fn start(gauge_labels: Option<&GaugeLabels>) -> Self { 93 | let gauge = if let Some(gauge_labels) = gauge_labels { 94 | let gauge = GAUGE.with_label_values(&[ 95 | gauge_labels.function, 96 | gauge_labels.module, 97 | gauge_labels.service_name, 98 | ]); 99 | gauge.inc(); 100 | Some(gauge) 101 | } else { 102 | None 103 | }; 104 | 105 | Self { 106 | start: Instant::now(), 107 | gauge, 108 | } 109 | } 110 | 111 | fn finish(self, counter_labels: &CounterLabels, histogram_labels: &HistogramLabels) { 112 | let duration = self.start.elapsed().as_secs_f64(); 113 | 114 | let counter_labels = counter_labels_to_prometheus_vec(counter_labels); 115 | COUNTER.with_label_values(&counter_labels).inc(); 116 | 117 | HISTOGRAM 118 | .with_label_values(&[ 119 | histogram_labels.function, 120 | histogram_labels.module, 121 | histogram_labels.service_name, 122 | histogram_labels.objective_name.unwrap_or_default(), 123 | histogram_labels 124 | .objective_percentile 125 | .as_ref() 126 | .map(|p| p.as_str()) 127 | .unwrap_or_default(), 128 | histogram_labels 129 | .objective_latency_threshold 130 | .as_ref() 131 | .map(|p| p.as_str()) 132 | .unwrap_or_default(), 133 | ]) 134 | .observe(duration); 135 | 136 | if let Some(gauge) = self.gauge { 137 | gauge.dec(); 138 | } 139 | } 140 | 141 | fn set_build_info(build_info_labels: &BuildInfoLabels) { 142 | SET_BUILD_INFO.call_once(|| { 143 | BUILD_INFO 144 | .with_label_values(&[ 145 | build_info_labels.commit, 146 | build_info_labels.version, 147 | build_info_labels.branch, 148 | build_info_labels.service_name, 149 | build_info_labels.repo_url, 150 | build_info_labels.repo_provider, 151 | build_info_labels.autometrics_version, 152 | ]) 153 | .set(1); 154 | }); 155 | } 156 | 157 | #[cfg(debug_assertions)] 158 | fn intitialize_metrics(function_descriptions: &[FunctionDescription]) { 159 | for function in function_descriptions { 160 | let labels = counter_labels_to_prometheus_vec(&CounterLabels::from(function)); 161 | COUNTER.with_label_values(&labels).inc_by(0); 162 | } 163 | } 164 | } 165 | 166 | /// Put the label values in the same order as the keys in the counter definition 167 | fn counter_labels_to_prometheus_vec(counter_labels: &CounterLabels) -> [&'static str; 10] { 168 | [ 169 | counter_labels.function, 170 | counter_labels.module, 171 | counter_labels.service_name, 172 | counter_labels.caller_function, 173 | counter_labels.caller_module, 174 | match counter_labels.result { 175 | Some(ResultLabel::Ok) => OK_KEY, 176 | Some(ResultLabel::Error) => ERROR_KEY, 177 | None => "", 178 | }, 179 | counter_labels.ok.unwrap_or_default(), 180 | counter_labels.error.unwrap_or_default(), 181 | counter_labels.objective_name.unwrap_or_default(), 182 | counter_labels 183 | .objective_percentile 184 | .as_ref() 185 | .map(|p| p.as_str()) 186 | .unwrap_or_default(), 187 | ] 188 | } 189 | -------------------------------------------------------------------------------- /autometrics/src/tracker/prometheus_client.rs: -------------------------------------------------------------------------------- 1 | use super::TrackMetrics; 2 | #[cfg(debug_assertions)] 3 | use crate::__private::FunctionDescription; 4 | #[cfg(exemplars)] 5 | use crate::exemplars::get_exemplar; 6 | use crate::labels::{BuildInfoLabels, CounterLabels, GaugeLabels, HistogramLabels}; 7 | use crate::{constants::*, settings::get_settings}; 8 | use once_cell::sync::Lazy; 9 | use prometheus_client::metrics::{family::Family, gauge::Gauge}; 10 | use prometheus_client::registry::{Registry, Unit}; 11 | use std::time::Instant; 12 | 13 | #[cfg(exemplars)] 14 | type CounterType = 15 | prometheus_client::metrics::exemplar::CounterWithExemplar>; 16 | #[cfg(not(exemplars))] 17 | type CounterType = prometheus_client::metrics::counter::Counter; 18 | 19 | #[cfg(exemplars)] 20 | type HistogramType = 21 | prometheus_client::metrics::exemplar::HistogramWithExemplars>; 22 | #[cfg(not(exemplars))] 23 | type HistogramType = prometheus_client::metrics::histogram::Histogram; 24 | 25 | static METRICS: Lazy<&Metrics> = Lazy::new(|| &get_settings().prometheus_client_metrics); 26 | 27 | pub(crate) fn initialize_registry(mut registry: Registry) -> (Registry, Metrics) { 28 | let counter = Family::::default(); 29 | registry.register( 30 | // Remove the _total suffix from the counter name 31 | // because the library adds it automatically 32 | COUNTER_NAME_PROMETHEUS.replace("_total", ""), 33 | COUNTER_DESCRIPTION, 34 | counter.clone(), 35 | ); 36 | 37 | let histogram = Family::::new_with_constructor(|| { 38 | HistogramType::new(get_settings().histogram_buckets.iter().copied()) 39 | }); 40 | registry.register_with_unit( 41 | // This also adds the _seconds suffix to the histogram name automatically 42 | HISTOGRAM_NAME_PROMETHEUS.replace("_seconds", ""), 43 | HISTOGRAM_DESCRIPTION, 44 | Unit::Seconds, 45 | histogram.clone(), 46 | ); 47 | 48 | let gauge = Family::::default(); 49 | registry.register(GAUGE_NAME_PROMETHEUS, GAUGE_DESCRIPTION, gauge.clone()); 50 | 51 | let build_info = Family::::default(); 52 | registry.register(BUILD_INFO_NAME, BUILD_INFO_DESCRIPTION, build_info.clone()); 53 | 54 | ( 55 | registry, 56 | Metrics { 57 | counter, 58 | histogram, 59 | gauge, 60 | build_info, 61 | }, 62 | ) 63 | } 64 | 65 | pub(crate) struct Metrics { 66 | counter: Family, 67 | histogram: Family, 68 | gauge: Family, 69 | build_info: Family, 70 | } 71 | 72 | pub struct PrometheusClientTracker { 73 | gauge_labels: Option, 74 | start_time: Instant, 75 | } 76 | 77 | impl TrackMetrics for PrometheusClientTracker { 78 | fn set_build_info(build_info_labels: &BuildInfoLabels) { 79 | METRICS.build_info.get_or_create(build_info_labels).set(1); 80 | } 81 | 82 | fn start(gauge_labels: Option<&GaugeLabels>) -> Self { 83 | if let Some(gauge_labels) = gauge_labels { 84 | METRICS.gauge.get_or_create(gauge_labels).inc(); 85 | } 86 | Self { 87 | gauge_labels: gauge_labels.cloned(), 88 | start_time: Instant::now(), 89 | } 90 | } 91 | 92 | fn finish(self, counter_labels: &CounterLabels, histogram_labels: &HistogramLabels) { 93 | #[cfg(exemplars)] 94 | let exemplar = get_exemplar().map(|exemplar| exemplar.into_iter().collect::>()); 95 | 96 | METRICS.counter.get_or_create(counter_labels).inc_by( 97 | 1, 98 | #[cfg(exemplars)] 99 | exemplar.clone(), 100 | ); 101 | 102 | METRICS.histogram.get_or_create(histogram_labels).observe( 103 | self.start_time.elapsed().as_secs_f64(), 104 | #[cfg(exemplars)] 105 | exemplar, 106 | ); 107 | 108 | if let Some(gauge_labels) = &self.gauge_labels { 109 | METRICS.gauge.get_or_create(gauge_labels).dec(); 110 | } 111 | } 112 | 113 | #[cfg(debug_assertions)] 114 | fn intitialize_metrics(function_descriptions: &[FunctionDescription]) { 115 | for function in function_descriptions { 116 | METRICS 117 | .counter 118 | .get_or_create(&CounterLabels::from(function)) 119 | .inc_by( 120 | 0, 121 | #[cfg(exemplars)] 122 | None, 123 | ); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /autometrics/tests/compilation.rs: -------------------------------------------------------------------------------- 1 | //! Tests relying on macros or compiler diagnostics 2 | 3 | #[test] 4 | fn harness() { 5 | let t = trybuild::TestCases::new(); 6 | 7 | // Test the ResultLabels macro 8 | t.pass("tests/compilation/result_labels/pass/*.rs"); 9 | t.compile_fail("tests/compilation/result_labels/fail/*.rs"); 10 | 11 | // Test that compiler reports errors in the correct location 12 | t.compile_fail("tests/compilation/error_locus/fail/*.rs"); 13 | } 14 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/error_locus/fail/report_original_line.rs: -------------------------------------------------------------------------------- 1 | // This test ensures that when an instrumented function has a compilation error, 2 | // then the error is reported at the correct line in the original code. 3 | use autometrics::autometrics; 4 | 5 | #[autometrics] 6 | fn bad_function() { 7 | // This vec is not mut 8 | let contents: Vec = Vec::new(); 9 | 10 | contents.push(2); 11 | } 12 | 13 | fn main() { 14 | bad_function(); 15 | } 16 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/error_locus/fail/report_original_line.stderr: -------------------------------------------------------------------------------- 1 | error[E0596]: cannot borrow `contents` as mutable, as it is not declared as mutable 2 | --> tests/compilation/error_locus/fail/report_original_line.rs:10:5 3 | | 4 | 10 | contents.push(2); 5 | | ^^^^^^^^ cannot borrow as mutable 6 | | 7 | help: consider changing this to be mutable 8 | | 9 | 8 | let mut contents: Vec = Vec::new(); 10 | | +++ 11 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/fail/wrong_attribute.rs: -------------------------------------------------------------------------------- 1 | // This test ensures that the macro fails with a readable 2 | // error when the attribute given to one variant inside the 3 | // enumeration is not in the correct form. 4 | use autometrics_macros::ResultLabels; 5 | 6 | struct Inner {} 7 | 8 | #[derive(ResultLabels)] 9 | enum MyError { 10 | Empty, 11 | #[label] 12 | ClientError { 13 | inner: Inner, 14 | }, 15 | ServerError(u64), 16 | } 17 | 18 | fn main() {} 19 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/fail/wrong_attribute.stderr: -------------------------------------------------------------------------------- 1 | error: Only `label(result = "RES")` (RES can be "ok" or "error") is supported 2 | --> tests/compilation/result_labels/fail/wrong_attribute.rs:11:7 3 | | 4 | 11 | #[label] 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/fail/wrong_kv_attribute.rs: -------------------------------------------------------------------------------- 1 | // This test ensures that the macro fails with a readable 2 | // error when the attribute given to one variant inside the 3 | // enumeration is not in the correct form. 4 | use autometrics_macros::ResultLabels; 5 | 6 | struct Inner {} 7 | 8 | #[derive(ResultLabels)] 9 | enum MyError { 10 | Empty, 11 | #[label = "error"] 12 | ClientError { 13 | inner: Inner, 14 | }, 15 | ServerError(u64), 16 | } 17 | 18 | fn main() {} 19 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/fail/wrong_kv_attribute.stderr: -------------------------------------------------------------------------------- 1 | error: Only `label(result = "RES")` (RES can be "ok" or "error") is supported 2 | --> tests/compilation/result_labels/fail/wrong_kv_attribute.rs:11:7 3 | | 4 | 11 | #[label = "error"] 5 | | ^^^^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/fail/wrong_result_name.rs: -------------------------------------------------------------------------------- 1 | // This test ensures that the macro fails with a readable 2 | // error when the attribute given to one variant inside the 3 | // enumeration does not use the correct key for the label. 4 | use autometrics_macros::ResultLabels; 5 | 6 | struct Inner {} 7 | 8 | #[derive(ResultLabels)] 9 | enum MyError { 10 | Empty, 11 | #[label(unknown = "ok")] 12 | ClientError { 13 | inner: Inner, 14 | }, 15 | ServerError(u64), 16 | } 17 | 18 | fn main() {} 19 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/fail/wrong_result_name.stderr: -------------------------------------------------------------------------------- 1 | error: Only `result = "RES"` (RES can be "ok" or "error") is supported 2 | --> tests/compilation/result_labels/fail/wrong_result_name.rs:11:13 3 | | 4 | 11 | #[label(unknown = "ok")] 5 | | ^^^^^^^ 6 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/fail/wrong_variant.rs: -------------------------------------------------------------------------------- 1 | // This test ensures that the macro fails with a readable error when the 2 | // attribute given to one variant inside the enumeration does not use one of the 3 | // predetermined values (that would make the automatic queries fail, so the 4 | // macros need to forbid wrong usage at compile time) 5 | use autometrics_macros::ResultLabels; 6 | 7 | struct Inner {} 8 | 9 | #[derive(ResultLabels)] 10 | enum MyError { 11 | Empty, 12 | #[label(result = "not ok")] 13 | ClientError { 14 | inner: Inner, 15 | }, 16 | ServerError(u64), 17 | } 18 | 19 | fn main() {} 20 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/fail/wrong_variant.stderr: -------------------------------------------------------------------------------- 1 | error: Only "ok" or "error" are accepted as result values 2 | --> tests/compilation/result_labels/fail/wrong_variant.rs:12:22 3 | | 4 | 12 | #[label(result = "not ok")] 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/pass/async_trait_support.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use autometrics::autometrics; 3 | 4 | // https://github.com/autometrics-dev/autometrics-rs/issues/141 5 | 6 | #[async_trait] 7 | trait TestTrait { 8 | async fn method() -> bool; 9 | async fn self_method(&self) -> bool; 10 | } 11 | 12 | #[derive(Default)] 13 | struct TestStruct; 14 | 15 | #[autometrics] 16 | #[async_trait] 17 | impl TestTrait for TestStruct { 18 | async fn method() -> bool { 19 | true 20 | } 21 | 22 | async fn self_method(&self) -> bool { 23 | true 24 | } 25 | } 26 | 27 | fn main() { 28 | let ts = TestStruct::default(); 29 | 30 | let _ = async move { 31 | ::method().await; 32 | ts.self_method().await; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/pass/generics.rs: -------------------------------------------------------------------------------- 1 | use autometrics::autometrics; 2 | use std::io; 3 | 4 | // general purpose `Result`, part of the std prelude. 5 | // notice both `Ok` and `Err` generic type arguments are explicitly provided 6 | #[autometrics] 7 | fn issue_121_a() -> Result { 8 | Ok("a") 9 | } 10 | 11 | // specialized `Result` which is part of std but not part of the std prelude. 12 | // notice there is only an explicit `Ok` type in the generic args, the `Err` generic argument 13 | // is type-defined 14 | #[autometrics] 15 | fn issue_121_b() -> io::Result { 16 | Ok("b") 17 | } 18 | 19 | // specialized `Result` which is part of a foreign crate 20 | // notice there is only an explicit `Ok` type in the generic args, the `Err` generic argument 21 | // is type-defined in the foreign crate 22 | #[autometrics] 23 | fn issue_121_c() -> ::http::Result { 24 | // CODE STYLE: please keep return formatted this way (with the leading `::`) 25 | Ok("c") 26 | } 27 | 28 | // Result where both `Ok` and `Error` are `impl` types 29 | #[autometrics] 30 | fn issue_121_d() -> Result { 31 | if true { 32 | Ok("d") 33 | } else { 34 | Err(io::Error::new(io::ErrorKind::Other, "issue 121d")) 35 | } 36 | } 37 | 38 | fn main() { 39 | // we need to handle all four code generation cases 40 | issue_121_a().unwrap(); 41 | issue_121_b().unwrap(); 42 | issue_121_c().unwrap(); 43 | issue_121_d().unwrap(); 44 | } 45 | -------------------------------------------------------------------------------- /autometrics/tests/compilation/result_labels/pass/macro.rs: -------------------------------------------------------------------------------- 1 | //! This test uses interfaces not meant to be directly used. 2 | //! 3 | //! The goal here is to make sure that the macro has the effect we want. 4 | //! autometrics (the library) is then responsible for orchestrating the 5 | //! calls to `get_result_labels_for_value!` correctly when observing 6 | //! function call results for the metrics. 7 | use autometrics::get_result_labels_for_value; 8 | use autometrics_macros::ResultLabels; 9 | 10 | #[derive(Clone)] 11 | struct Inner {} 12 | 13 | #[derive(ResultLabels, Clone)] 14 | enum MyEnum { 15 | /// When manually marked as 'error', returning this variant will 16 | /// _ALWAYS_ be considered as an error for Autometrics. 17 | /// Notably, even if you return Ok(MyEnum::Empty) from a function. 18 | #[label(result = "error")] 19 | Empty, 20 | /// When manually marked as 'ok', returning this variant will 21 | /// _ALWAYS_ be considered as a succesful result for Autometrics. 22 | /// Notably, even if you return Err(MyEnum::Empty) from a function. 23 | #[label(result = "ok")] 24 | ClientError { inner: Inner }, 25 | /// Without any manual override, Autometrics will guess from the 26 | /// context when possible to know whether something is an issue or 27 | /// not. This means: 28 | /// - Ok(MyEnum::AmbiguousValue(_)) is a success for Autometrics 29 | /// - Err(MyEnum::AmbiguousValue(_)) is an error for Autometrics 30 | /// - Just returning MyEnum::AmbiguousValue(_) won't do anything (just like returning 31 | /// a bare primitive type like usize) 32 | AmbiguousValue(u64), 33 | } 34 | 35 | fn main() { 36 | let is_ok = MyEnum::ClientError { inner: Inner {} }; 37 | let labels = get_result_labels_for_value!(&is_ok); 38 | assert_eq!(labels.unwrap().0, "ok"); 39 | 40 | let err = MyEnum::Empty; 41 | let labels = get_result_labels_for_value!(&err); 42 | assert_eq!(labels.unwrap().0, "error"); 43 | 44 | let no_idea = MyEnum::AmbiguousValue(42); 45 | let labels = get_result_labels_for_value!(&no_idea); 46 | assert_eq!(labels, None); 47 | 48 | // Testing behaviour within an Ok() error variant 49 | let ok: Result = Ok(is_ok.clone()); 50 | let labels = get_result_labels_for_value!(&ok); 51 | assert_eq!( 52 | labels.unwrap().0, 53 | "ok", 54 | "When wrapped as the Ok variant of a result, a manually marked 'ok' variant translates to 'ok'." 55 | ); 56 | 57 | let ok: Result = Ok(no_idea.clone()); 58 | let labels = get_result_labels_for_value!(&ok); 59 | assert_eq!( 60 | labels.unwrap().0, 61 | "ok", 62 | "When wrapped as the Ok variant of a result, an ambiguous variant translates to 'ok'." 63 | ); 64 | 65 | let err_in_ok: Result = Ok(err.clone()); 66 | let labels = get_result_labels_for_value!(&err_in_ok); 67 | assert_eq!( 68 | labels.unwrap().0, 69 | "error", 70 | "When wrapped as the Ok variant of a result, a manually marked 'error' variant translates to 'error'." 71 | ); 72 | 73 | // Testing behaviour within an Err() error variant 74 | let ok_in_err: Result<(), MyEnum> = Err(is_ok); 75 | let labels = get_result_labels_for_value!(&ok_in_err); 76 | assert_eq!( 77 | labels.unwrap().0, 78 | "ok", 79 | "When wrapped as the Err variant of a result, a manually marked 'ok' variant translates to 'ok'." 80 | ); 81 | 82 | let not_ok: Result<(), MyEnum> = Err(err); 83 | let labels = get_result_labels_for_value!(¬_ok); 84 | assert_eq!( 85 | labels.unwrap().0, 86 | "error", 87 | "When wrapped as the Err variant of a result, a manually marked 'error' variant translates to 'error'." 88 | ); 89 | 90 | let ambiguous: Result<(), MyEnum> = Err(no_idea); 91 | let labels = get_result_labels_for_value!(&ambiguous); 92 | assert_eq!( 93 | labels.unwrap().0, 94 | "error", 95 | "When wrapped as the Err variant of a result, an ambiguous variant translates to 'error'." 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /autometrics/tests/exemplars_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(prometheus_exporter, exemplars))] 2 | 3 | use autometrics::{autometrics, prometheus_exporter}; 4 | 5 | #[cfg(exemplars_tracing)] 6 | #[test] 7 | fn single_field() { 8 | use tracing_subscriber::prelude::*; 9 | prometheus_exporter::try_init().ok(); 10 | 11 | #[autometrics] 12 | #[tracing::instrument(fields(trace_id = "test_trace_id"))] 13 | fn single_field_fn() {} 14 | 15 | let subscriber = tracing_subscriber::fmt::fmt().finish().with( 16 | autometrics::exemplars::tracing::AutometricsExemplarExtractor::from_fields(&["trace_id"]), 17 | ); 18 | tracing::subscriber::with_default(subscriber, || single_field_fn()); 19 | 20 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 21 | assert!(metrics.lines().any(|line| { 22 | line.starts_with("function_calls_total{") 23 | && line.contains(r#"function="single_field_fn""#) 24 | && line.ends_with(r#"} 1 # {trace_id="test_trace_id"} 1.0"#) 25 | })) 26 | } 27 | 28 | #[cfg(exemplars_tracing)] 29 | #[test] 30 | fn multiple_fields() { 31 | use tracing_subscriber::prelude::*; 32 | prometheus_exporter::try_init().ok(); 33 | 34 | #[autometrics] 35 | #[tracing::instrument(fields(trace_id = "test_trace_id", foo = 99))] 36 | fn multiple_fields_fn() {} 37 | 38 | let subscriber = tracing_subscriber::fmt::fmt().finish().with( 39 | autometrics::exemplars::tracing::AutometricsExemplarExtractor::from_fields(&[ 40 | "trace_id", "foo", 41 | ]), 42 | ); 43 | tracing::subscriber::with_default(subscriber, || multiple_fields_fn()); 44 | 45 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 46 | println!("{}", metrics); 47 | assert!(metrics.lines().any(|line| { 48 | line.starts_with("function_calls_total{") 49 | && line.contains(r#"function="multiple_fields_fn""#) 50 | && (line.ends_with(r#"} 1 # {trace_id="test_trace_id",foo="99"} 1.0"#) 51 | || line.ends_with(r#"} 1 # {foo="99",trace_id="test_trace_id"} 1.0"#)) 52 | })) 53 | } 54 | 55 | #[cfg(exemplars_tracing_opentelemetry)] 56 | #[test] 57 | fn tracing_opentelemetry_context() { 58 | use opentelemetry::trace::TracerProvider as _; 59 | use opentelemetry_sdk::trace::TracerProvider; 60 | use opentelemetry_stdout::SpanExporter; 61 | use std::io; 62 | use tracing_subscriber::{layer::SubscriberExt, Registry}; 63 | 64 | prometheus_exporter::try_init().ok(); 65 | 66 | let exporter = SpanExporter::builder().with_writer(io::sink()).build(); 67 | let provider = TracerProvider::builder() 68 | .with_simple_exporter(exporter) 69 | .build(); 70 | let tracer = provider.tracer("test"); 71 | 72 | // This adds the OpenTelemetry Context to every tracing Span 73 | #[cfg(feature = "exemplars-tracing-opentelemetry-0_25")] 74 | let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer); 75 | 76 | let subscriber = Registry::default().with(otel_layer); 77 | 78 | #[autometrics] 79 | #[tracing::instrument] 80 | fn opentelemetry_context_fn() {} 81 | 82 | tracing::subscriber::with_default(subscriber, opentelemetry_context_fn); 83 | 84 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 85 | assert!(metrics.lines().any(|line| { 86 | line.starts_with("function_calls_total{") 87 | && line.contains(r#"function="opentelemetry_context_fn""#) 88 | && (line.contains(r#"trace_id=""#) || line.contains(r#"span_id=""#)) 89 | })) 90 | } 91 | -------------------------------------------------------------------------------- /autometrics/tests/init_to_zero_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "prometheus-exporter")] 2 | 3 | use autometrics::{autometrics, prometheus_exporter}; 4 | 5 | #[cfg(debug_assertions)] 6 | #[test] 7 | fn zero_metrics() { 8 | // This test is in its own file because there is a race condition when multiple tests 9 | // are concurrently calling prometheus_exporter::try_init. One of the tests will 10 | // initialize the exporter and set the global OnceCell while the others are blocked. 11 | // The thread that initialized the exporter will then set the metrics to zero. However, 12 | // this test may already try to read the metrics before they are set to zero. 13 | prometheus_exporter::init(); 14 | 15 | #[autometrics] 16 | fn zero_metrics_fn() {} 17 | 18 | // Note that we are not calling the function, but it should still be registered 19 | 20 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 21 | println!("{}", metrics); 22 | assert!(metrics 23 | .lines() 24 | .any(|line| line.contains(r#"function="zero_metrics_fn""#) && line.ends_with("} 0"))); 25 | } 26 | -------------------------------------------------------------------------------- /autometrics/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(prometheus_exporter)] 2 | use autometrics::{autometrics, prometheus_exporter}; 3 | 4 | #[test] 5 | fn single_function() { 6 | prometheus_exporter::try_init().ok(); 7 | 8 | #[autometrics] 9 | fn hello_world() -> &'static str { 10 | "Hello world!" 11 | } 12 | 13 | hello_world(); 14 | hello_world(); 15 | 16 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 17 | assert!(metrics.lines().any(|line| { 18 | (line.starts_with("function_calls_total{")) 19 | && line.contains(r#"function="hello_world""#) 20 | && line.contains(r#"module="integration_test""#) 21 | && line.contains(r#"service_name="autometrics""#) 22 | && line.ends_with("} 2") 23 | })); 24 | assert!(metrics.lines().any(|line| line 25 | .starts_with("function_calls_duration_seconds_bucket{") 26 | && line.contains(r#"function="hello_world""#) 27 | && line.contains(r#"module="integration_test""#) 28 | && line.contains(r#"service_name="autometrics""#) 29 | && line.ends_with("} 2"))); 30 | } 31 | 32 | #[test] 33 | fn impl_block() { 34 | prometheus_exporter::try_init().ok(); 35 | 36 | struct Foo; 37 | 38 | #[autometrics] 39 | impl Foo { 40 | fn test_fn() -> &'static str { 41 | "Hello world!" 42 | } 43 | 44 | fn test_method(&self) -> &'static str { 45 | "Goodnight moon" 46 | } 47 | } 48 | 49 | Foo::test_fn(); 50 | Foo.test_method(); 51 | 52 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 53 | assert!(metrics.lines().any(|line| { 54 | line.starts_with("function_calls_total{") 55 | && line.contains(r#"function="Foo::test_fn""#) 56 | && line.ends_with("} 1") 57 | })); 58 | assert!(metrics.lines().any(|line| line 59 | .starts_with("function_calls_duration_seconds_bucket{") 60 | && line.contains(r#"function="Foo::test_fn""#) 61 | && line.ends_with("} 1"))); 62 | 63 | assert!(metrics.lines().any(|line| { 64 | line.starts_with("function_calls_total{") 65 | && line.contains(r#"function="Foo::test_method""#) 66 | && line.ends_with("} 1") 67 | })); 68 | assert!(metrics.lines().any(|line| line 69 | .starts_with("function_calls_duration_seconds_bucket{") 70 | && line.contains(r#"function="Foo::test_method""#) 71 | && line.ends_with("} 1"))); 72 | } 73 | 74 | #[test] 75 | fn struct_name_autometrics_macro_attribute() { 76 | prometheus_exporter::try_init().ok(); 77 | 78 | struct Bar; 79 | 80 | impl Bar { 81 | #[autometrics(struct_name = "Bar")] 82 | fn test_fn() -> &'static str { 83 | "Hello world!" 84 | } 85 | } 86 | 87 | Bar::test_fn(); 88 | 89 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 90 | assert!(metrics.lines().any(|line| { 91 | line.starts_with("function_calls_total{") 92 | && line.contains(r#"function="Bar::test_fn""#) 93 | && line.ends_with("} 1") 94 | })); 95 | } 96 | 97 | #[test] 98 | fn result() { 99 | prometheus_exporter::try_init().ok(); 100 | 101 | #[autometrics] 102 | fn result_fn(should_error: bool) -> Result<(), ()> { 103 | if should_error { 104 | Err(()) 105 | } else { 106 | Ok(()) 107 | } 108 | } 109 | 110 | result_fn(true).ok(); 111 | result_fn(true).ok(); 112 | result_fn(false).ok(); 113 | 114 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 115 | assert!(metrics 116 | .lines() 117 | .any(|line| line.starts_with("function_calls_total{") 118 | && line.contains(r#"function="result_fn""#) 119 | && line.contains(r#"result="error""#) 120 | && line.ends_with("} 2"))); 121 | assert!(metrics 122 | .lines() 123 | .any(|line| line.starts_with("function_calls_total{") 124 | && line.contains(r#"function="result_fn""#) 125 | && line.contains(r#"result="ok""#) 126 | && line.ends_with("} 1"))); 127 | } 128 | 129 | #[test] 130 | fn ok_if() { 131 | prometheus_exporter::try_init().ok(); 132 | 133 | #[autometrics(ok_if = Option::is_some)] 134 | fn ok_if_fn() -> Option<()> { 135 | None 136 | } 137 | 138 | ok_if_fn(); 139 | 140 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 141 | assert!(metrics.lines().any(|line| { 142 | line.starts_with("function_calls_total{") 143 | && line.contains(r#"function="ok_if_fn""#) 144 | && line.contains(r#"result="error""#) 145 | && line.ends_with("} 1") 146 | })); 147 | } 148 | 149 | #[test] 150 | fn error_if() { 151 | prometheus_exporter::try_init().ok(); 152 | 153 | #[autometrics(error_if = Option::is_none)] 154 | fn error_if_fn() -> Option<()> { 155 | None 156 | } 157 | 158 | error_if_fn(); 159 | 160 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 161 | assert!(metrics.lines().any(|line| { 162 | line.starts_with("function_calls_total{") 163 | && line.contains(r#"function="error_if_fn""#) 164 | && line.contains(r#"result="error""#) 165 | && line.ends_with("} 1") 166 | })); 167 | } 168 | 169 | #[test] 170 | fn caller_labels() { 171 | prometheus_exporter::try_init().ok(); 172 | 173 | mod module_1 { 174 | #[autometrics::autometrics] 175 | pub fn function_1() { 176 | module_2::function_2() 177 | } 178 | 179 | mod module_2 { 180 | #[autometrics::autometrics] 181 | pub fn function_2() {} 182 | } 183 | } 184 | 185 | module_1::function_1(); 186 | 187 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 188 | assert!(metrics.lines().any(|line| { 189 | line.starts_with("function_calls_total{") 190 | && line.contains(r#"caller_function="function_1""#) 191 | && line.contains(r#"caller_module="integration_test::module_1""#) 192 | && line.contains(r#"function="function_2""#) 193 | && line.contains(r#"module="integration_test::module_1::module_2""#) 194 | && line.ends_with("} 1") 195 | })); 196 | } 197 | 198 | #[test] 199 | fn build_info() { 200 | prometheus_exporter::try_init().ok(); 201 | 202 | #[autometrics] 203 | fn function_just_to_initialize_build_info() {} 204 | 205 | function_just_to_initialize_build_info(); 206 | function_just_to_initialize_build_info(); 207 | 208 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 209 | assert!(metrics.lines().any(|line| line.starts_with("build_info{") 210 | && line.contains(r#"branch="""#) 211 | && line.contains(r#"commit="""#) 212 | && line.contains(&format!("version=\"{}\"", env!("CARGO_PKG_VERSION"))) 213 | && line.contains(r#"service_name="autometrics""#) 214 | && line.ends_with("} 1"))); 215 | } 216 | -------------------------------------------------------------------------------- /autometrics/tests/objectives_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(prometheus_exporter)] 2 | use autometrics::{autometrics, objectives::*, prometheus_exporter}; 3 | 4 | #[test] 5 | fn success_rate() { 6 | prometheus_exporter::try_init().ok(); 7 | 8 | const OBJECTIVE: Objective = Objective::new("test").success_rate(ObjectivePercentile::P99); 9 | 10 | #[autometrics(objective = OBJECTIVE)] 11 | fn success_rate_fn() -> &'static str { 12 | "Hello world!" 13 | } 14 | 15 | success_rate_fn(); 16 | success_rate_fn(); 17 | 18 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 19 | assert!(metrics 20 | .lines() 21 | .any(|line| line.starts_with("function_calls_total{") 22 | && line.contains(r#"function="success_rate_fn""#) 23 | && line.contains(r#"objective_name="test""#) 24 | && line.contains(r#"objective_percentile="99""#) 25 | && line.ends_with("} 2"))); 26 | } 27 | 28 | #[cfg(prometheus_exporter)] 29 | #[test] 30 | fn latency() { 31 | prometheus_exporter::try_init().ok(); 32 | 33 | const OBJECTIVE: Objective = 34 | Objective::new("test").latency(ObjectiveLatency::Ms100, ObjectivePercentile::P99_9); 35 | 36 | #[autometrics(objective = OBJECTIVE)] 37 | fn latency_fn() -> &'static str { 38 | "Hello world!" 39 | } 40 | 41 | latency_fn(); 42 | latency_fn(); 43 | 44 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 45 | assert!(metrics.lines().any(|line| { 46 | line.starts_with("function_calls_duration_seconds_bucket{") 47 | && line.contains(r#"function="latency_fn""#) 48 | && line.contains(r#"objective_latency_threshold="0.1""#) 49 | && line.contains(r#"objective_name="test""#) 50 | && line.contains(r#"objective_percentile="99.9""#) 51 | && line.ends_with("} 2") 52 | })); 53 | } 54 | 55 | #[cfg(prometheus_exporter)] 56 | #[test] 57 | fn combined_objective() { 58 | prometheus_exporter::try_init().ok(); 59 | 60 | const OBJECTIVE: Objective = Objective::new("test") 61 | .success_rate(ObjectivePercentile::P99) 62 | .latency(ObjectiveLatency::Ms100, ObjectivePercentile::P99_9); 63 | 64 | #[autometrics(objective = OBJECTIVE)] 65 | fn combined_objective_fn() -> &'static str { 66 | "Hello world!" 67 | } 68 | 69 | combined_objective_fn(); 70 | combined_objective_fn(); 71 | 72 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 73 | assert!(metrics.lines().any(|line| { 74 | line.starts_with("function_calls_total{") 75 | && line.contains(r#"function="combined_objective_fn""#) 76 | && line.contains(r#"objective_name="test""#) 77 | && line.contains(r#"objective_percentile="99""#) 78 | && line.ends_with("} 2") 79 | })); 80 | assert!(metrics.lines().any(|line| { 81 | line.starts_with("function_calls_duration_seconds_bucket{") 82 | && line.contains(r#"function="combined_objective_fn""#) 83 | && line.contains(r#"objective_latency_threshold="0.1""#) 84 | && line.contains(r#"objective_name="test""#) 85 | && line.contains(r#"objective_percentile="99.9""#) 86 | && line.ends_with("} 2") 87 | })); 88 | } 89 | -------------------------------------------------------------------------------- /autometrics/tests/settings_custom_registry.rs: -------------------------------------------------------------------------------- 1 | #![cfg(prometheus_exporter)] 2 | 3 | use autometrics::{autometrics, prometheus_exporter, settings::AutometricsSettings}; 4 | 5 | #[cfg(prometheus_client)] 6 | #[test] 7 | fn custom_prometheus_client_registry() { 8 | use prometheus_client::encoding::text::encode; 9 | use prometheus_client::metrics::counter::Counter; 10 | use prometheus_client::metrics::family::Family; 11 | use prometheus_client::registry::Registry; 12 | 13 | #[autometrics] 14 | fn hello_world() -> &'static str { 15 | "Hello world!" 16 | } 17 | 18 | // Create our own registry 19 | let mut registry = ::default(); 20 | 21 | // Also create a custom metric 22 | let custom_metric = Family::, Counter>::default(); 23 | registry.register("custom_metric", "My custom metric", custom_metric.clone()); 24 | 25 | let settings = AutometricsSettings::builder() 26 | .prometheus_client_registry(registry) 27 | .init(); 28 | 29 | // Increment the custom metric 30 | custom_metric.get_or_create(&vec![("foo", "bar")]).inc(); 31 | 32 | hello_world(); 33 | 34 | let mut metrics = String::new(); 35 | encode(&mut metrics, settings.prometheus_client_registry()).unwrap(); 36 | 37 | // Check that both the autometrics metrics and the custom metrics are present 38 | assert!(metrics 39 | .lines() 40 | .any(|line| line.starts_with("function_calls_total{") 41 | && line.contains(r#"function="hello_world""#))); 42 | assert!(metrics 43 | .lines() 44 | .any(|line| line == "custom_metric_total{foo=\"bar\"} 1")); 45 | 46 | // The output of the prometheus_exporter should be the same 47 | assert_eq!(metrics, prometheus_exporter::encode_to_string().unwrap()); 48 | } 49 | 50 | #[cfg(prometheus)] 51 | #[test] 52 | fn custom_prometheus_registry() { 53 | use prometheus::{register_counter_vec_with_registry, Registry, TextEncoder}; 54 | let registry = Registry::new(); 55 | 56 | let custom_metric = register_counter_vec_with_registry!( 57 | "custom_metric", 58 | "My custom metric", 59 | &["foo"], 60 | registry.clone() 61 | ) 62 | .unwrap(); 63 | 64 | let settings = AutometricsSettings::builder() 65 | .prometheus_registry(registry) 66 | .init(); 67 | 68 | #[autometrics] 69 | fn hello_world() -> &'static str { 70 | "Hello world!" 71 | } 72 | 73 | hello_world(); 74 | custom_metric.with_label_values(&["bar"]).inc(); 75 | 76 | let mut metrics = String::new(); 77 | TextEncoder::new() 78 | .encode_utf8(&settings.prometheus_registry().gather(), &mut metrics) 79 | .unwrap(); 80 | 81 | // Check that both the autometrics metrics and the custom metrics are present 82 | assert!(metrics 83 | .lines() 84 | .any(|line| line.starts_with("function_calls_total{") 85 | && line.contains(r#"function="hello_world""#))); 86 | assert!(metrics 87 | .lines() 88 | .any(|line| line == "custom_metric{foo=\"bar\"} 1")); 89 | 90 | // The output of the prometheus_exporter should be the same 91 | assert_eq!(metrics, prometheus_exporter::encode_to_string().unwrap()); 92 | } 93 | 94 | #[cfg(opentelemetry)] 95 | #[test] 96 | fn custom_opentelemetry_registry() { 97 | use opentelemetry::{global, KeyValue}; 98 | use prometheus::{Registry, TextEncoder}; 99 | 100 | // OpenTelemetry uses the `prometheus` crate under the hood 101 | let registry = Registry::new(); 102 | 103 | let settings = AutometricsSettings::builder() 104 | .prometheus_registry(registry) 105 | .init(); 106 | 107 | let custom_metric = global::meter("foo").u64_counter("custom_metric").init(); 108 | 109 | #[autometrics] 110 | fn hello_world() -> &'static str { 111 | "Hello world!" 112 | } 113 | 114 | hello_world(); 115 | custom_metric.add(1, &[KeyValue::new("foo", "bar")]); 116 | 117 | let mut metrics = String::new(); 118 | TextEncoder::new() 119 | .encode_utf8(&settings.prometheus_registry().gather(), &mut metrics) 120 | .unwrap(); 121 | 122 | // Check that both the autometrics metrics and the custom metrics are present 123 | assert!(metrics 124 | .lines() 125 | .any(|line| line.starts_with("function_calls_total{") 126 | && line.contains(r#"function="hello_world""#))); 127 | assert!(metrics 128 | .lines() 129 | .any(|line| line.starts_with("custom_metric_total{") 130 | && line.contains("foo=\"bar\"") 131 | && line.ends_with("} 1"))); 132 | 133 | // The output of the prometheus_exporter should be the same 134 | assert_eq!(metrics, prometheus_exporter::encode_to_string().unwrap()); 135 | } 136 | -------------------------------------------------------------------------------- /autometrics/tests/settings_histogram_buckets_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(prometheus_exporter)] 2 | 3 | use autometrics::{autometrics, prometheus_exporter, settings::AutometricsSettings}; 4 | 5 | #[test] 6 | fn custom_histogram_buckets() { 7 | #[autometrics] 8 | fn custom_histogram_buckets_fn() -> &'static str { 9 | "Hello world!" 10 | } 11 | 12 | AutometricsSettings::builder() 13 | .histogram_buckets(vec![0.1, 0.2, 0.3, 0.4, 0.5]) 14 | .init(); 15 | 16 | custom_histogram_buckets_fn(); 17 | 18 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 19 | assert!(metrics.lines().any(|line| line.contains(r#"le="0.1"#))); 20 | assert!(metrics.lines().any(|line| line.contains(r#"le="0.2"#))); 21 | assert!(metrics.lines().any(|line| line.contains(r#"le="0.3"#))); 22 | assert!(metrics.lines().any(|line| line.contains(r#"le="0.4"#))); 23 | assert!(metrics.lines().any(|line| line.contains(r#"le="0.5"#))); 24 | } 25 | -------------------------------------------------------------------------------- /autometrics/tests/settings_service_name_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(prometheus_exporter)] 2 | 3 | use autometrics::{autometrics, prometheus_exporter, settings::AutometricsSettings}; 4 | 5 | #[test] 6 | fn set_service_name() { 7 | #[autometrics] 8 | fn test_fn() -> &'static str { 9 | "Hello world!" 10 | } 11 | 12 | AutometricsSettings::builder() 13 | .service_name("test_service") 14 | .init(); 15 | 16 | let metrics = prometheus_exporter::encode_to_string().unwrap(); 17 | assert!(metrics 18 | .lines() 19 | .any(|line| line.contains(r#"service_name="test_service""#))); 20 | } 21 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Autometrics Examples 2 | 3 | This directory contains various examples showing off how to use different autometrics features and integrations. 4 | 5 | You can run each of the examples with: 6 | ```shell 7 | cargo run --package example-{name of example} 8 | ``` 9 | 10 | ## Basic Examples 11 | 12 | - [actix-web](./actix-web) - Use autometrics to instrument HTTP handlers using the `actix-web` framework 13 | - [axum](./axum) - Use autometrics to instrument HTTP handlers using the `axum` framework 14 | - [custom-metrics](./custom-metrics/) - Define your own custom metrics alongside the ones generated by autometrics (using any of the metrics collection crates) 15 | - [exemplars-tracing](./exemplars-tracing/) - Use fields from `tracing::Span`s as Prometheus exemplars 16 | - [opentelemetry-push](./opentelemetry-push/) - Push metrics to an OpenTelemetry Collector via the OTLP HTTP or gRPC protocol using the Autometrics provided interface 17 | - [grpc-http](./grpc-http/) - Instrument Rust gRPC services with metrics using Tonic, warp, and Autometrics. 18 | - [opentelemetry-push-custom](./opentelemetry-push-custom/) - Push metrics to an OpenTelemetry Collector via the OTLP gRPC protocol using custom options 19 | 20 | ## Full Example 21 | 22 | Look at the [full-api](./full-api) example to see autometrics in use in an example API server built with `axum`, `thiserror`, and `tokio`. 23 | -------------------------------------------------------------------------------- /examples/actix-web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-actix-web" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | actix-web = "4.3.1" 9 | autometrics = { path = "../../autometrics", features = ["prometheus-exporter"] } 10 | autometrics-example-util = { path = "../util" } 11 | rand = "0.8.5" 12 | reqwest = "0.11" 13 | tokio = { version = "1.28.2", features = ["full"] } 14 | -------------------------------------------------------------------------------- /examples/actix-web/README.md: -------------------------------------------------------------------------------- 1 | # Autometrics actix-web Example 2 | 3 | This shows how you can instrument [`actix-web`](https://github.com/actix/actix-web) 4 | HTTP handler functions with autometrics. 5 | 6 | ## Running the example 7 | 8 | **Note:** You will need [Prometheus](https://prometheus.io/download/) installed locally for the full experience. 9 | 10 | ```shell 11 | cargo run -p example-actix-web 12 | ``` 13 | 14 | This will start the server, generate some fake traffic, and run a local Prometheus instance that is configured 15 | to scrape the metrics from the server's `/metrics` endpoint. 16 | -------------------------------------------------------------------------------- /examples/actix-web/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_web::http::StatusCode; 2 | use actix_web::{get, App, HttpServer, Responder}; 3 | use autometrics::autometrics; 4 | use autometrics_example_util::{run_prometheus, sleep_random_duration}; 5 | use rand::{random, thread_rng, Rng}; 6 | use std::io; 7 | use std::time::Duration; 8 | use tokio::time::sleep; 9 | 10 | #[get("/")] 11 | #[autometrics] 12 | async fn index_get() -> &'static str { 13 | "Hello world!" 14 | } 15 | 16 | #[get("/random-error")] 17 | #[autometrics] 18 | async fn random_error_get() -> Result<&'static str, io::Error> { 19 | let should_error: bool = random(); 20 | 21 | sleep_random_duration().await; 22 | 23 | if should_error { 24 | Err(io::Error::new(io::ErrorKind::Other, "its joever")) 25 | } else { 26 | Ok("ok") 27 | } 28 | } 29 | 30 | /// This function doesn't return a Result, but we can determine whether 31 | /// we want to consider it a success or not by passing a function to the `ok_if` parameter. 32 | #[autometrics(ok_if = is_success)] 33 | pub async fn route_that_returns_responder() -> impl Responder { 34 | ("Hello world!", StatusCode::OK) 35 | } 36 | 37 | /// Determine whether the response was a success or not 38 | fn is_success(_: &R) -> bool { 39 | random() 40 | } 41 | 42 | #[tokio::main] 43 | async fn main() -> io::Result<()> { 44 | let address = "127.0.0.1:3000"; 45 | 46 | // Run Prometheus and generate random traffic for the app 47 | // (You would not actually do this in production, but it makes it easier to see the example in action) 48 | let _prometheus = run_prometheus(false); 49 | tokio::spawn(generate_random_traffic()); 50 | 51 | println!( 52 | "The example API server is now running on: {address} \n\ 53 | Wait a few seconds for the traffic generator to create some fake traffic. \n\ 54 | Then, hover over one of the HTTP handler functions (in your editor) to bring up the Rust Docs. \n\ 55 | Click on one of the Autometrics links to see the graph for that handler's metrics in Prometheus." 56 | ); 57 | 58 | HttpServer::new(|| App::new().service(index_get).service(random_error_get)) 59 | .bind(address)? 60 | .run() 61 | .await 62 | } 63 | 64 | /// Make some random API calls to generate data that we can see in the graphs 65 | async fn generate_random_traffic() { 66 | loop { 67 | let sleep_duration = Duration::from_millis(thread_rng().gen_range(10..50)); 68 | 69 | let url = match thread_rng().gen_range(0..2) { 70 | 0 => "http://localhost:3000", 71 | 1 => "http://localhost:3000/random-error", 72 | _ => unreachable!(), 73 | }; 74 | 75 | let _ = reqwest::get(url).await; 76 | sleep(sleep_duration).await 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-axum" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | autometrics = { path = "../../autometrics", features = ["prometheus-exporter"] } 9 | autometrics-example-util = { path = "../util" } 10 | axum = { version = "0.7.2", features = ["json"] } 11 | rand = "0.8" 12 | reqwest = { version = "0.11", features = ["json"] } 13 | tokio = { version = "1", features = ["full"] } 14 | -------------------------------------------------------------------------------- /examples/axum/README.md: -------------------------------------------------------------------------------- 1 | # Autometrics Axum Example 2 | 3 | This shows how you can instrument [Axum](https://github.com/tokio-rs/axum) HTTP handler functions with autometrics. 4 | 5 | For a more complete example also using axum, see the [full API example](../full-api/). 6 | 7 | ## Running the example 8 | 9 | **Note:** You will need [prometheus](https://prometheus.io/download/) installed locally for the full experience. 10 | 11 | ```shell 12 | cargo run -p example-axum 13 | ``` 14 | 15 | This will start the server, generate some fake traffic, and run a local Prometheus instance that is configured to scrape the metrics from the server's `/metrics` endpoint. 16 | -------------------------------------------------------------------------------- /examples/axum/src/main.rs: -------------------------------------------------------------------------------- 1 | use autometrics::{autometrics, prometheus_exporter}; 2 | use autometrics_example_util::{run_prometheus, sleep_random_duration}; 3 | use axum::{http::StatusCode, response::IntoResponse, routing::get, Router}; 4 | use rand::{random, thread_rng, Rng}; 5 | use std::error::Error; 6 | use std::net::Ipv4Addr; 7 | use std::time::Duration; 8 | use tokio::net::TcpListener; 9 | 10 | // Starting simple, hover over the function name to see the Autometrics graph links in the Rust Docs! 11 | /// This is a simple endpoint that never errors 12 | #[autometrics] 13 | pub async fn get_index() -> &'static str { 14 | "Hello, World!" 15 | } 16 | 17 | /// This is a function that returns an error ~50% of the time 18 | /// The call counter metric generated by autometrics will have a label 19 | /// `result` = `ok` or `error`, depending on what the function returned 20 | #[autometrics] 21 | pub async fn get_random_error() -> Result<(), ()> { 22 | let should_error = random::(); 23 | 24 | sleep_random_duration().await; 25 | 26 | if should_error { 27 | Err(()) 28 | } else { 29 | Ok(()) 30 | } 31 | } 32 | 33 | /// This function doesn't return a Result, but we can determine whether 34 | /// we want to consider it a success or not by passing a function to the `ok_if` parameter. 35 | #[autometrics(ok_if = is_success)] 36 | pub async fn route_that_returns_into_response() -> impl IntoResponse { 37 | (StatusCode::OK, "Hello, World!") 38 | } 39 | 40 | /// Determine whether the response was a success or not 41 | fn is_success(response: &R) -> bool 42 | where 43 | R: Copy + IntoResponse, 44 | { 45 | response.into_response().status().is_success() 46 | } 47 | 48 | #[tokio::main] 49 | pub async fn main() -> Result<(), Box> { 50 | // Run Prometheus and generate random traffic for the app 51 | // (You would not actually do this in production, but it makes it easier to see the example in action) 52 | let _prometheus = run_prometheus(false); 53 | tokio::spawn(generate_random_traffic()); 54 | 55 | let app = Router::new() 56 | .route("/", get(get_index)) 57 | .route("/random-error", get(get_random_error)) 58 | // Expose the metrics for Prometheus to scrape 59 | .route( 60 | "/metrics", 61 | get(|| async { prometheus_exporter::encode_http_response() }), 62 | ); 63 | 64 | let listener = TcpListener::bind((Ipv4Addr::from([127, 0, 0, 1]), 3000)).await?; 65 | let addr = listener.local_addr()?; 66 | 67 | println!( 68 | "The example API server is now running on: {addr} 69 | 70 | Wait a few seconds for the traffic generator to create some fake traffic. 71 | Then, hover over one of the HTTP handler functions (in your editor) to bring up the Rust Docs. 72 | 73 | Click on one of the Autometrics links to see the graph for that handler's metrics in Prometheus." 74 | ); 75 | 76 | axum::serve(listener, app).await?; 77 | Ok(()) 78 | } 79 | 80 | /// Make some random API calls to generate data that we can see in the graphs 81 | pub async fn generate_random_traffic() { 82 | let client = reqwest::Client::new(); 83 | loop { 84 | let request_type = thread_rng().gen_range(0..2); 85 | let sleep_duration = Duration::from_millis(thread_rng().gen_range(10..50)); 86 | match request_type { 87 | 0 => { 88 | let _ = client.get("http://localhost:3000").send().await; 89 | } 90 | 1 => { 91 | let _ = reqwest::get("http://localhost:3000/random-error").await; 92 | } 93 | _ => unreachable!(), 94 | } 95 | tokio::time::sleep(sleep_duration).await 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /examples/custom-metrics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-custom-metrics" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [features] 8 | metrics = ["autometrics/metrics", "dep:metrics"] 9 | opentelemetry = ["autometrics/opentelemetry", "dep:opentelemetry"] 10 | prometheus = ["autometrics/prometheus", "dep:prometheus"] 11 | prometheus-client = ["autometrics/prometheus-client", "dep:prometheus-client"] 12 | 13 | [dependencies] 14 | autometrics = { path = "../../autometrics", features = ["prometheus-exporter"] } 15 | metrics = { version = "0.21.1", optional = true } 16 | once_cell = "1.17" 17 | opentelemetry = { version = "0.20", features = ["metrics"], optional = true } 18 | prometheus = { version = "0.13", optional = true } 19 | prometheus-client = { version = "0.21.2", optional = true } 20 | -------------------------------------------------------------------------------- /examples/custom-metrics/README.md: -------------------------------------------------------------------------------- 1 | # Autometrics + Custom Metrics Example 2 | 3 | This example demonstrates how you can collect custom metrics in addition to the ones generated by autometrics. 4 | 5 | ## Running the example 6 | 7 | ### Using the `opentelemetry` crate 8 | 9 | ```shell 10 | cargo run -p example-custom-metrics --features=opentelemetry-0_20 11 | ``` 12 | 13 | ### Using the `metrics` crate 14 | 15 | ```shell 16 | cargo run -p example-custom-metrics --features=metrics-0_24 17 | ``` 18 | 19 | ### Using the `prometheus` crate 20 | 21 | ```shell 22 | cargo run -p example-custom-metrics --features=prometheus-0_13 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/custom-metrics/src/main.rs: -------------------------------------------------------------------------------- 1 | use autometrics::{autometrics, prometheus_exporter}; 2 | use once_cell::sync::{Lazy, OnceCell}; 3 | #[cfg(feature = "prometheus-client")] 4 | use prometheus_client::metrics::{counter::Counter, family::Family}; 5 | 6 | /// Example HTTP handler function 7 | #[autometrics] 8 | pub fn get_index_handler() -> Result { 9 | Ok("Hello world!".to_string()) 10 | } 11 | 12 | // Run the example with `--features=metrics` to use the `metrics` crate to define additional metrics. 13 | #[cfg(feature = "metrics")] 14 | pub fn function_with_custom_metrics_metric() { 15 | use metrics::counter; 16 | 17 | counter!("custom_metrics_counter", 1, "foo" => "bar"); 18 | } 19 | 20 | // Run the example with `--features=opentelemetry` to use the `opentelemetry` crate to define additional metrics. 21 | #[cfg(feature = "openetelemetry")] 22 | pub fn function_with_custom_opentelemetry_metric() { 23 | use once_cell::sync::Lazy; 24 | use opentelemetry::{global, metrics::Counter, KeyValue}; 25 | 26 | static COUNTER: Lazy> = Lazy::new(|| { 27 | global::meter("") 28 | .u64_counter("custom_opentelemetry_counter") 29 | .init() 30 | }); 31 | COUNTER.add(1, &[KeyValue::new("foo", "bar")]); 32 | } 33 | 34 | // Run the example with `--features=prometheus` to use the `prometheus` crate to define additional metrics. 35 | #[cfg(feature = "prometheus")] 36 | pub fn function_with_custom_prometheus_metric() { 37 | use once_cell::sync::Lazy; 38 | use prometheus::{register_int_counter_vec, IntCounterVec}; 39 | 40 | static COUNTER: Lazy = Lazy::new(|| { 41 | register_int_counter_vec!( 42 | "custom_prometheus_counter", 43 | "Custom counter", 44 | &["foo", "library"] 45 | ) 46 | .unwrap() 47 | }); 48 | 49 | COUNTER.with_label_values(&["bar", "prometheus"]).inc(); 50 | } 51 | 52 | #[cfg(feature = "prometheus-client")] 53 | static CUSTOM_COUNTER: OnceCell, Counter>> = OnceCell::new(); 54 | 55 | #[cfg(feature = "prometheus-client")] 56 | pub fn function_with_custom_prometheus_client_metric() { 57 | CUSTOM_COUNTER 58 | .get() 59 | .unwrap() 60 | .get_or_create(&vec![("foo", "bar")]) 61 | .inc(); 62 | } 63 | 64 | pub fn main() { 65 | // The global metrics exporter will collect the metrics generated by 66 | // autometrics as well as any custom metrics you add 67 | #[cfg(not(feature = "prometheus-client"))] 68 | prometheus_exporter::init(); 69 | 70 | #[cfg(feature = "prometheus-client")] 71 | { 72 | use autometrics::settings::AutometricsSettingsBuilder; 73 | use prometheus_client::registry::Registry; 74 | 75 | let mut registry = ::default(); 76 | let custom_counter = Family::, Counter>::default(); 77 | registry.register("custom_counter", "Custom counter", custom_counter.clone()); 78 | CUSTOM_COUNTER.set(custom_counter).unwrap(); 79 | 80 | AutometricsSettings::builder() 81 | .prometheus_client_registry(registry) 82 | .init(); 83 | } 84 | 85 | for _i in 0..5 { 86 | get_index_handler().unwrap(); 87 | 88 | #[cfg(feature = "metrics")] 89 | function_with_custom_metrics_metric(); 90 | #[cfg(feature = "opentelemetry")] 91 | function_with_custom_opentelemetry_metric(); 92 | #[cfg(feature = "prometheus")] 93 | function_with_custom_prometheus_metric(); 94 | #[cfg(feature = "prometheus-client")] 95 | function_with_custom_prometheus_client_metric(); 96 | } 97 | 98 | // Normally, you would expose a /metrics endpoint that Prometheus would scrape 99 | // but in this example, we're just printing out the metrics in the Prometheus text format 100 | println!("{}", prometheus_exporter::encode_to_string().unwrap()); 101 | } 102 | -------------------------------------------------------------------------------- /examples/exemplars-tracing-opentelemetry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-exemplars-tracing-opentelemetry" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | autometrics = { path = "../../autometrics", features = [ 9 | "prometheus-client-0_22", 10 | "prometheus-exporter", 11 | "exemplars-tracing-opentelemetry-0_25", 12 | ] } 13 | autometrics-example-util = { path = "../util" } 14 | axum = { version = "0.7.2", features = ["json"] } 15 | opentelemetry = "0.24" 16 | opentelemetry_sdk = "0.24" 17 | opentelemetry-stdout = { version = "0.5", features = ["trace"] } 18 | reqwest = { version = "0.11", features = ["json"] } 19 | tokio = { version = "1", features = ["full"] } 20 | tracing = "0.1" 21 | tracing-opentelemetry = "0.25" 22 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 23 | -------------------------------------------------------------------------------- /examples/exemplars-tracing-opentelemetry/README.md: -------------------------------------------------------------------------------- 1 | # Tracing + OpenTelemetry Exemplars Example 2 | 3 | This example demonstrates how Autometrics can create exemplars with the `trace_id` and `span_id` from the [`opentelemetry::Context`](https://docs.rs/opentelemetry/latest/opentelemetry/struct.Context.html), which is created by the [`tracing_opentelemetry::OpenTelemetryLayer`](https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/struct.OpenTelemetryLayer.html) and propagated by the `tracing` library. 4 | 5 | > **Note** 6 | > 7 | > Prometheus must be [specifically configured](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage) to enable the experimental exemplars feature. 8 | -------------------------------------------------------------------------------- /examples/exemplars-tracing-opentelemetry/src/main.rs: -------------------------------------------------------------------------------- 1 | use autometrics::{autometrics, prometheus_exporter}; 2 | use autometrics_example_util::run_prometheus; 3 | use axum::{routing::get, Router}; 4 | use opentelemetry::trace::TracerProvider as _; 5 | use opentelemetry_sdk::trace::TracerProvider; 6 | use opentelemetry_stdout::SpanExporter; 7 | use std::error::Error; 8 | use std::net::Ipv4Addr; 9 | use std::{io, net::SocketAddr, time::Duration}; 10 | use tokio::net::TcpListener; 11 | use tracing::{instrument, trace}; 12 | use tracing_opentelemetry::OpenTelemetryLayer; 13 | use tracing_subscriber::{layer::SubscriberExt, prelude::*, Registry}; 14 | 15 | // The instrument macro creates a new span for every call to this function, 16 | // and the OpenTelemetryLayer added below attaches the OpenTelemetry Context 17 | // to every span. 18 | // 19 | // Autometrics will pick up that Context and create exemplars from it. 20 | #[autometrics] 21 | #[instrument] 22 | async fn outer_function() { 23 | inner_function(); 24 | } 25 | 26 | // This function will also have exemplars because it is called within 27 | // the span of the outer_function 28 | #[autometrics] 29 | fn inner_function() { 30 | trace!("Inner function called"); 31 | } 32 | 33 | #[tokio::main] 34 | async fn main() -> Result<(), Box> { 35 | // Run Prometheus with flag --enable-feature=exemplars-storage 36 | let _prometheus = run_prometheus(true); 37 | tokio::spawn(generate_random_traffic()); 38 | 39 | // This exporter will discard the spans but you can use the other to see them 40 | let exporter = SpanExporter::builder().with_writer(io::sink()).build(); 41 | // let exporter = SpanExporter::default(); 42 | 43 | let provider = TracerProvider::builder() 44 | .with_simple_exporter(exporter) 45 | .build(); 46 | let tracer = provider.tracer("example"); 47 | 48 | // This adds the OpenTelemetry Context to every tracing Span 49 | let otel_layer = OpenTelemetryLayer::new(tracer); 50 | Registry::default().with(otel_layer).init(); 51 | 52 | prometheus_exporter::init(); 53 | 54 | let app = Router::new().route("/", get(outer_function)).route( 55 | "/metrics", 56 | // Expose the metrics to Prometheus in the OpenMetrics format 57 | get(|| async { prometheus_exporter::encode_http_response() }), 58 | ); 59 | 60 | println!("\nVisit the following URL to see one of the charts along with the exemplars:"); 61 | println!("http://localhost:9090/graph?g0.expr=%23%20Rate%20of%20calls%20to%20the%20%60outer_function%60%20function%20per%20second%2C%20averaged%20over%205%20minute%20windows%0A%0Asum%20by%20(function%2C%20module%2C%20commit%2C%20version)%20(rate(%7B__name__%3D~%22function_calls(_count)%3F(_total)%3F%22%2Cfunction%3D%22outer_function%22%7D%5B5m%5D)%20*%20on%20(instance%2C%20job)%20group_left(version%2C%20commit)%20last_over_time(build_info%5B1s%5D))&g0.tab=0&g0.stacked=0&g0.show_exemplars=1&g0.range_input=1h"); 62 | 63 | let listener = TcpListener::bind((Ipv4Addr::from([127, 0, 0, 1]), 3000)).await?; 64 | axum::serve( 65 | listener, 66 | app.into_make_service_with_connect_info::(), 67 | ) 68 | .await?; 69 | 70 | opentelemetry::global::shutdown_tracer_provider(); 71 | Ok(()) 72 | } 73 | 74 | pub async fn generate_random_traffic() { 75 | let client = reqwest::Client::new(); 76 | loop { 77 | client.get("http://localhost:3000").send().await.ok(); 78 | tokio::time::sleep(Duration::from_millis(100)).await 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/exemplars-tracing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-exemplars-tracing" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | autometrics = { path = "../../autometrics", features = [ 9 | "prometheus-client-0_22", 10 | "prometheus-exporter", 11 | "exemplars-tracing" 12 | ] } 13 | autometrics-example-util = { path = "../util" } 14 | axum = { version = "0.7.2", features = ["json"] } 15 | reqwest = { version = "0.11", features = ["json"] } 16 | tokio = { version = "1", features = ["full"] } 17 | tracing = "0.1" 18 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 19 | uuid = { version = "1.3", features = ["v4"] } 20 | -------------------------------------------------------------------------------- /examples/exemplars-tracing/README.md: -------------------------------------------------------------------------------- 1 | # Tracing Exemplars Example 2 | 3 | This example demonstrates how Autometrics can pick up the `trace_id` from [`tracing::Span`](https://docs.rs/tracing/latest/tracing/struct.Span.html)s and attach them to the metrics as [exemplars](https://grafana.com/docs/grafana/latest/fundamentals/exemplars/). 4 | 5 | > **Note** 6 | > 7 | > Prometheus must be [specifically configured](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage) to enable the experimental exemplars feature. 8 | -------------------------------------------------------------------------------- /examples/exemplars-tracing/src/main.rs: -------------------------------------------------------------------------------- 1 | use autometrics::{ 2 | autometrics, exemplars::tracing::AutometricsExemplarExtractor, prometheus_exporter, 3 | }; 4 | use autometrics_example_util::run_prometheus; 5 | use axum::{http::header::CONTENT_TYPE, response::Response, routing::get, Router}; 6 | use std::error::Error; 7 | use std::net::Ipv4Addr; 8 | use std::{net::SocketAddr, time::Duration}; 9 | use tokio::net::TcpListener; 10 | use tracing::{instrument, trace}; 11 | use tracing_subscriber::{prelude::*, EnvFilter}; 12 | use uuid::Uuid; 13 | 14 | // Autometrics looks for a field called `trace_id` and attaches 15 | // that as an exemplar for the metrics it generates. 16 | #[autometrics] 17 | #[instrument(fields(trace_id = %Uuid::new_v4()))] 18 | async fn outer_function() -> String { 19 | trace!("Outer function called"); 20 | inner_function("hello"); 21 | "Hello world!".to_string() 22 | } 23 | 24 | // This function will also have the `trace_id` attached as an exemplar 25 | // because it is called within the same span as `outer_function`. 26 | #[autometrics] 27 | #[instrument] 28 | fn inner_function(param: &str) { 29 | trace!("Inner function called"); 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() -> Result<(), Box> { 34 | // Run Prometheus with flag --enable-feature=exemplars-storage 35 | let _prometheus = run_prometheus(true); 36 | tokio::spawn(generate_random_traffic()); 37 | 38 | prometheus_exporter::init(); 39 | tracing_subscriber::fmt::fmt() 40 | .finish() 41 | .with(EnvFilter::from_default_env()) 42 | // Set up the tracing layer to expose the `trace_id` field to Autometrics. 43 | .with(AutometricsExemplarExtractor::from_fields(&["trace_id"])) 44 | .init(); 45 | 46 | let app = Router::new().route("/", get(outer_function)).route( 47 | "/metrics", 48 | // Expose the metrics to Prometheus in the OpenMetrics format 49 | get(|| async { prometheus_exporter::encode_http_response() }), 50 | ); 51 | 52 | println!("\nVisit the following URL to see one of the charts along with the exemplars:"); 53 | println!("http://localhost:9090/graph?g0.expr=%23%20Rate%20of%20calls%20to%20the%20%60outer_function%60%20function%20per%20second%2C%20averaged%20over%205%20minute%20windows%0A%0Asum%20by%20(function%2C%20module%2C%20commit%2C%20version)%20(rate(%7B__name__%3D~%22function_calls(_count)%3F(_total)%3F%22%2Cfunction%3D%22outer_function%22%7D%5B5m%5D)%20*%20on%20(instance%2C%20job)%20group_left(version%2C%20commit)%20last_over_time(build_info%5B1s%5D))&g0.tab=0&g0.stacked=0&g0.show_exemplars=1&g0.range_input=1h"); 54 | 55 | let listener = TcpListener::bind((Ipv4Addr::from([127, 0, 0, 1]), 3000)).await?; 56 | axum::serve(listener, app).await?; 57 | Ok(()) 58 | } 59 | 60 | pub async fn generate_random_traffic() { 61 | let client = reqwest::Client::new(); 62 | loop { 63 | client.get("http://localhost:3000").send().await.ok(); 64 | tokio::time::sleep(Duration::from_millis(100)).await 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/full-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-full-api" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | autometrics = { path = "../../autometrics", features = ["prometheus-exporter"] } 9 | autometrics-example-util = { path = "../util" } 10 | axum = { version = "0.7.2", features = ["json"] } 11 | rand = "0.8" 12 | reqwest = { version = "0.11", features = ["json"] } 13 | serde = { version = "1", features = ["derive"] } 14 | strum = { version = "0.24", features = ["derive"] } 15 | thiserror = "1" 16 | tokio = { version = "1", features = ["full"] } 17 | 18 | [build-dependencies] 19 | vergen = { version = "8.1", features = ["git", "gitcl"] } 20 | -------------------------------------------------------------------------------- /examples/full-api/README.md: -------------------------------------------------------------------------------- 1 | # Autometrics Full API Example 2 | 3 | This is a complete example of how to use autometrics with an API server built with `axum`, `thiserror`, and `tokio`. 4 | 5 | ## Running the example 6 | 7 | **Note:** You will need [prometheus](https://prometheus.io/download/) installed locally for the full experience. 8 | 9 | This will start the server, generate some fake traffic, and run a local Prometheus instance that is configured to scrape the metrics from the server's `/metrics` endpoint. 10 | 11 | ```shell 12 | cargo run -p example-full-api 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/full-api/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | vergen::EmitBuilder::builder() 3 | .git_sha(true) // short commit hash 4 | .emit() 5 | .expect("Unable to generate build info"); 6 | } 7 | -------------------------------------------------------------------------------- /examples/full-api/src/database.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ApiError; 2 | use autometrics::autometrics; 3 | use autometrics_example_util::sleep_random_duration; 4 | use rand::random; 5 | 6 | #[derive(Clone)] 7 | pub struct Database; 8 | 9 | // You can instrument a whole impl block like this: 10 | #[autometrics] 11 | impl Database { 12 | #[skip_autometrics] 13 | pub fn new() -> Self { 14 | Self 15 | } 16 | 17 | /// An internal function that is also instrumented with autometrics 18 | pub async fn load_details(&self) -> Result<(), ApiError> { 19 | let should_error = random::(); 20 | 21 | sleep_random_duration().await; 22 | 23 | if should_error { 24 | Err(ApiError::Internal) 25 | } else { 26 | Ok(()) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/full-api/src/error.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use axum::response::{IntoResponse, Response}; 3 | use strum::IntoStaticStr; 4 | use thiserror::Error; 5 | 6 | // We're using `thiserror` to define our error type, and we're using `strum` to 7 | // enable the error variants to be turned into &'static str's, which 8 | // will actually become another label on the call counter metric. 9 | // 10 | // In this case, the label will be `error` = `not_found`, `bad_request`, or `internal`. 11 | // 12 | // Instead of looking at high-level HTTP status codes in our metrics, 13 | // we'll instead see the actual variant name of the error. 14 | #[derive(Debug, Error, IntoStaticStr)] 15 | #[strum(serialize_all = "snake_case")] 16 | pub enum ApiError { 17 | #[error("User not found")] 18 | NotFound, 19 | #[error("Bad request")] 20 | BadRequest, 21 | #[error("Internal server error")] 22 | Internal, 23 | } 24 | 25 | impl IntoResponse for ApiError { 26 | fn into_response(self) -> Response { 27 | let status_code = match self { 28 | ApiError::NotFound => StatusCode::NOT_FOUND, 29 | ApiError::BadRequest => StatusCode::BAD_REQUEST, 30 | ApiError::Internal => StatusCode::INTERNAL_SERVER_ERROR, 31 | }; 32 | (status_code, format!("{:?}", self)).into_response() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/full-api/src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::database::Database; 2 | use crate::util::generate_random_traffic; 3 | use autometrics::prometheus_exporter; 4 | use autometrics_example_util::run_prometheus; 5 | use axum::routing::{get, post}; 6 | use axum::Router; 7 | use std::error::Error; 8 | use std::net::{Ipv4Addr, SocketAddr}; 9 | use tokio::net::TcpListener; 10 | 11 | mod database; 12 | mod error; 13 | mod routes; 14 | mod util; 15 | 16 | /// Run the API server as well as Prometheus and a traffic generator 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | // Run Prometheus and generate random traffic for the app 20 | // (You would not actually do this in production, but it makes it easier to see the example in action) 21 | let _prometheus = run_prometheus(false); 22 | tokio::spawn(generate_random_traffic()); 23 | 24 | // Set up the exporter to collect metrics 25 | prometheus_exporter::init(); 26 | 27 | let app = Router::new() 28 | .route("/", get(routes::get_index)) 29 | .route("/users", post(routes::create_user)) 30 | .route("/random-error", get(routes::get_random_error)) 31 | // Expose the metrics for Prometheus to scrape 32 | .route( 33 | "/metrics", 34 | get(|| async { prometheus_exporter::encode_http_response() }), 35 | ) 36 | .with_state(Database::new()); 37 | 38 | let listener = TcpListener::bind((Ipv4Addr::from([127, 0, 0, 1]), 3000)).await?; 39 | let addr = listener.local_addr()?; 40 | 41 | println!( 42 | "The example API server is now running on: {addr} 43 | 44 | Wait a few seconds for the traffic generator to create some fake traffic. 45 | Then, hover over one of the HTTP handler functions (in your editor) to bring up the Rust Docs. 46 | 47 | Click on one of the Autometrics links to see the graph for that handler's metrics in Prometheus." 48 | ); 49 | 50 | axum::serve(listener, app).await?; 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/full-api/src/routes.rs: -------------------------------------------------------------------------------- 1 | use autometrics::{autometrics, objectives::*}; 2 | use autometrics_example_util::sleep_random_duration; 3 | use axum::{extract::State, http::StatusCode, Json}; 4 | use rand::{thread_rng, Rng}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::database::Database; 8 | use crate::error::ApiError; 9 | 10 | /// This is a Service-Level Objective (SLO) we're defining for our API. 11 | const API_SLO: Objective = Objective::new("api") 12 | .success_rate(ObjectivePercentile::P99_9) 13 | .latency(ObjectiveLatency::Ms500, ObjectivePercentile::P99); 14 | 15 | // Starting simple, hover over the function name to see the Autometrics graph links in the Rust Docs! 16 | /// This is a simple endpoint that never errors 17 | #[autometrics] 18 | pub async fn get_index() -> &'static str { 19 | "Hello, World!" 20 | } 21 | 22 | /// This is a function that returns an error ~25% of the time 23 | /// The call counter metric generated by autometrics will have a label 24 | /// `result` = `ok` or `error`, depending on what the function returned 25 | #[autometrics(objective = API_SLO)] 26 | pub async fn get_random_error() -> Result<(), ApiError> { 27 | let error = thread_rng().gen_range(0..4); 28 | 29 | sleep_random_duration().await; 30 | 31 | match error { 32 | 0 => Err(ApiError::NotFound), 33 | 1 => Err(ApiError::BadRequest), 34 | 2 => Err(ApiError::Internal), 35 | _ => Ok(()), 36 | } 37 | } 38 | 39 | // This handler calls another internal function that is also instrumented with autometrics. 40 | // 41 | // Unlike other instrumentation libraries, autometrics is designed to give you more 42 | // granular metrics that allow you to dig into the internals of your application 43 | // before even reaching for logs or traces. 44 | // 45 | // Try hovering over the function name to see the graph links and pay special attention 46 | // to the links for the functions _called by this function_. 47 | // You can also hover over the load_details_from_database function to see the graph links for that function. 48 | #[autometrics(objective = API_SLO)] 49 | pub async fn create_user( 50 | State(database): State, 51 | Json(payload): Json, 52 | ) -> Result, ApiError> { 53 | let user = User { 54 | id: 1337, 55 | username: payload.username, 56 | }; 57 | 58 | database.load_details().await?; 59 | 60 | sleep_random_duration().await; 61 | 62 | Ok(Json(user)) 63 | } 64 | 65 | // The input to our `create_user` handler 66 | #[derive(Serialize, Deserialize)] 67 | pub struct CreateUser { 68 | pub username: String, 69 | } 70 | 71 | // The output to our `create_user` handler 72 | #[derive(Serialize)] 73 | pub struct User { 74 | pub id: u64, 75 | pub username: String, 76 | } 77 | -------------------------------------------------------------------------------- /examples/full-api/src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::routes::CreateUser; 2 | use rand::{thread_rng, Rng}; 3 | use std::time::Duration; 4 | use tokio::time::sleep; 5 | 6 | /// Make some random API calls to generate data that we can see in the graphs 7 | pub async fn generate_random_traffic() { 8 | let client = reqwest::Client::new(); 9 | loop { 10 | let request_type = thread_rng().gen_range(0..3); 11 | let sleep_duration = Duration::from_millis(thread_rng().gen_range(10..50)); 12 | match request_type { 13 | 0 => { 14 | let _ = client.get("http://localhost:3000").send().await; 15 | } 16 | 1 => { 17 | let _ = client 18 | .post("http://localhost:3000/users") 19 | .json(&CreateUser { 20 | username: "test".to_string(), 21 | }) 22 | .send() 23 | .await; 24 | } 25 | 2 => { 26 | let _ = reqwest::get("http://localhost:3000/random-error").await; 27 | } 28 | _ => unreachable!(), 29 | } 30 | sleep(sleep_duration).await 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/grpc-http/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-grpc-http" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | autometrics = { path = "../../autometrics", features = ["prometheus-exporter"] } 9 | prost = "0.12" 10 | tokio = { version = "1", features = ["full"] } 11 | tonic = "0.10" 12 | tonic-health = "0.10" 13 | warp = "0.3" 14 | 15 | [build-dependencies] 16 | tonic-build = "0.10" 17 | -------------------------------------------------------------------------------- /examples/grpc-http/README.md: -------------------------------------------------------------------------------- 1 | # gRPC service built with Tonic, http server build with warp, and Instrumented with Autometrics 2 | 3 | This code example has been adapted and modified from a Blog post by Mies Hernandez van Leuffen: [Adding observability to Rust gRPC services using Tonic and Autometrics](https://autometrics.dev/blog/adding-observability-to-rust-grpc-services-using-tonic-and-autometrics). 4 | 5 | ## Overview 6 | 7 | This example shows how to: 8 | * Add observability to a gRPC services 9 | * Add a http service 10 | * Start both, the grpc and http server 11 | * Add a graceful shutdown to both, grpc and http server 12 | * Closes a DB connection during graceful shutdown 13 | 14 | ### Install the protobuf compiler 15 | 16 | The protobuf compiler (protoc) compiles protocol buffers into Rust code. 17 | Cargo will call protoc automatically during the build process, but you will 18 | get an error when protoc is not installed. Therefore, ensure protoc is installed. 19 | 20 | The recommended installation for macOS is via [Homebrew](https://brew.sh/): 21 | 22 | ```bash 23 | brew install protobuf 24 | ``` 25 | Check if the installation worked correctly: 26 | 27 | ```bash 28 | protoc --version 29 | ``` 30 | 31 | ## Local Observability Development 32 | 33 | The easiest way to get up and running with this application is to clone the repo and get a local Prometheus setup using the [Autometrics CLI](https://github.com/autometrics-dev/am). 34 | 35 | Read more about Autometrics in Rust [here](https://github.com/autometrics-dev/autometrics-rs) and general docs [here](https://docs.autometrics.dev/). 36 | 37 | 38 | ### Install the Autometrics CLI 39 | 40 | The recommended installation for macOS is via [Homebrew](https://brew.sh/): 41 | 42 | ``` 43 | brew install autometrics-dev/tap/am 44 | ``` 45 | 46 | Alternatively, you can download the latest version from the [releases page](https://github.com/autometrics-dev/am/releases) 47 | 48 | Spin up local Prometheus and start scraping your application that listens on port :8080. 49 | 50 | ``` 51 | am start :8080 52 | ``` 53 | 54 | If you now inspect the Autometrics explorer on `http://localhost:6789` you will see your metrics. However, upon first start, all matrics are 55 | empty because no request has been sent yet. 56 | 57 | Now you can test your endpoints and generate some traffic and refresh the autometrics explorer to see you metrics. 58 | 59 | ### Starting the Service 60 | 61 | ```bash 62 | cargo run 63 | ``` 64 | 65 | Expected output: 66 | 67 | ``` 68 | Started gRPC server on port 50051 69 | Started metrics on port 8080 70 | Explore autometrics at http://127.0.0.1:6789 71 | ``` 72 | 73 | ### Stopping the Service 74 | 75 | You can stop the service either via ctrl-c ore by sending a SIGTERM signal to kill the process. This has been implemented for Windows, Linux, Mac, and should also work on Docker and Kubernetes. 76 | 77 | On Windows, Linux, or Mac, just hit Ctrl-C 78 | 79 | Alternatively, you can send a SIGTERM signal from another process 80 | using the kill command on Linux or Mac. 81 | 82 | In a second terminal, run 83 | 84 | ```bash 85 | ps | grep grpc-http 86 | ``` 87 | 88 | Sample output: 89 | 90 | ``` 91 | 73014 ttys002 0:00.25 /Users/.../autometrics-rs/target/debug/grpc-http 92 | ``` 93 | 94 | In this example, the service runs on PID 73014. Let's send a sigterm signal to shutdown the service. On you system, a different PID will be returned so please use that one instead. 95 | 96 | ```bash 97 | kill 73014 98 | ``` 99 | 100 | Expected output: 101 | 102 | ``` 103 | Received SIGTERM 104 | DB connection closed 105 | gRPC shutdown complete 106 | http shutdown complete 107 | ``` 108 | 109 | 110 | ## Testing the GRPC endpoints 111 | 112 | Easiest way to test the endpoints is with `grpcurl` (`brew install grpcurl`). 113 | 114 | ```bash 115 | grpcurl -plaintext -import-path ./proto -proto job.proto -d '{"name": "Tonic"}' 'localhost:50051' job.JobRunner.SendJob 116 | ``` 117 | 118 | returns 119 | 120 | ``` 121 | { 122 | "message": "Hello Tonic!" 123 | } 124 | ``` 125 | 126 | Getting the list of jobs (currently hardcoded to return one job) 127 | 128 | ```bash 129 | grpcurl -plaintext -import-path ./proto -proto job.proto -d '{}' 'localhost:50051' job.JobRunner.ListJobs 130 | ``` 131 | 132 | returns: 133 | 134 | ``` 135 | { 136 | "job": [ 137 | { 138 | "id": 1, 139 | "name": "test" 140 | } 141 | ] 142 | } 143 | ``` 144 | 145 | ## Viewing the metrics 146 | 147 | When you inspect the Autometrics explorer on `http://localhost:6789` you will see your metrics and SLOs. The explorer shows four tabs: 148 | 149 | 1) Dashboard: Aggregated overview of all metrics 150 | 2) Functions: Detailed metrics for each instrumented API function 151 | 3) SLO's: Service Level Agreements for each instrumented API function 152 | 4) Alerts: Notifications of violated SLO's or any other anomaly. 153 | 154 | -------------------------------------------------------------------------------- /examples/grpc-http/build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | tonic_build::compile_protos("proto/job.proto")?; 3 | Ok(()) 4 | } 5 | -------------------------------------------------------------------------------- /examples/grpc-http/proto/job.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package job; 3 | 4 | service JobRunner { 5 | rpc SendJob (JobRequest) returns (JobReply); 6 | rpc ListJobs (Empty) returns (JobList); 7 | } 8 | 9 | message Empty {} 10 | 11 | message Job { 12 | int32 id = 1; 13 | string name = 2; 14 | 15 | enum Status { 16 | NOT_STARTED = 0; 17 | RUNNING = 1; 18 | FINISHED = 2; 19 | } 20 | } 21 | 22 | message JobRequest { 23 | string name = 1; 24 | } 25 | 26 | message JobReply { 27 | string message = 1; 28 | 29 | enum Status { 30 | NOT_STARTED = 0; 31 | RUNNING = 1; 32 | FINISHED = 2; 33 | } 34 | } 35 | 36 | message JobList { 37 | repeated Job job = 1; 38 | } 39 | -------------------------------------------------------------------------------- /examples/grpc-http/src/db_manager.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Error; 2 | 3 | // Clone is required for the `tokio::signal::unix::SignalKind::terminate()` handler 4 | // Sometimes, you can't derive clone, then you have to wrap the DBManager in an Arc or Arc 5 | #[derive(Debug, Default, Clone, Copy)] 6 | pub struct DBManager { 7 | // Put your DB client here. For example: 8 | // db: rusqlite, 9 | } 10 | 11 | impl DBManager { 12 | pub fn new() -> DBManager { 13 | DBManager { 14 | // Put your database client here. For example: 15 | // db: rusqlite::Connection::open(":memory:").unwrap(); 16 | } 17 | } 18 | 19 | pub async fn connect_to_db(&self) -> Result<(), Error> { 20 | Ok(()) 21 | } 22 | 23 | pub async fn close_db(&self) -> Result<(), Error> { 24 | Ok(()) 25 | } 26 | 27 | pub async fn query_table(&self) -> Result<(), Error> { 28 | println!("Query table"); 29 | Ok(()) 30 | } 31 | 32 | pub async fn write_into_table(&self) -> Result<(), Error> { 33 | println!("Write into table"); 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/grpc-http/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use tonic::transport::Server as TonicServer; 3 | use warp::Filter; 4 | use warp::http::StatusCode; 5 | 6 | use autometrics::prometheus_exporter; 7 | use server::MyJobRunner; 8 | 9 | use crate::server::job::job_runner_server::JobRunnerServer; 10 | 11 | mod db_manager; 12 | mod server; 13 | mod shutdown; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<(), Box> { 17 | // Set up prometheus metrics exporter 18 | prometheus_exporter::init(); 19 | 20 | // Set up two different ports for gRPC and HTTP 21 | let grpc_addr = "127.0.0.1:50051" 22 | .parse() 23 | .expect("Failed to parse gRPC address"); 24 | let web_addr: SocketAddr = "127.0.0.1:8080" 25 | .parse() 26 | .expect("Failed to parse web address"); 27 | 28 | // Build new DBManager that connects to the database 29 | let dbm = db_manager::DBManager::new(); 30 | // Connect to the database 31 | dbm.connect_to_db() 32 | .await 33 | .expect("Failed to connect to database"); 34 | 35 | // gRPC server with DBManager 36 | let grpc_svc = JobRunnerServer::new(MyJobRunner::new(dbm)); 37 | 38 | // Sigint signal handler that closes the DB connection upon shutdown 39 | let signal = shutdown::grpc_sigint(dbm.clone()); 40 | 41 | // Construct health service for gRPC server 42 | let (mut health_reporter, health_svc) = tonic_health::server::health_reporter(); 43 | health_reporter 44 | .set_serving::>() 45 | .await; 46 | 47 | // Build gRPC server with health service and signal sigint handler 48 | let grpc_server = TonicServer::builder() 49 | .add_service(grpc_svc) 50 | .add_service(health_svc) 51 | .serve_with_shutdown(grpc_addr, signal); 52 | 53 | // Build http /metrics endpoint 54 | let routes = warp::get() 55 | .and(warp::path("metrics")) 56 | .map(|| prometheus_exporter::encode_to_string().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)); 57 | 58 | // Build http web server 59 | let (_, web_server) = 60 | warp::serve(routes).bind_with_graceful_shutdown(web_addr, shutdown::http_sigint()); 61 | 62 | // Create handler for each server 63 | // https://github.com/hyperium/tonic/discussions/740 64 | let grpc_handle = tokio::spawn(grpc_server); 65 | let grpc_web_handle = tokio::spawn(web_server); 66 | 67 | // Join all servers together and start the the main loop 68 | print_start(&web_addr, &grpc_addr); 69 | let _ = tokio::try_join!(grpc_handle, grpc_web_handle) 70 | .expect("Failed to start gRPC and http server"); 71 | 72 | Ok(()) 73 | } 74 | 75 | fn print_start(web_addr: &SocketAddr, grpc_addr: &SocketAddr) { 76 | println!(); 77 | println!("Started gRPC server on port {:?}", grpc_addr.port()); 78 | println!("Started metrics on port {:?}", web_addr.port()); 79 | println!("Stop service with Ctrl+C"); 80 | println!(); 81 | println!("Explore autometrics at http://127.0.0.1:6789"); 82 | println!(); 83 | } 84 | -------------------------------------------------------------------------------- /examples/grpc-http/src/server.rs: -------------------------------------------------------------------------------- 1 | use crate::db_manager::DBManager; 2 | use autometrics::autometrics; 3 | use job::job_runner_server::JobRunner; 4 | use job::{Empty, JobList, JobReply, JobRequest}; 5 | use tonic::{Request, Response, Status}; 6 | 7 | use autometrics::objectives::{Objective, ObjectiveLatency, ObjectivePercentile}; 8 | 9 | // Add autometrics Service-Level Objectives (SLOs) 10 | // https://docs.autometrics.dev/rust/adding-alerts-and-slos 11 | const API_SLO: Objective = Objective::new("job_runner_api") 12 | // We expect 99.9% of all requests to succeed. 13 | .success_rate(ObjectivePercentile::P99_9) 14 | // We expect 99% of all latencies to be below 250ms. 15 | .latency(ObjectiveLatency::Ms250, ObjectivePercentile::P99); 16 | // Autometrics raises an alert whenever any of the SLO objectives fail. 17 | 18 | pub mod job { 19 | tonic::include_proto!("job"); 20 | } 21 | 22 | #[derive(Debug, Default)] 23 | pub struct MyJobRunner { 24 | db_manager: DBManager, 25 | } 26 | 27 | impl MyJobRunner { 28 | pub fn new(db_manager: DBManager) -> Self { 29 | Self { db_manager } 30 | } 31 | } 32 | 33 | // Instrument all API functions of the implementation of JobRunner via macro. 34 | // https://docs.autometrics.dev/rust/quickstart 35 | // Attach the SLO to each API function. 36 | // 37 | // Notice, all API functions are instrumented with the same SLO. 38 | // If you want to have different SLOs for different API functions, 39 | // You have to create a separate SLO for each API function and instrument 40 | // each API function individually instead of using the macro on trait level. 41 | // Docs https://docs.autometrics.dev/rust/adding-alerts-and-slos 42 | #[autometrics(objective = API_SLO)] 43 | #[tonic::async_trait] 44 | impl JobRunner for MyJobRunner { 45 | async fn send_job(&self, request: Request) -> Result, Status> { 46 | println!("Got a request: {:?}", request); 47 | 48 | // Write into the mock database 49 | self.db_manager 50 | .write_into_table() 51 | .await 52 | .expect("Failed to query database"); 53 | 54 | let reply = job::JobReply { 55 | message: format!("Hello {}!", request.into_inner().name).into(), 56 | }; 57 | 58 | Ok(Response::new(reply)) 59 | } 60 | 61 | async fn list_jobs(&self, request: Request) -> Result, Status> { 62 | println!("Got a request: {:?}", request); 63 | 64 | // Query the mock database 65 | self.db_manager 66 | .query_table() 67 | .await 68 | .expect("Failed to query database"); 69 | 70 | let reply = job::JobList { 71 | job: vec![job::Job { 72 | id: 1, 73 | name: "test".into(), 74 | }], 75 | }; 76 | 77 | Ok(Response::new(reply)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/grpc-http/src/shutdown.rs: -------------------------------------------------------------------------------- 1 | use crate::db_manager::DBManager; 2 | 3 | // Signal sender is non-clonable therefore we need to create a new one for each server. 4 | // https://github.com/rust-lang/futures-rs/issues/1971 5 | pub(crate) async fn http_sigint() { 6 | wait_for_signal().await; 7 | 8 | println!("http shutdown complete"); 9 | } 10 | 11 | pub(crate) async fn grpc_sigint(dbm: DBManager) { 12 | wait_for_signal().await; 13 | 14 | // Shutdown the DB connection. 15 | dbm.close_db() 16 | .await 17 | .expect("Failed to close database connection"); 18 | println!("DB connection closed"); 19 | 20 | println!("gRPC shutdown complete"); 21 | } 22 | 23 | /// Registers signal handlers and waits for a signal that indicates a shutdown request. 24 | pub(crate) async fn wait_for_signal() { 25 | wait_for_signal_impl().await 26 | } 27 | 28 | /// Waits for a signal that requests a graceful shutdown, like SIGTERM, SIGINT (Ctrl-C), or SIGQUIT. 29 | #[cfg(unix)] 30 | async fn wait_for_signal_impl() { 31 | use tokio::signal::unix::{signal, SignalKind}; 32 | 33 | // Docs: https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html 34 | let mut signal_terminate = signal(SignalKind::terminate()).unwrap(); 35 | let mut signal_interrupt = signal(SignalKind::interrupt()).unwrap(); 36 | let mut signal_quit = signal(SignalKind::quit()).unwrap(); 37 | 38 | // https://docs.rs/tokio/latest/tokio/macro.select.html 39 | tokio::select! { 40 | _ = signal_terminate.recv() => println!("Received SIGTERM"), 41 | _ = signal_interrupt.recv() => println!("Received SIGINT"), 42 | _ = signal_quit.recv() => println!("Received SIGQUIT"), 43 | } 44 | } 45 | 46 | /// Waits for a signal that requests a graceful shutdown, Ctrl-C (SIGINT). 47 | #[cfg(windows)] 48 | async fn wait_for_signal_impl() { 49 | use tokio::signal::windows; 50 | 51 | // Docs: https://learn.microsoft.com/en-us/windows/console/handlerroutine 52 | let mut signal_c = windows::ctrl_c().unwrap(); 53 | let mut signal_break = windows::ctrl_break().unwrap(); 54 | let mut signal_close = windows::ctrl_close().unwrap(); 55 | let mut signal_shutdown = windows::ctrl_shutdown().unwrap(); 56 | 57 | // https://docs.rs/tokio/latest/tokio/macro.select.html 58 | tokio::select! { 59 | _ = signal_c.recv() => println!("Received CTRL_C."), 60 | _ = signal_break.recv() => println!("Received CTRL_BREAK."), 61 | _ = signal_close.recv() => println!("Received CTRL_CLOSE."), 62 | _ = signal_shutdown.recv() => println!("Received CTRL_SHUTDOWN."), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/opentelemetry-push-custom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-opentelemetry-push-custom" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | autometrics = { path = "../../autometrics", features = ["opentelemetry-0_24"] } 9 | autometrics-example-util = { path = "../util" } 10 | # Note that the version of the opentelemetry crate MUST match 11 | # the version used by autometrics 12 | opentelemetry = { version = "0.24", features = ["metrics"] } 13 | opentelemetry_sdk = { version = "0.24", features = ["metrics", "rt-tokio"] } 14 | opentelemetry-otlp = { version = "0.17", features = ["tonic", "metrics"] } 15 | opentelemetry-semantic-conventions = { version = "0.16.0" } 16 | tokio = { version = "1", features = ["full"] } 17 | -------------------------------------------------------------------------------- /examples/opentelemetry-push-custom/README.md: -------------------------------------------------------------------------------- 1 | # Autometrics + OTLP push controller (custom) 2 | 3 | This example demonstrates how you can push autometrics via OTLP gRPC protocol to the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) or another OTel-compatible solution 4 | without using the Autometrics provided interface. 5 | 6 | ## Running the example 7 | 8 | ### Start a basic OTEL-Collector 9 | 10 | You can use the [`otel-collector-config.yml`](./otel-collector-config.yml) file to start an otel-collector container that listens on 0.0.0.0:4317 for incoming otlp-gRPC traffic, and exports the metrics it receives to stdout. 11 | 12 | Run the following command in a second terminal to start a container in interactive mode: 13 | 14 | ```bash 15 | docker run -it --name otel-col \ 16 | -p 4317:4317 -p 13133:13133 \ 17 | -v $PWD/otel-collector-config.yml:/etc/otelcol/config.yaml \ 18 | otel/opentelemetry-collector:latest 19 | ``` 20 | 21 | You should see the collector initialization output, that should end with something like: 22 | 23 | ```text 24 | ... 25 | 2023-06-07T15:56:42.617Z info otlpreceiver@v0.75.0/otlp.go:94 Starting GRPC server {"kind": "receiver", "name": "otlp", "data_type": "metrics", "endpoint": "0.0.0.0:4317"} 26 | 2023-06-07T15:56:42.618Z info service/service.go:146 Everything is ready. Begin running and processing data. 27 | ``` 28 | 29 | ### Execute example code 30 | 31 | Then come back on your primary shell and run this example: 32 | 33 | ```shell 34 | cargo run -p example-opentelemetry-push-custom 35 | ``` 36 | 37 | ### Check the output 38 | 39 | On the stdout of the terminal where you started the opentelemetry-collector container, you should see the metrics generated by autometrics macro being pushed every 10 seconds since example exit, like: 40 | 41 | ```text 42 | ... 43 | Metric #0 44 | Descriptor: 45 | -> Name: function.calls 46 | -> Description: Autometrics counter for tracking function calls 47 | -> Unit: 48 | -> DataType: Sum 49 | -> IsMonotonic: true 50 | -> AggregationTemporality: Cumulative 51 | NumberDataPoints #0 52 | Data point attributes: 53 | -> caller: Str() 54 | -> function: Str(do_stuff) 55 | -> module: Str(example_opentelemetry_push) 56 | StartTimestamp: 2023-06-07 16:01:08.549300623 +0000 UTC 57 | Timestamp: 2023-06-07 16:01:48.551531429 +0000 UTC 58 | Value: 10 59 | Metric #1 60 | Descriptor: 61 | -> Name: build_info 62 | -> Description: Autometrics info metric for tracking software version and build details 63 | -> Unit: 64 | -> DataType: Sum 65 | -> IsMonotonic: false 66 | -> AggregationTemporality: Cumulative 67 | NumberDataPoints #0 68 | Data point attributes: 69 | -> branch: Str() 70 | -> commit: Str() 71 | -> version: Str(0.0.0) 72 | StartTimestamp: 2023-06-07 16:01:08.549300623 +0000 UTC 73 | Timestamp: 2023-06-07 16:01:48.551531429 +0000 UTC 74 | Value: 1.000000 75 | Metric #2 76 | Descriptor: 77 | -> Name: function.calls.duration 78 | -> Description: Autometrics histogram for tracking function call duration 79 | -> Unit: 80 | -> DataType: Sum 81 | -> IsMonotonic: false 82 | -> AggregationTemporality: Cumulative 83 | NumberDataPoints #0 84 | Data point attributes: 85 | -> function: Str(do_stuff) 86 | -> module: Str(example_opentelemetry_push) 87 | StartTimestamp: 2023-06-07 16:01:08.549300623 +0000 UTC 88 | Timestamp: 2023-06-07 16:01:48.551531429 +0000 UTC 89 | Value: 0.000122 90 | {"kind": "exporter", "data_type": "metrics", "name": "logging"} 91 | ... 92 | ``` 93 | 94 | ### Cleanup 95 | 96 | In the end, to stop the opentelemetry collector container just hit `^C`. 97 | 98 | Then delete the container with 99 | 100 | ```bash 101 | docker rm otel-col 102 | ``` 103 | 104 | ## OpenTelemetry Metrics Push Controller 105 | 106 | The metric push controller is implemented as from this [example](https://github.com/open-telemetry/opentelemetry-rust/blob/f20c9b40547ee20b6ec99414bb21abdd3a54d99b/examples/basic-otlp/src/main.rs#L35-L52) from `opentelemetry-rust` crate. 107 | -------------------------------------------------------------------------------- /examples/opentelemetry-push-custom/otel-collector-config.yml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | endpoint: 0.0.0.0:4317 6 | 7 | exporters: 8 | logging: 9 | loglevel: debug 10 | 11 | processors: 12 | batch: 13 | 14 | service: 15 | pipelines: 16 | metrics: 17 | receivers: [otlp] 18 | processors: [] 19 | exporters: [logging] 20 | -------------------------------------------------------------------------------- /examples/opentelemetry-push-custom/src/main.rs: -------------------------------------------------------------------------------- 1 | use autometrics::autometrics; 2 | use autometrics_example_util::sleep_random_duration; 3 | use opentelemetry::metrics::MetricsError; 4 | use opentelemetry::sdk::metrics::MeterProvider; 5 | use opentelemetry::{runtime, Context}; 6 | use opentelemetry_otlp::{ExportConfig, WithExportConfig}; 7 | use std::error::Error; 8 | use std::time::Duration; 9 | use tokio::time::sleep; 10 | 11 | fn init_metrics() -> Result { 12 | let export_config = ExportConfig { 13 | endpoint: "http://localhost:4317".to_string(), 14 | ..ExportConfig::default() 15 | }; 16 | let push_interval = Duration::from_secs(1); 17 | opentelemetry_otlp::new_pipeline() 18 | .metrics(runtime::Tokio) 19 | .with_exporter( 20 | opentelemetry_otlp::new_exporter() 21 | .tonic() 22 | .with_export_config(export_config), 23 | ) 24 | .with_period(push_interval) 25 | .build() 26 | } 27 | 28 | #[autometrics] 29 | async fn do_stuff() { 30 | println!("Doing stuff..."); 31 | sleep_random_duration().await; 32 | } 33 | 34 | #[tokio::main] 35 | async fn main() -> Result<(), Box> { 36 | let meter_provider = init_metrics()?; 37 | let cx = Context::current(); 38 | 39 | for _ in 0..100 { 40 | do_stuff().await; 41 | } 42 | 43 | println!("Waiting so that we could see metrics going down..."); 44 | sleep(Duration::from_secs(10)).await; 45 | meter_provider.force_flush(&cx)?; 46 | 47 | meter_provider.shutdown()?; 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /examples/opentelemetry-push/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-opentelemetry-push" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | autometrics = { path = "../../autometrics", features = ["opentelemetry-0_24", "otel-push-exporter-http", "otel-push-exporter-tokio"] } 9 | autometrics-example-util = { path = "../util" } 10 | tokio = { version = "1", features = ["full"] } 11 | -------------------------------------------------------------------------------- /examples/opentelemetry-push/README.md: -------------------------------------------------------------------------------- 1 | # Autometrics + OTLP push controller 2 | 3 | This example demonstrates how you can push autometrics via OTLP HTTP and GRPC protocol 4 | to the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) or another OTel-compatible solution. It 5 | uses the Autometrics provided wrapper to achieve this. 6 | 7 | ## Running the example 8 | 9 | ### Start a basic OTEL-Collector 10 | 11 | You can use the [`otel-collector-config.yml`](./otel-collector-config.yml) file to start an otel-collector container that listens on 0.0.0.0:4317 for incoming otlp-gRPC traffic as well as on 0.0.0.0:4318 for incoming otlp-http traffic, and exports the metrics it receives to stdout. 12 | 13 | Run the following command in a second terminal to start a container in interactive mode: 14 | 15 | ```bash 16 | docker run -it --name otel-col \ 17 | -p 4317:4317 -p 13133:13133 \ 18 | -v $PWD/otel-collector-config.yml:/etc/otelcol/config.yaml \ 19 | otel/opentelemetry-collector-contrib:latest 20 | ``` 21 | 22 | You should see the collector initialization output, that should end with something like: 23 | 24 | ```text 25 | ... 26 | 2023-06-07T15:56:42.617Z info otlpreceiver@v0.75.0/otlp.go:94 Starting GRPC server {"kind": "receiver", "name": "otlp", "data_type": "metrics", "endpoint": "0.0.0.0:4317"} 27 | 2023-06-07T15:56:42.618Z info service/service.go:146 Everything is ready. Begin running and processing data. 28 | ``` 29 | 30 | ### Execute example code 31 | 32 | Then come back on your primary shell and run this example: 33 | 34 | ```shell 35 | cargo run -p example-opentelemetry-push 36 | ``` 37 | 38 | ### Check the output 39 | 40 | On the stdout of the terminal where you started the opentelemetry-collector container, you should see the metrics generated by autometrics macro being pushed every 10 seconds since example exit, like: 41 | 42 | ```text 43 | ... 44 | Metric #0 45 | Descriptor: 46 | -> Name: function.calls 47 | -> Description: Autometrics counter for tracking function calls 48 | -> Unit: 49 | -> DataType: Sum 50 | -> IsMonotonic: true 51 | -> AggregationTemporality: Cumulative 52 | NumberDataPoints #0 53 | Data point attributes: 54 | -> caller: Str() 55 | -> function: Str(do_stuff) 56 | -> module: Str(example_opentelemetry_push) 57 | StartTimestamp: 2023-06-07 16:01:08.549300623 +0000 UTC 58 | Timestamp: 2023-06-07 16:01:48.551531429 +0000 UTC 59 | Value: 10 60 | Metric #1 61 | Descriptor: 62 | -> Name: build_info 63 | -> Description: Autometrics info metric for tracking software version and build details 64 | -> Unit: 65 | -> DataType: Sum 66 | -> IsMonotonic: false 67 | -> AggregationTemporality: Cumulative 68 | NumberDataPoints #0 69 | Data point attributes: 70 | -> branch: Str() 71 | -> commit: Str() 72 | -> version: Str(0.0.0) 73 | StartTimestamp: 2023-06-07 16:01:08.549300623 +0000 UTC 74 | Timestamp: 2023-06-07 16:01:48.551531429 +0000 UTC 75 | Value: 1.000000 76 | Metric #2 77 | Descriptor: 78 | -> Name: function.calls.duration 79 | -> Description: Autometrics histogram for tracking function call duration 80 | -> Unit: 81 | -> DataType: Sum 82 | -> IsMonotonic: false 83 | -> AggregationTemporality: Cumulative 84 | NumberDataPoints #0 85 | Data point attributes: 86 | -> function: Str(do_stuff) 87 | -> module: Str(example_opentelemetry_push) 88 | StartTimestamp: 2023-06-07 16:01:08.549300623 +0000 UTC 89 | Timestamp: 2023-06-07 16:01:48.551531429 +0000 UTC 90 | Value: 0.000122 91 | {"kind": "exporter", "data_type": "metrics", "name": "logging"} 92 | ... 93 | ``` 94 | 95 | ### Cleanup 96 | 97 | In the end, to stop the opentelemetry collector container just hit `^C`. 98 | 99 | Then delete the container with 100 | 101 | ```bash 102 | docker rm otel-col 103 | ``` 104 | -------------------------------------------------------------------------------- /examples/opentelemetry-push/otel-collector-config.yml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | http: 5 | endpoint: 0.0.0.0:4318 6 | grpc: 7 | endpoint: 0.0.0.0:4317 8 | 9 | exporters: 10 | logging: 11 | loglevel: debug 12 | 13 | processors: 14 | batch: 15 | 16 | service: 17 | pipelines: 18 | metrics: 19 | receivers: [otlp] 20 | processors: [] 21 | exporters: [logging] 22 | -------------------------------------------------------------------------------- /examples/opentelemetry-push/src/main.rs: -------------------------------------------------------------------------------- 1 | use autometrics::{autometrics, otel_push_exporter}; 2 | use autometrics_example_util::sleep_random_duration; 3 | use std::error::Error; 4 | use std::time::Duration; 5 | use tokio::time::sleep; 6 | 7 | #[autometrics] 8 | async fn do_stuff() { 9 | println!("Doing stuff..."); 10 | sleep_random_duration().await; 11 | } 12 | 13 | #[tokio::main] 14 | async fn main() -> Result<(), Box> { 15 | // NOTICE: the variable gets assigned to `_meter_provider` instead of just `_`, as the later case 16 | // would cause it to be dropped immediately and thus shut down. 17 | let _meter_provider = otel_push_exporter::init_http("http://0.0.0.0:4318")?; 18 | // or: otel_push_exporter::init_grpc("http://0.0.0.0:4317"); 19 | 20 | for _ in 0..100 { 21 | do_stuff().await; 22 | } 23 | 24 | println!("Waiting so that we could see metrics going down..."); 25 | sleep(Duration::from_secs(10)).await; 26 | 27 | // no need to call `.shutdown` as the returned `OtelMeterProvider` has a `Drop` implementation 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /examples/util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autometrics-example-util" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dependencies] 8 | rand = "0.8" 9 | tokio = { version = "1", features = ["time"] } 10 | -------------------------------------------------------------------------------- /examples/util/gitpod/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full:latest 2 | 3 | # Install Prometheus 4 | RUN bash -c "brew install prometheus" 5 | 6 | # Install a recent rust version 7 | RUN bash -c "rustup toolchain install 1.65.0" 8 | # Install 'cargo watch' for better dev experience 9 | RUN bash -c "cargo install cargo-watch" 10 | -------------------------------------------------------------------------------- /examples/util/prometheus.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: example-api-metrics 3 | metrics_path: /metrics 4 | static_configs: 5 | - targets: ['localhost:3000'] 6 | # For a real deployment, you would want the scrape interval to be 7 | # much longer but this is just for demo purposes and we want the 8 | # data to show up quickly 9 | scrape_interval: 200ms 10 | rule_files: 11 | - "../../autometrics.rules.yml" 12 | -------------------------------------------------------------------------------- /examples/util/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rand::{thread_rng, Rng}; 2 | use std::process::{Child, Command, Stdio}; 3 | use std::{io::ErrorKind, time::Duration}; 4 | use tokio::time::sleep; 5 | 6 | const PROMETHEUS_CONFIG_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/prometheus.yml"); 7 | 8 | pub struct ChildGuard(Child); 9 | 10 | impl Drop for ChildGuard { 11 | fn drop(&mut self) { 12 | match self.0.kill() { 13 | Ok(_) => eprintln!("Stopped Prometheus server"), 14 | Err(_) => eprintln!("Failed to stop Prometheus server"), 15 | } 16 | } 17 | } 18 | 19 | pub fn run_prometheus(enable_exemplars: bool) -> ChildGuard { 20 | let mut args = vec!["--config.file", PROMETHEUS_CONFIG_PATH]; 21 | if enable_exemplars { 22 | args.push("--enable-feature=exemplar-storage"); 23 | } 24 | 25 | match Command::new("prometheus") 26 | .args(&args) 27 | .stdout(Stdio::null()) 28 | .stderr(Stdio::null()) 29 | .spawn() 30 | { 31 | Err(err) if err.kind() == ErrorKind::NotFound => { 32 | panic!("Failed to start prometheus (do you have the prometheus binary installed and in your path?)"); 33 | } 34 | Err(err) => { 35 | panic!("Failed to start prometheus: {}", err); 36 | } 37 | Ok(child) => { 38 | eprintln!( 39 | "Running Prometheus on port 9090 (using config file: {PROMETHEUS_CONFIG_PATH})", 40 | ); 41 | if enable_exemplars { 42 | eprintln!( 43 | "Exemplars are enabled (using the flag: --enable-feature=exemplar-storage)" 44 | ); 45 | } 46 | ChildGuard(child) 47 | } 48 | } 49 | } 50 | 51 | pub async fn sleep_random_duration() { 52 | let sleep_duration = Duration::from_millis(thread_rng().gen_range(0..300)); 53 | sleep(sleep_duration).await; 54 | } 55 | --------------------------------------------------------------------------------