├── .github └── workflows │ └── base.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── Cargo.toml ├── base-metric-layer-example │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── builder-example │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── endpoint-type-example │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── exporter-statsd-example │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs └── simple-example │ ├── .gitignore │ ├── Cargo.toml │ └── src │ └── main.rs ├── src ├── builder.rs ├── lib.rs ├── lifecycle │ ├── body.rs │ ├── future.rs │ ├── layer.rs │ ├── mod.rs │ └── service.rs └── utils.rs └── tests ├── base.rs ├── common.rs ├── prefix.rs └── snapshots ├── base__metric_handle_rendered_correctly.snap └── prefix__metric_handle_rendered_correctly_with_prefix.snap /.github/workflows/base.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | check: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | pwd: 19 | - . 20 | - examples 21 | 22 | steps: 23 | - uses: actions/checkout@master 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: nightly 27 | override: true 28 | profile: minimal 29 | components: clippy, rustfmt 30 | - uses: Swatinem/rust-cache@v2 31 | with: 32 | key: ${{ matrix.pwd }} 33 | workspaces: ${{ matrix.pwd }} 34 | - name: clippy 35 | working-directory: ${{ matrix.pwd }} 36 | run: | 37 | cargo clippy --all --all-targets --all-features 38 | - name: rustfmt 39 | working-directory: ${{ matrix.pwd }} 40 | run: | 41 | cargo fmt --all -- --check 42 | check-docs: 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - uses: actions/checkout@master 47 | - uses: actions-rs/toolchain@v1 48 | with: 49 | toolchain: stable 50 | override: true 51 | profile: minimal 52 | - uses: Swatinem/rust-cache@v2 53 | - name: cargo doc 54 | env: 55 | RUSTDOCFLAGS: "-D rustdoc::broken-intra-doc-links" 56 | run: cargo doc --all-features --no-deps 57 | 58 | test: 59 | needs: check 60 | 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - uses: actions/checkout@master 65 | - uses: actions-rs/toolchain@v1 66 | with: 67 | toolchain: stable 68 | override: true 69 | profile: minimal 70 | - uses: Swatinem/rust-cache@v2 71 | - name: Run tests 72 | uses: actions-rs/cargo@v1 73 | with: 74 | command: test 75 | args: --all --all-features --all-targets 76 | 77 | test-docs: 78 | needs: check 79 | 80 | runs-on: ubuntu-latest 81 | 82 | steps: 83 | - uses: actions/checkout@master 84 | - uses: actions-rs/toolchain@v1 85 | with: 86 | toolchain: stable 87 | override: true 88 | profile: minimal 89 | - uses: Swatinem/rust-cache@v2 90 | - name: Run doc tests 91 | uses: actions-rs/cargo@v1 92 | with: 93 | command: test 94 | args: --all-features --doc 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | examples/target 4 | examples/Cargo.lock 5 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | # [Unreleased] 6 | 7 | # [0.8.0] 8 | 9 | ### Changed 10 | 11 | - Compatibility with `axum = "0.8"`. This also updates `matchit` to `0.8`, changing how group patterns are described: 12 | for example, `with_group_patterns_as("/foo", &["/foo/:bar"])` needs to be changed to `with_group_patterns_as("/foo", &["/foo/{bar}"])`. 13 | The metrics values are also impacted: for example, the value `"/foo/:bar"` is now `"/foo/{bar}"`. 14 | This change bumps MSRV to 1.75. [\#69] 15 | - Disable the default features in `metrics-exporter-prometheus` to skip the binding of port 9000, and the upkeep task is manually spawned. [\#75] 16 | - Add a new feature `http-listener` to enable that exact feature in `metrics-exporter-prometheus`. [\#79] 17 | - Replace the `once_cell` dependency with `std::sync::OnceLock` [\#78] 18 | 19 | ### Fixed 20 | 21 | - Fixed the long-standing pending requests metric leak. [\#74] 22 | - Removed the sideeffect of binding port 9000. [\#75] 23 | 24 | # [0.7.0] - 2024-07-20 25 | 26 | ### Changed 27 | 28 | - `MakeDefaultHandle::make_default_handle` now takes `self` as argument. This allows custom implementor structs to hold non-static data. [\#49] 29 | - Change the default initialization of `PrometheusHandle` to prevent unbounded memory growth of histograms. [\#52] 30 | - Bump `metrics` to `0.23`, `metrics-exporter-prometheus` to `0.15`. [\#52] 31 | - Document MSRV as 1.70 currently. [\#52] 32 | 33 | ### Added 34 | 35 | - `GenericMetricLayer::pair_from` to initialize from a concrete struct. `GenericMetricLayer::pair` now requires that the handle type implements `Default`. [\#49] 36 | - `BaseMetricLayer` that serves a more lightweight alternative to `GenericMetricLayer`. [\#56] 37 | 38 | # [0.6.1] - 2024-01-23 39 | 40 | - Disabled the `"push-gateway"` feature in `metrics-exporter-prometheus` by default, and added a way to enable it via 41 | the same name under `axum_prometheus`. This change ensures that this crate can still be built without openssl support, see [here](https://github.com/Ptrskay3/axum-prometheus/issues/42). [\#44] 42 | - Update examples to `metrics-exporter-prometheus` to `0.13` and `metrics` to `0.22`. [\#43] 43 | 44 | 45 | # [0.6.0] - 2024-01-22 46 | 47 | - Update `metrics-exporter-prometheus` to `0.13` and `metrics` to `0.22`. [\#39] 48 | 49 | # [0.5.0] - 2023-11-27 50 | 51 | ### Added 52 | 53 | - Support for response body size metric, which can be turned on via `PrometheusMetricLayerBuilder::enable_response_body_size`. [\#33] 54 | - All metrics now are initialized via `metrics::describe_*` function by default, but can be turned off with `PrometheusMetricLayerBuilder::no_initialize_metrics`. [\#33] 55 | - Compatibility with `http-body = "1.0"` and`axum = "0.7"`. [\#36] 56 | 57 | ### Changed 58 | 59 | - The lower-level Lifecycle API has changed: separated the `OnBodyChunk` trait, which is ran when a response body chunk has been generated. [#\33] 60 | 61 | # [0.4.0] - 2023-07-24 62 | 63 | ### Added 64 | 65 | - Support for different exporters than Prometheus. Developers now allowed to use their own exporter - as long as it's using the `metrics.rs` ecosystem. This is meant to be a non-breaking change - if you're using Prometheus, you shouldn't notice any changes in the public API. If you do however, please file an issue! [\#28] 66 | - An example showcasing `StatsD` exporter [\#28] 67 | - Simple snapshot tests [\#28] 68 | - Utility functions to get metric names at runtime [\#28] 69 | 70 | ### Fixed 71 | 72 | - Previous attempts to fix `PrometheusMetricBuilder::with_prefix` in 0.3.4 were not complete, this is now fully addressed. [\#28] 73 | 74 | # [0.3.4] - 2023-07-16 75 | 76 | ### Fixed 77 | 78 | - `PrometheusMetricBuilder::with_prefix` is now properly setting the metric prefix, and the metric handle also takes that prefix into account. 79 | Previously the metric initialization incorrectly ignored the prefix, which caused the requests duration histogram to use `quantile` instead of `le` labels. 80 | 81 | # [0.3.3] - 2023-05-02 82 | 83 | - Update `metrics-exporter-prometheus` to `0.12` and `metrics` to `0.21`. 84 | 85 | # [0.3.2] - 2023-03-25 86 | 87 | ### Added 88 | 89 | - The status code of the response is now captured in the total requests counter metric. 90 | 91 | # [0.3.1] - 2023-02-16 92 | 93 | ### Added 94 | 95 | - `with_prefix` to `PrometheusMetricLayerBuilder`, which can be used to rename the default prefix (`axum`) for all metrics. This is especially useful when 96 | working with cargo workspaces that has more than one `axum_prometheus` instance (since environment variables don't work there). 97 | 98 | ## [0.3.0] - 2023-01-04 99 | 100 | ### Added 101 | 102 | - Routing patterns can be ignored, and grouped together when reporting to Prometheus. 103 | - Endpoint label behavior can be altered with the new `EndpointLabel` enum. 104 | - Added a new builder `PrometheusMetricLayerBuilder` to easily customize these. 105 | 106 | ```rust 107 | let (prometheus_layer, metric_handle) = PrometheusMetricLayerBuilder::new() 108 | // ignore reporting requests that match "/foo" or "/sensitive" 109 | .with_ignore_patterns(&["/foo", "/sensitive"]) 110 | // if the any of the second argument matches, report them at the `/bar` endpoint 111 | .with_group_patterns_as("/bar", &["/foo/:bar", "/foo/:bar/:baz"]) 112 | // use `axum::extract::MatchedPath`, and if that fails, use the exact requested URI 113 | .with_endpoint_label_type(EndpointLabel::MatchedPath) 114 | .with_default_metrics() 115 | .build_pair(); 116 | ``` 117 | 118 | - A [builder-example](examples/builder-example/) and an [endpoint-type-example](examples/endpoint-type-example/). 119 | 120 | - The metric names can be changed by setting some environmental variables at compile time. It is best to set these in the `config.toml` (note this is not the same file as `Cargo.toml`): 121 | ```toml 122 | [env] 123 | AXUM_HTTP_REQUESTS_TOTAL = "my_app_requests_total" 124 | AXUM_HTTP_REQUESTS_DURATION_SECONDS = "my_app_requests_duration_seconds" 125 | AXUM_HTTP_REQUESTS_PENDING = "my_app_requests_pending" 126 | ``` 127 | 128 | ## [0.2.0] - 2022-10-25 129 | 130 | ### Added 131 | 132 | - Compatibility with `axum-core = "0.3"` and thus `axum = "0.6"`. 133 | 134 | ## 0.1.0 135 | 136 | First version. 137 | 138 | [unreleased]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.8.0..master 139 | [0.2.0]: https://github.com/Ptrskay3/axum-prometheus/compare/9fb600d7d9ac2e6d38e6399119fc7ba7f25d5fe0...756dc67bf2baae2de406e012bdaa2334ce0fcdcb 140 | [0.3.0]: https://github.com/Ptrskay3/axum-prometheus/compare/axum-0.6...release/0.3 141 | [0.3.1]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.3...release/0.3.1 142 | [0.3.2]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.3.1...release/0.3.2 143 | [0.3.3]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.3.2...release/0.3.3 144 | [0.3.4]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.3.3...release/0.3.4 145 | [0.4.0]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.3.4...release/0.4.0 146 | [0.5.0]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.4.0...release/0.5.0 147 | [0.6.0]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.5.0...release/0.6.0 148 | [0.6.1]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.6.0...release/0.6.1 149 | [0.7.0]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.6.1...release/0.7.0 150 | [0.8.0]: https://github.com/Ptrskay3/axum-prometheus/compare/release/0.7.0...release/0.8.0 151 | [\#28]: https://github.com/Ptrskay3/axum-prometheus/pull/28 152 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-prometheus" 3 | version = "0.8.0" 4 | edition = "2021" 5 | homepage = "https://github.com/Ptrskay3/axum-prometheus" 6 | license = "MIT" 7 | description = "A tower middleware to collect and export HTTP metrics for Axum" 8 | rust-version = "1.75" 9 | keywords = ["axum", "prometheus", "metrics"] 10 | categories = ["asynchronous", "network-programming", "web-programming", "development-tools::profiling"] 11 | repository = "https://github.com/Ptrskay3/axum-prometheus" 12 | 13 | [dependencies] 14 | axum = "0.8.0" 15 | http = "1.2.0" 16 | http-body = "1.0.0" 17 | metrics = "0.24.1" 18 | metrics-exporter-prometheus = { version = "0.16.0", optional = true, default-features = false } 19 | pin-project-lite = "0.2.15" 20 | tower = "0.5.1" 21 | tokio = { version = "1.42.0", features = ["rt-multi-thread", "macros"] } 22 | tower-http = "0.6.2" 23 | bytes = "1.9.0" 24 | futures-core = "0.3.24" 25 | matchit = "0.8" 26 | 27 | [dev-dependencies] 28 | insta = { version = "1.41.1", features = ["yaml", "filters"] } 29 | http-body-util = "0.1.0" 30 | 31 | [features] 32 | default = ["prometheus"] 33 | prometheus = ["metrics-exporter-prometheus"] 34 | push-gateway = ["metrics-exporter-prometheus/push-gateway"] 35 | http-listener = ["metrics-exporter-prometheus/http-listener"] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Peter Leeh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Axum-Prometheus 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | A middleware to collect HTTP metrics for Axum applications. 16 | 17 | `axum-prometheus` relies on [`metrics.rs`](https://metrics.rs/) and its ecosystem to collect and export metrics - for instance for Prometheus, `metrics_exporter_prometheus` is used as a backend to interact with Prometheus. 18 | 19 | ## Metrics 20 | 21 | By default, three HTTP metrics are tracked 22 | 23 | - `axum_http_requests_total` (labels: endpoint, method, status): the total number of HTTP requests handled (counter) 24 | - `axum_http_requests_duration_seconds` (labels: endpoint, method, status): the request duration for all HTTP requests handled (histogram) 25 | - `axum_http_requests_pending` (labels: endpoint, method): the number of currently in-flight requests (gauge) 26 | 27 | This crate also allows to track response body sizes as a histogram — see `PrometheusMetricLayerBuilder::enable_response_body_size`. 28 | 29 | ### Renaming Metrics 30 | 31 | These metrics can be renamed by specifying environmental variables at compile time: 32 | 33 | - `AXUM_HTTP_REQUESTS_TOTAL` 34 | - `AXUM_HTTP_REQUESTS_DURATION_SECONDS` 35 | - `AXUM_HTTP_REQUESTS_PENDING` 36 | - `AXUM_HTTP_RESPONSE_BODY_SIZE` (if body size tracking is enabled) 37 | 38 | These environmental variables can be set in your `.cargo/config.toml` since Cargo 1.56: 39 | 40 | ```toml 41 | [env] 42 | AXUM_HTTP_REQUESTS_TOTAL = "my_app_requests_total" 43 | AXUM_HTTP_REQUESTS_DURATION_SECONDS = "my_app_requests_duration_seconds" 44 | AXUM_HTTP_REQUESTS_PENDING = "my_app_requests_pending" 45 | AXUM_HTTP_RESPONSE_BODY_SIZE = "my_app_response_body_size" 46 | ``` 47 | 48 | ..or optionally use `PrometheusMetricLayerBuilder::with_prefix` function. 49 | 50 | ### Compatibility 51 | 52 | | Axum Version | Crate Version | 53 | |--------------|---------------------| 54 | | `0.5` | `0.1` | 55 | | `0.6` | `0.2`, `0.3`, `0.4` | 56 | | `0.7` | `0.5`, `0.6`, `0.7` | 57 | | `0.8` | `0.8` | 58 | 59 | #### MSRV 60 | 61 | This crate's current MSRV is 1.75. 62 | 63 | ## Usage 64 | 65 | For more elaborate use-cases, see the [`builder example`](examples/builder-example/). 66 | 67 | Add `axum-prometheus` to your `Cargo.toml`. 68 | 69 | ```toml 70 | [dependencies] 71 | axum-prometheus = "0.8.0" 72 | ``` 73 | 74 | Then you instantiate the prometheus middleware: 75 | 76 | ```rust 77 | use std::{net::SocketAddr, time::Duration}; 78 | use axum::{routing::get, Router}; 79 | use axum_prometheus::PrometheusMetricLayer; 80 | 81 | #[tokio::main] 82 | async fn main() { 83 | let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair(); 84 | let app = Router::<()>::new() 85 | .route("/fast", get(|| async {})) 86 | .route( 87 | "/slow", 88 | get(|| async { 89 | tokio::time::sleep(Duration::from_secs(1)).await; 90 | }), 91 | ) 92 | .route("/metrics", get(|| async move { metric_handle.render() })) 93 | .layer(prometheus_layer); 94 | 95 | 96 | let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 97 | .await 98 | .unwrap(); 99 | axum::serve(listener, app).await.unwrap(); 100 | } 101 | ``` 102 | 103 | Note that the `/metrics` endpoint is not automatically exposed, so you need to add that as a route manually. 104 | Calling the `/metrics` endpoint will expose your metrics: 105 | 106 | ```not_rust 107 | axum_http_requests_total{method="GET",endpoint="/metrics",status="200"} 5 108 | axum_http_requests_pending{method="GET",endpoint="/metrics"} 1 109 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.005"} 4 110 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.01"} 4 111 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.025"} 4 112 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.05"} 4 113 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.1"} 4 114 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.25"} 4 115 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.5"} 4 116 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="1"} 4 117 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="2.5"} 4 118 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="5"} 4 119 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="10"} 4 120 | axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="+Inf"} 4 121 | axum_http_requests_duration_seconds_sum{method="GET",status="200",endpoint="/metrics"} 0.001997171 122 | axum_http_requests_duration_seconds_count{method="GET",status="200",endpoint="/metrics"} 4 123 | ``` 124 | 125 | Let's note that since `metrics-exporter-prometheus = "0.13"` that crate [introduced](https://github.com/metrics-rs/metrics/commit/d817f5c6f4909eeafbd9ff9ceadbf29302169bfa) the `push-gateway` default feature, that 126 | requires openssl support. The `axum_prometheus` crate __does not__ rely on, nor enable this feature by default — if you need it, 127 | you may enable it through the `"push-gateway"` feature in `axum_prometheus`. 128 | 129 | ## Prometheus push gateway feature 130 | 131 | This crate currently has no higher level API for the `push-gateway` feature. If you plan to use it, enable the `push-gateway` feature in `axum-prometheus`, use `BaseMetricLayer`, and setup your recorder manually, similar to the [`base-metric-layer-example`](./examples/base-metric-layer-example/src/main.rs). 132 | 133 | ## Using a different exporter than Prometheus 134 | 135 | This crate may be used with other exporters than Prometheus. First, disable the default features: 136 | 137 | ```toml 138 | axum-prometheus = { version = "0.8.0", default-features = false } 139 | ``` 140 | 141 | Then implement the `MakeDefaultHandle` for the provider you'd like to use. For `StatsD`: 142 | 143 | ```rust 144 | use metrics_exporter_statsd::StatsdBuilder; 145 | use axum_prometheus::{MakeDefaultHandle, GenericMetricLayer}; 146 | 147 | // The custom StatsD exporter struct. It may take fields as well. 148 | struct Recorder { port: u16 } 149 | 150 | // In order to use this with `axum_prometheus`, we must implement `MakeDefaultHandle`. 151 | impl MakeDefaultHandle for Recorder { 152 | // We don't need to return anything meaningful from here (unlike PrometheusHandle) 153 | // Let's just return an empty tuple. 154 | type Out = (); 155 | 156 | fn make_default_handle(self) -> Self::Out { 157 | // The regular setup for StatsD. Notice that `self` is passed in by value. 158 | let recorder = StatsdBuilder::from("127.0.0.1", self.port) 159 | .with_queue_size(5000) 160 | .with_buffer_size(1024) 161 | .build(Some("prefix")) 162 | .expect("Could not create StatsdRecorder"); 163 | 164 | metrics::set_boxed_recorder(Box::new(recorder)).unwrap(); 165 | } 166 | } 167 | 168 | fn main() { 169 | // Use `GenericMetricLayer` instead of `PrometheusMetricLayer`. 170 | // Generally `GenericMetricLayer::pair_from` is what you're looking for. 171 | // It lets you pass in a concrete initialized `Recorder`. 172 | let (metric_layer, _handle) = GenericMetricLayer::pair_from(Recorder { port: 8125 }); 173 | } 174 | ``` 175 | 176 | It's also possible to use `GenericMetricLayer::pair`, however it's only callable if the recorder struct implements `Default` as well. 177 | ```rust 178 | use metrics_exporter_statsd::StatsdBuilder; 179 | use axum_prometheus::{MakeDefaultHandle, GenericMetricLayer}; 180 | 181 | #[derive(Default)] 182 | struct Recorder { port: u16 } 183 | 184 | impl MakeDefaultHandle for Recorder { 185 | /* .. same as before .. */ 186 | } 187 | 188 | fn main() { 189 | // This will internally call `Recorder::make_default_handle(Recorder::default)`. 190 | let (metric_layer, _handle) = GenericMetricLayer::<_, Recorder>::pair(); 191 | } 192 | ``` 193 | 194 | --- 195 | 196 | This crate is similar to (and takes inspiration from) [`actix-web-prom`](https://github.com/nlopes/actix-web-prom) and [`rocket_prometheus`](https://github.com/sd2k/rocket_prometheus), 197 | and also builds on top of davidpdrsn's [earlier work with LifeCycleHooks](https://github.com/tower-rs/tower-http/pull/96) in `tower-http`. 198 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["*"] 3 | resolver = "2" 4 | exclude = ["target"] 5 | -------------------------------------------------------------------------------- /examples/base-metric-layer-example/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/base-metric-layer-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "base-metric-layer-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | axum = "0.8.0" 9 | tokio = { version = "1.0", features = ["full"] } 10 | tracing = "0.1" 11 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 12 | axum-prometheus = { path = "../../", features = ["push-gateway"] } 13 | -------------------------------------------------------------------------------- /examples/base-metric-layer-example/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This example uses the `BaseMetricLayer`, which only emits metrics using the `metrics` crate's macros, 2 | //! and the global exporter/recorder is fully up to user to initialize and configure. 3 | //! 4 | //! Run with 5 | //! 6 | //! ```not_rust 7 | //! cd examples && cargo run -p base-metric-layer-example 8 | //! ``` 9 | //! 10 | use axum::{routing::get, Router}; 11 | use axum_prometheus::{metrics_exporter_prometheus::PrometheusBuilder, BaseMetricLayer}; 12 | use std::{net::SocketAddr, time::Duration}; 13 | 14 | #[tokio::main] 15 | async fn main() { 16 | // Initialize the recorder as you like. This example uses push gateway mode instead of a http listener. 17 | // To use this, don't forget to enable the "push-gateway" feature in `axum-prometheus`. 18 | PrometheusBuilder::new() 19 | .with_push_gateway( 20 | "http://127.0.0.1:9091/metrics/job/example", 21 | Duration::from_secs(10), 22 | None, 23 | None, 24 | ) 25 | .expect("push gateway endpoint should be valid") 26 | .install() 27 | .expect("failed to install Prometheus recorder"); 28 | 29 | let app = Router::<()>::new() 30 | .route("/fast", get(|| async {})) 31 | .route( 32 | "/slow", 33 | get(|| async { 34 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 35 | }), 36 | ) 37 | // Only need to add this layer at the end. 38 | .layer(BaseMetricLayer::new()); 39 | let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 40 | .await 41 | .unwrap(); 42 | axum::serve(listener, app).await.unwrap() 43 | } 44 | -------------------------------------------------------------------------------- /examples/builder-example/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/builder-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "builder-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | axum = "0.8.0" 9 | tokio = { version = "1.0", features = ["full"] } 10 | tracing = "0.1" 11 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 12 | axum-prometheus = { path = "../../" } 13 | -------------------------------------------------------------------------------- /examples/builder-example/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Run with 2 | //! 3 | //! ```not_rust 4 | //! cd examples && cargo run -p builder-example 5 | //! ``` 6 | 7 | use axum::{routing::get, Router}; 8 | use axum_prometheus::{ 9 | metrics_exporter_prometheus::{Matcher, PrometheusBuilder}, 10 | utils::SECONDS_DURATION_BUCKETS, 11 | PrometheusMetricLayerBuilder, AXUM_HTTP_REQUESTS_DURATION_SECONDS, 12 | }; 13 | use std::{net::SocketAddr, time::Duration}; 14 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | tracing_subscriber::registry() 19 | .with( 20 | tracing_subscriber::EnvFilter::try_from_default_env() 21 | .unwrap_or_else(|_| "builder_example=debug".into()), 22 | ) 23 | .with(tracing_subscriber::fmt::layer()) 24 | .init(); 25 | 26 | let (prometheus_layer, metric_handle) = PrometheusMetricLayerBuilder::new() 27 | .with_prefix("builder-example") 28 | // ignore reporting requests that match "/metrics" 29 | .with_ignore_pattern("/metrics") 30 | // if the any of the second argument matches, report them at the `/foo` endpoint 31 | .with_group_patterns_as("/foo", &["/foo/{bar}", "/foo/{bar}/{baz}"]) 32 | // build a custom PrometheusHandle 33 | .with_metrics_from_fn(|| { 34 | PrometheusBuilder::new() 35 | .set_buckets_for_metric( 36 | Matcher::Full(AXUM_HTTP_REQUESTS_DURATION_SECONDS.to_string()), 37 | SECONDS_DURATION_BUCKETS, 38 | ) 39 | .unwrap() 40 | .install_recorder() 41 | .unwrap() 42 | }) 43 | .build_pair(); 44 | 45 | let app = Router::new() 46 | .route( 47 | "/foo/{bar}", 48 | get(|| async { 49 | tracing::debug!("calling /foo/{{bar}}"); 50 | }), 51 | ) 52 | .route( 53 | "/foo/{bar}/{baz}", 54 | get(|| async { 55 | tracing::debug!("calling /foo/{{bar}}/{{baz}}"); 56 | }), 57 | ) 58 | .route( 59 | "/fast", 60 | get(|| async { 61 | tracing::debug!("calling /fast"); 62 | }), 63 | ) 64 | .route( 65 | "/slow", 66 | get(|| async { 67 | tracing::debug!("calling /slow"); 68 | tokio::time::sleep(Duration::from_secs(1)).await; 69 | }), 70 | ) 71 | .route("/metrics", get(|| async move { metric_handle.render() })) 72 | .layer(prometheus_layer); 73 | 74 | let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 75 | .await 76 | .unwrap(); 77 | axum::serve(listener, app).await.unwrap(); 78 | } 79 | -------------------------------------------------------------------------------- /examples/endpoint-type-example/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/endpoint-type-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "endpoint-type-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | axum = "0.8.0" 9 | tokio = { version = "1.0", features = ["full"] } 10 | tracing = "0.1" 11 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 12 | axum-prometheus = { path = "../../" } 13 | -------------------------------------------------------------------------------- /examples/endpoint-type-example/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Run with 2 | //! 3 | //! ```not_rust 4 | //! cd examples && cargo run -p endpoint-type-example 5 | //! ``` 6 | 7 | use axum::{routing::get, Router}; 8 | use axum_prometheus::{EndpointLabel, PrometheusMetricLayerBuilder}; 9 | use std::net::SocketAddr; 10 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | tracing_subscriber::registry() 15 | .with( 16 | tracing_subscriber::EnvFilter::try_from_default_env() 17 | .unwrap_or_else(|_| "endpoint_type_example=debug".into()), 18 | ) 19 | .with(tracing_subscriber::fmt::layer()) 20 | .init(); 21 | 22 | let (prometheus_layer, metric_handle) = PrometheusMetricLayerBuilder::new() 23 | .with_endpoint_label_type(EndpointLabel::MatchedPathWithFallbackFn(|path| { 24 | format!("{}_changed", path) 25 | })) 26 | .with_default_metrics() 27 | .build_pair(); 28 | 29 | let app = Router::new() 30 | .route( 31 | "/foo/{bar}", 32 | get(|| async { 33 | tracing::debug!("calling /foo/{{bar}}"); 34 | }), 35 | ) 36 | .nest( 37 | "/baz", 38 | Router::new().route( 39 | "/qux/{a}", 40 | get(|| async { 41 | // Calling `/baz/qux/2`, this'll show up as `endpoint="/baz/qux/2_changed` because of the fallback function. 42 | tracing::debug!("calling /baz/qux/{{a}}"); 43 | }), 44 | ), 45 | ) 46 | .route("/metrics", get(|| async move { metric_handle.render() })) 47 | .layer(prometheus_layer); 48 | 49 | let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 50 | .await 51 | .unwrap(); 52 | axum::serve(listener, app).await.unwrap(); 53 | } 54 | -------------------------------------------------------------------------------- /examples/exporter-statsd-example/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/exporter-statsd-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "exporter-statsd-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | axum = "0.8.0" 9 | tokio = { version = "1.0", features = ["full"] } 10 | tracing = "0.1" 11 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 12 | metrics-exporter-statsd = "0.9.0" 13 | axum-prometheus = { path = "../../", default-features = false } 14 | -------------------------------------------------------------------------------- /examples/exporter-statsd-example/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Run with 2 | //! 3 | //! ```not_rust 4 | //! cd examples && cargo run -p exporter-statsd-example 5 | //! ``` 6 | 7 | use axum::{routing::get, Router}; 8 | use axum_prometheus::{metrics, GenericMetricLayer, MakeDefaultHandle}; 9 | use metrics_exporter_statsd::StatsdBuilder; 10 | use std::net::SocketAddr; 11 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 12 | 13 | struct Recorder<'a> { 14 | host: &'a str, 15 | port: u16, 16 | queue_size: usize, 17 | buffer_size: usize, 18 | prefix: Option<&'a str>, 19 | } 20 | 21 | // In order to use this with `axum_prometheus`, we must implement `MakeDefaultHandle`. 22 | impl<'a> MakeDefaultHandle for Recorder<'a> { 23 | // We don't need to return anything meaningful from here (unlike PrometheusHandle) 24 | // Let's just return an empty tuple. 25 | type Out = (); 26 | 27 | fn make_default_handle(self) -> Self::Out { 28 | // The regular setup for StatsD.. 29 | let recorder = StatsdBuilder::from(self.host, self.port) 30 | .with_queue_size(self.queue_size) 31 | .with_buffer_size(self.buffer_size) 32 | .build(self.prefix) 33 | .expect("Could not create StatsDRecorder"); 34 | 35 | metrics::set_global_recorder(recorder).unwrap(); 36 | } 37 | } 38 | 39 | #[tokio::main] 40 | async fn main() { 41 | tracing_subscriber::registry() 42 | .with( 43 | tracing_subscriber::EnvFilter::try_from_default_env() 44 | .unwrap_or_else(|_| "exporter_statsd_example=debug".into()), 45 | ) 46 | .with(tracing_subscriber::fmt::layer()) 47 | .init(); 48 | 49 | // Use `GenericMetricLayer` instead of `PrometheusMetricLayer`. 50 | // By using `pair_from`, you can inject any values into the recorder. 51 | // `GenericMetricLayer::pair` is only callable if the recorder struct implements Default. 52 | let (metric_layer, _) = GenericMetricLayer::pair_from(Recorder { 53 | host: "127.0.0.1", 54 | port: 8125, 55 | queue_size: 5000, 56 | buffer_size: 1024, 57 | prefix: Some("prefix"), 58 | }); 59 | let app = Router::new() 60 | .route("/foo", get(|| async {})) 61 | .route("/bar", get(|| async {})) 62 | .layer(metric_layer); 63 | 64 | let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 65 | .await 66 | .unwrap(); 67 | axum::serve(listener, app).await.unwrap(); 68 | } 69 | -------------------------------------------------------------------------------- /examples/simple-example/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/simple-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | axum = "0.8.0" 9 | tokio = { version = "1.0", features = ["full"] } 10 | tracing = "0.1" 11 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 12 | axum-prometheus = { path = "../../" } 13 | -------------------------------------------------------------------------------- /examples/simple-example/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Run with 2 | //! 3 | //! ```not_rust 4 | //! cd examples && cargo run -p simple-example 5 | //! ``` 6 | 7 | use axum::{routing::get, Router}; 8 | use std::{net::SocketAddr, time::Duration}; 9 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | tracing_subscriber::registry() 14 | .with( 15 | tracing_subscriber::EnvFilter::try_from_default_env() 16 | .unwrap_or_else(|_| "simple_example=debug".into()), 17 | ) 18 | .with(tracing_subscriber::fmt::layer()) 19 | .init(); 20 | 21 | let (prometheus_layer, metric_handle) = axum_prometheus::PrometheusMetricLayer::pair(); 22 | let app = Router::new() 23 | .route("/fast", get(|| async {})) 24 | .route( 25 | "/slow", 26 | get(|| async { 27 | tokio::time::sleep(Duration::from_secs(1)).await; 28 | }), 29 | ) 30 | .route("/metrics", get(|| async move { metric_handle.render() })) 31 | .layer(prometheus_layer); 32 | 33 | let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 34 | .await 35 | .unwrap(); 36 | axum::serve(listener, app).await.unwrap(); 37 | } 38 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::marker::PhantomData; 3 | 4 | #[cfg(feature = "prometheus")] 5 | use metrics_exporter_prometheus::PrometheusHandle; 6 | 7 | use crate::{set_prefix, GenericMetricLayer, MakeDefaultHandle, Traffic}; 8 | 9 | #[doc(hidden)] 10 | mod sealed { 11 | use super::{LayerOnly, Paired}; 12 | pub trait Sealed {} 13 | impl Sealed for LayerOnly {} 14 | impl Sealed for Paired {} 15 | } 16 | pub trait MetricBuilderState: sealed::Sealed {} 17 | 18 | pub enum Paired {} 19 | pub enum LayerOnly {} 20 | impl MetricBuilderState for Paired {} 21 | impl MetricBuilderState for LayerOnly {} 22 | 23 | #[derive(Default, Clone)] 24 | /// Determines how endpoints are reported. 25 | pub enum EndpointLabel { 26 | /// The reported endpoint label is always the fully qualified uri path that has been requested. 27 | Exact, 28 | /// The reported endpoint label is determined by first trying to extract and return [`axum::extract::MatchedPath`], 29 | /// and if that fails (typically on [nested routes]) it falls back to [`EndpointLabel::Exact`] behavior. This is 30 | /// the default option. 31 | /// 32 | /// [nested routes]: https://docs.rs/axum/latest/axum/extract/struct.MatchedPath.html#matched-path-in-nested-routers 33 | #[default] 34 | MatchedPath, 35 | /// Same as [`EndpointLabel::MatchedPath`], but instead of falling back to the exact uri called, it's given to a user-defined 36 | /// fallback function, that is expected to produce a String, which is then reported to Prometheus. 37 | MatchedPathWithFallbackFn(for<'f> fn(&'f str) -> String), 38 | } 39 | 40 | /// A builder for [`GenericMetricLayer`] that enables further customizations. 41 | /// 42 | /// Most of the example code uses [`PrometheusMetricLayerBuilder`], which is only a type alias 43 | /// specialized for Prometheus. 44 | /// 45 | /// ## Example 46 | /// ```rust,no_run 47 | /// use axum_prometheus::PrometheusMetricLayerBuilder; 48 | /// 49 | /// let (metric_layer, metric_handle) = PrometheusMetricLayerBuilder::new() 50 | /// .with_ignore_patterns(&["/metrics", "/sensitive"]) 51 | /// .with_group_patterns_as("/foo", &["/foo/:bar", "/foo/:bar/:baz"]) 52 | /// .with_group_patterns_as("/bar", &["/auth/*path"]) 53 | /// .with_default_metrics() 54 | /// .build_pair(); 55 | /// ``` 56 | #[derive(Clone, Default)] 57 | pub struct MetricLayerBuilder<'a, T, M, S: MetricBuilderState> { 58 | pub(crate) traffic: Traffic<'a>, 59 | pub(crate) metric_handle: Option, 60 | pub(crate) metric_prefix: Option, 61 | pub(crate) enable_body_size: bool, 62 | pub(crate) no_initialize_metrics: bool, 63 | pub(crate) _marker: PhantomData<(S, M)>, 64 | } 65 | 66 | impl<'a, T, M, S> MetricLayerBuilder<'a, T, M, S> 67 | where 68 | S: MetricBuilderState, 69 | { 70 | /// Skip reporting a specific route pattern. 71 | /// 72 | /// In the following example 73 | /// ```rust 74 | /// use axum_prometheus::PrometheusMetricLayerBuilder; 75 | /// 76 | /// let metric_layer = PrometheusMetricLayerBuilder::new() 77 | /// .with_ignore_pattern("/metrics") 78 | /// .build(); 79 | /// ``` 80 | /// any request that's URI path matches "/metrics" will be skipped altogether 81 | /// when reporting to the external provider. 82 | /// 83 | /// Supports the same features as `axum`'s Router. 84 | /// 85 | /// _Note that ignore patterns always checked before any other group pattern rule is applied 86 | /// and it short-circuits if a certain route is ignored._ 87 | pub fn with_ignore_pattern(mut self, ignore_pattern: &'a str) -> Self { 88 | self.traffic.with_ignore_pattern(ignore_pattern); 89 | self 90 | } 91 | 92 | /// Skip reporting a collection of route patterns. 93 | /// 94 | /// Equivalent with calling [`with_ignore_pattern`] repeatedly. 95 | /// 96 | /// ```rust 97 | /// use axum_prometheus::PrometheusMetricLayerBuilder; 98 | /// 99 | /// let metric_layer = PrometheusMetricLayerBuilder::new() 100 | /// .with_ignore_patterns(&["/foo", "/bar/:baz"]) 101 | /// .build(); 102 | /// ``` 103 | /// 104 | /// Supports the same features as `axum`'s Router. 105 | /// 106 | /// _Note that ignore patterns always checked before any other group pattern rule is applied 107 | /// and it short-circuits if a certain route is ignored._ 108 | /// 109 | /// [`with_ignore_pattern`]: crate::MetricLayerBuilder::with_ignore_pattern 110 | pub fn with_ignore_patterns(mut self, ignore_patterns: &'a [&'a str]) -> Self { 111 | self.traffic.with_ignore_patterns(ignore_patterns); 112 | self 113 | } 114 | 115 | /// Group matching route patterns and report them under the given (arbitrary) endpoint. 116 | /// 117 | /// This feature is commonly useful for parametrized routes. Let's say you have these two routes: 118 | /// - `/foo/:bar` 119 | /// - `/foo/:bar/:baz` 120 | /// 121 | /// By default every unique request URL path gets reported with different endpoint label. 122 | /// This feature allows you to report these under a custom endpoint, for instance `/foo`: 123 | /// 124 | /// ```rust 125 | /// use axum_prometheus::PrometheusMetricLayerBuilder; 126 | /// 127 | /// let metric_layer = PrometheusMetricLayerBuilder::new() 128 | /// // the choice of "/foo" is arbitrary 129 | /// .with_group_patterns_as("/foo", &["/foo/:bar", "foo/:bar/:baz"]) 130 | /// .build(); 131 | /// ``` 132 | pub fn with_group_patterns_as( 133 | mut self, 134 | group_pattern: &'a str, 135 | patterns: &'a [&'a str], 136 | ) -> Self { 137 | self.traffic.with_group_patterns_as(group_pattern, patterns); 138 | self 139 | } 140 | 141 | /// Determine how endpoints are reported. For more information, see [`EndpointLabel`]. 142 | /// 143 | /// [`EndpointLabel`]: crate::EndpointLabel 144 | pub fn with_endpoint_label_type(mut self, endpoint_label: EndpointLabel) -> Self { 145 | self.traffic.with_endpoint_label_type(endpoint_label); 146 | self 147 | } 148 | 149 | /// Enable response body size tracking. 150 | /// 151 | /// #### Note: 152 | /// This may introduce some performance overhead. 153 | pub fn enable_response_body_size(mut self, enable: bool) -> Self { 154 | self.enable_body_size = enable; 155 | self 156 | } 157 | 158 | /// By default, all metrics are initialized via `metrics::describe_*` macros, setting descriptions and units. 159 | /// 160 | /// This function disables this initialization. 161 | pub fn no_initialize_metrics(mut self) -> Self { 162 | self.no_initialize_metrics = true; 163 | self 164 | } 165 | } 166 | 167 | impl<'a, T, M> MetricLayerBuilder<'a, T, M, LayerOnly> { 168 | /// Initialize the builder. 169 | pub fn new() -> MetricLayerBuilder<'a, T, M, LayerOnly> { 170 | MetricLayerBuilder { 171 | _marker: PhantomData, 172 | traffic: Traffic::new(), 173 | metric_handle: None, 174 | no_initialize_metrics: false, 175 | metric_prefix: None, 176 | enable_body_size: false, 177 | } 178 | } 179 | 180 | /// Use a prefix for the metrics instead of `axum`. This will use the following 181 | /// metric names: 182 | /// - `{prefix}_http_requests_total` 183 | /// - `{prefix}_http_requests_pending` 184 | /// - `{prefix}_http_requests_duration_seconds` 185 | /// 186 | /// ..and will also use `{prefix}_http_response_body_size`, if response body size tracking is enabled. 187 | /// 188 | /// This method will take precedence over environment variables. 189 | /// 190 | /// ## Note 191 | /// 192 | /// This function inherently changes the metric names, beware to use the appropriate names. 193 | /// There're functions in the `utils` module to get them at runtime. 194 | /// 195 | /// [`utils`]: crate::utils 196 | pub fn with_prefix(mut self, prefix: impl Into>) -> Self { 197 | self.metric_prefix = Some(prefix.into().into_owned()); 198 | self 199 | } 200 | } 201 | impl<'a, T, M> MetricLayerBuilder<'a, T, M, LayerOnly> 202 | where 203 | M: MakeDefaultHandle, 204 | { 205 | /// Finalize the builder and get the previously registered metric handle out of it. 206 | pub fn build(self) -> GenericMetricLayer<'a, T, M> { 207 | GenericMetricLayer::from_builder(self) 208 | } 209 | } 210 | 211 | impl<'a, T, M> MetricLayerBuilder<'a, T, M, LayerOnly> 212 | where 213 | M: MakeDefaultHandle + Default, 214 | { 215 | /// Attach the default exporter handle to the builder. This is similar to 216 | /// initializing with [`GenericMetricLayer::pair`]. 217 | /// 218 | /// After calling this function you can finalize with the [`build_pair`] method, and 219 | /// can no longer call [`build`]. 220 | /// 221 | /// [`build`]: crate::MetricLayerBuilder::build 222 | /// [`build_pair`]: crate::MetricLayerBuilder::build_pair 223 | pub fn with_default_metrics(self) -> MetricLayerBuilder<'a, T, M, Paired> { 224 | let mut builder = MetricLayerBuilder::<'_, _, _, Paired>::from_layer_only(self); 225 | builder.metric_handle = Some(M::make_default_handle(M::default())); 226 | builder 227 | } 228 | } 229 | impl<'a, T, M> MetricLayerBuilder<'a, T, M, LayerOnly> { 230 | /// Attach a custom built exporter handle to the builder that's returned from the passed 231 | /// in closure. 232 | /// 233 | /// ## Example 234 | /// ```rust,no_run 235 | /// use axum_prometheus::{ 236 | /// PrometheusMetricLayerBuilder, AXUM_HTTP_REQUESTS_DURATION_SECONDS, utils::SECONDS_DURATION_BUCKETS, 237 | /// }; 238 | /// use metrics_exporter_prometheus::{Matcher, PrometheusBuilder}; 239 | /// 240 | /// let (metric_layer, metric_handle) = PrometheusMetricLayerBuilder::new() 241 | /// .with_metrics_from_fn(|| { 242 | /// PrometheusBuilder::new() 243 | /// .set_buckets_for_metric( 244 | /// Matcher::Full(AXUM_HTTP_REQUESTS_DURATION_SECONDS.to_string()), 245 | /// SECONDS_DURATION_BUCKETS, 246 | /// ) 247 | /// .unwrap() 248 | /// .install_recorder() 249 | /// .unwrap() 250 | /// }) 251 | /// .build_pair(); 252 | /// ``` 253 | /// After calling this function you can finalize with the [`build_pair`] method, and 254 | /// can no longer call [`build`]. 255 | /// 256 | /// [`build`]: crate::MetricLayerBuilder::build 257 | /// [`build_pair`]: crate::MetricLayerBuilder::build_pair 258 | pub fn with_metrics_from_fn( 259 | self, 260 | f: impl FnOnce() -> T, 261 | ) -> MetricLayerBuilder<'a, T, M, Paired> { 262 | let mut builder = MetricLayerBuilder::<'_, _, _, Paired>::from_layer_only(self); 263 | builder.metric_handle = Some(f()); 264 | builder 265 | } 266 | } 267 | 268 | impl<'a, T, M> MetricLayerBuilder<'a, T, M, Paired> { 269 | pub(crate) fn from_layer_only(layer_only: MetricLayerBuilder<'a, T, M, LayerOnly>) -> Self { 270 | if let Some(prefix) = layer_only.metric_prefix.as_ref() { 271 | set_prefix(prefix); 272 | } 273 | if !layer_only.no_initialize_metrics { 274 | describe_metrics(layer_only.enable_body_size); 275 | } 276 | MetricLayerBuilder { 277 | _marker: PhantomData, 278 | traffic: layer_only.traffic, 279 | metric_handle: layer_only.metric_handle, 280 | no_initialize_metrics: layer_only.no_initialize_metrics, 281 | metric_prefix: layer_only.metric_prefix, 282 | enable_body_size: layer_only.enable_body_size, 283 | } 284 | } 285 | } 286 | 287 | impl<'a, T, M> MetricLayerBuilder<'a, T, M, Paired> 288 | where 289 | M: MakeDefaultHandle + Default, 290 | { 291 | /// Finalize the builder and get out the [`GenericMetricLayer`] and the 292 | /// exporter handle out of it as a tuple. 293 | pub fn build_pair(self) -> (GenericMetricLayer<'a, T, M>, T) { 294 | GenericMetricLayer::pair_from_builder(self) 295 | } 296 | } 297 | 298 | #[cfg(feature = "prometheus")] 299 | /// A builder for [`crate::PrometheusMetricLayer`] that enables further customizations. 300 | pub type PrometheusMetricLayerBuilder<'a, S> = 301 | MetricLayerBuilder<'a, PrometheusHandle, crate::Handle, S>; 302 | 303 | fn describe_metrics(enable_body_size: bool) { 304 | metrics::describe_counter!( 305 | crate::utils::requests_total_name(), 306 | metrics::Unit::Count, 307 | "The number of times a HTTP request was processed." 308 | ); 309 | metrics::describe_gauge!( 310 | crate::utils::requests_pending_name(), 311 | metrics::Unit::Count, 312 | "The number of currently in-flight requests." 313 | ); 314 | metrics::describe_histogram!( 315 | crate::utils::requests_duration_name(), 316 | metrics::Unit::Seconds, 317 | "The distribution of HTTP response times." 318 | ); 319 | if enable_body_size { 320 | metrics::describe_histogram!( 321 | crate::utils::response_body_size_name(), 322 | metrics::Unit::Count, 323 | "The distribution of HTTP response body sizes." 324 | ); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //!A middleware to collect HTTP metrics for Axum applications. 2 | //! 3 | //! `axum-prometheus` relies on [`metrics.rs`](https://metrics.rs/) and its ecosystem to collect and export metrics - for instance for Prometheus, `metrics_exporter_prometheus` is used as a backend to interact with Prometheus. 4 | //! 5 | //! ## Metrics 6 | //! 7 | //! By default three HTTP metrics are tracked 8 | //! - `axum_http_requests_total` (labels: endpoint, method, status): the total number of HTTP requests handled (counter) 9 | //! - `axum_http_requests_duration_seconds` (labels: endpoint, method, status): the request duration for all HTTP requests handled (histogram) 10 | //! - `axum_http_requests_pending` (labels: endpoint, method): the number of currently in-flight requests (gauge) 11 | //! 12 | //! This crate also allows to track response body sizes as a histogram — see [`PrometheusMetricLayerBuilder::enable_response_body_size`]. 13 | //! 14 | //! ### Renaming Metrics 15 | //! 16 | //! These metrics can be renamed by specifying environmental variables at compile time: 17 | //! - `AXUM_HTTP_REQUESTS_TOTAL` 18 | //! - `AXUM_HTTP_REQUESTS_DURATION_SECONDS` 19 | //! - `AXUM_HTTP_REQUESTS_PENDING` 20 | //! - `AXUM_HTTP_RESPONSE_BODY_SIZE` (if body size tracking is enabled) 21 | //! 22 | //! These environmental variables can be set in your `.cargo/config.toml` since Cargo 1.56: 23 | //! ```toml 24 | //! [env] 25 | //! AXUM_HTTP_REQUESTS_TOTAL = "my_app_requests_total" 26 | //! AXUM_HTTP_REQUESTS_DURATION_SECONDS = "my_app_requests_duration_seconds" 27 | //! AXUM_HTTP_REQUESTS_PENDING = "my_app_requests_pending" 28 | //! AXUM_HTTP_RESPONSE_BODY_SIZE = "my_app_response_body_size" 29 | //! ``` 30 | //! 31 | //! ..or optionally use [`PrometheusMetricLayerBuilder::with_prefix`] function. 32 | //! 33 | //! ## Usage 34 | //! 35 | //! For more elaborate use-cases, see the builder-example that leverages [`PrometheusMetricLayerBuilder`]. 36 | //! 37 | //! Add `axum-prometheus` to your `Cargo.toml`. 38 | //! ```not_rust 39 | //! [dependencies] 40 | //! axum-prometheus = "0.8.0" 41 | //! ``` 42 | //! 43 | //! Then you instantiate the prometheus middleware: 44 | //! ```rust,no_run 45 | //! use std::{net::SocketAddr, time::Duration}; 46 | //! use axum::{routing::get, Router}; 47 | //! use axum_prometheus::PrometheusMetricLayer; 48 | //! 49 | //! #[tokio::main] 50 | //! async fn main() { 51 | //! let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair(); 52 | //! let app = Router::new() 53 | //! .route("/fast", get(|| async {})) 54 | //! .route( 55 | //! "/slow", 56 | //! get(|| async { 57 | //! tokio::time::sleep(Duration::from_secs(1)).await; 58 | //! }), 59 | //! ) 60 | //! .route("/metrics", get(|| async move { metric_handle.render() })) 61 | //! .layer(prometheus_layer); 62 | //! 63 | //! let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 64 | //! .await 65 | //! .unwrap(); 66 | //! axum::serve(listener, app).await.unwrap() 67 | //! } 68 | //! ``` 69 | //! 70 | //! Note that the `/metrics` endpoint is not automatically exposed, so you need to add that as a route manually. 71 | //! Calling the `/metrics` endpoint will expose your metrics: 72 | //! ```not_rust 73 | //! axum_http_requests_total{method="GET",endpoint="/metrics",status="200"} 5 74 | //! axum_http_requests_pending{method="GET",endpoint="/metrics"} 1 75 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.005"} 4 76 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.01"} 4 77 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.025"} 4 78 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.05"} 4 79 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.1"} 4 80 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.25"} 4 81 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="0.5"} 4 82 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="1"} 4 83 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="2.5"} 4 84 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="5"} 4 85 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="10"} 4 86 | //! axum_http_requests_duration_seconds_bucket{method="GET",status="200",endpoint="/metrics",le="+Inf"} 4 87 | //! axum_http_requests_duration_seconds_sum{method="GET",status="200",endpoint="/metrics"} 0.001997171 88 | //! axum_http_requests_duration_seconds_count{method="GET",status="200",endpoint="/metrics"} 4 89 | //! ``` 90 | //! 91 | //! ## Prometheus push gateway feature 92 | //! This crate currently has no higher level API for the `push-gateway` feature. If you plan to use it, enable the 93 | //! `push-gateway` feature in `axum-prometheus`, use `BaseMetricLayer`, and setup your recorder manually, similar to 94 | //! the `base-metric-layer-example`. 95 | //! 96 | //! ## Using a different exporter than Prometheus 97 | //! 98 | //! This crate may be used with other exporters than Prometheus. First, disable the default features: 99 | //! 100 | //! ```toml 101 | //! axum-prometheus = { version = "0.8.0", default-features = false } 102 | //! ``` 103 | //! 104 | //! Then implement the `MakeDefaultHandle` for the provider you'd like to use. For `StatsD`: 105 | //! 106 | //! ```rust,ignore 107 | //! use metrics_exporter_statsd::StatsdBuilder; 108 | //! use axum_prometheus::{MakeDefaultHandle, GenericMetricLayer}; 109 | //! 110 | //! // The custom StatsD exporter struct. It may take fields as well. 111 | //! struct Recorder { port: u16 } 112 | //! 113 | //! // In order to use this with `axum_prometheus`, we must implement `MakeDefaultHandle`. 114 | //! impl MakeDefaultHandle for Recorder { 115 | //! // We don't need to return anything meaningful from here (unlike PrometheusHandle) 116 | //! // Let's just return an empty tuple. 117 | //! type Out = (); 118 | //! 119 | //! fn make_default_handle(self) -> Self::Out { 120 | //! // The regular setup for StatsD. Notice that `self` is passed in by value. 121 | //! let recorder = StatsdBuilder::from("127.0.0.1", self.port) 122 | //! .with_queue_size(5000) 123 | //! .with_buffer_size(1024) 124 | //! .build(Some("prefix")) 125 | //! .expect("Could not create StatsdRecorder"); 126 | //! 127 | //! metrics::set_boxed_recorder(Box::new(recorder)).unwrap(); 128 | //! } 129 | //! } 130 | //! 131 | //! fn main() { 132 | //! // Use `GenericMetricLayer` instead of `PrometheusMetricLayer`. 133 | //! // Generally `GenericMetricLayer::pair_from` is what you're looking for. 134 | //! // It lets you pass in a concrete initialized `Recorder`. 135 | //! let (metric_layer, _handle) = GenericMetricLayer::pair_from(Recorder { port: 8125 }); 136 | //! } 137 | //! ``` 138 | //! 139 | //! It's also possible to use `GenericMetricLayer::pair`, however it's only callable if the recorder struct implements `Default` as well. 140 | //! 141 | //! ```rust,ignore 142 | //! use metrics_exporter_statsd::StatsdBuilder; 143 | //! use axum_prometheus::{MakeDefaultHandle, GenericMetricLayer}; 144 | //! 145 | //! #[derive(Default)] 146 | //! struct Recorder { port: u16 } 147 | //! 148 | //! impl MakeDefaultHandle for Recorder { 149 | //! /* .. same as before .. */ 150 | //! } 151 | //! 152 | //! fn main() { 153 | //! // This will internally call `Recorder::make_default_handle(Recorder::default)`. 154 | //! let (metric_layer, _handle) = GenericMetricLayer::<_, Recorder>::pair(); 155 | //! } 156 | //! ``` 157 | //! 158 | //! This crate is similar to (and takes inspiration from) [`actix-web-prom`](https://github.com/nlopes/actix-web-prom) and [`rocket_prometheus`](https://github.com/sd2k/rocket_prometheus), 159 | //! and also builds on top of davidpdrsn's [earlier work with LifeCycleHooks](https://github.com/tower-rs/tower-http/pull/96) in `tower-http`. 160 | //! 161 | //! [`PrometheusMetricLayerBuilder`]: crate::PrometheusMetricLayerBuilder 162 | 163 | #![allow(clippy::module_name_repetitions, clippy::unreadable_literal)] 164 | 165 | /// Identifies the gauge used for the requests pending metric. Defaults to 166 | /// `axum_http_requests_pending`, but can be changed by setting the `AXUM_HTTP_REQUESTS_PENDING` 167 | /// env at compile time. 168 | pub const AXUM_HTTP_REQUESTS_PENDING: &str = match option_env!("AXUM_HTTP_REQUESTS_PENDING") { 169 | Some(n) => n, 170 | None => "axum_http_requests_pending", 171 | }; 172 | 173 | /// Identifies the histogram/summary used for request latency. Defaults to `axum_http_requests_duration_seconds`, 174 | /// but can be changed by setting the `AXUM_HTTP_REQUESTS_DURATION_SECONDS` env at compile time. 175 | pub const AXUM_HTTP_REQUESTS_DURATION_SECONDS: &str = 176 | match option_env!("AXUM_HTTP_REQUESTS_DURATION_SECONDS") { 177 | Some(n) => n, 178 | None => "axum_http_requests_duration_seconds", 179 | }; 180 | 181 | /// Identifies the counter used for requests total. Defaults to `axum_http_requests_total`, 182 | /// but can be changed by setting the `AXUM_HTTP_REQUESTS_TOTAL` env at compile time. 183 | pub const AXUM_HTTP_REQUESTS_TOTAL: &str = match option_env!("AXUM_HTTP_REQUESTS_TOTAL") { 184 | Some(n) => n, 185 | None => "axum_http_requests_total", 186 | }; 187 | 188 | /// Identifies the histogram/summary used for response body size. Defaults to `axum_http_response_body_size`, 189 | /// but can be changed by setting the `AXUM_HTTP_RESPONSE_BODY_SIZE` env at compile time. 190 | pub const AXUM_HTTP_RESPONSE_BODY_SIZE: &str = match option_env!("AXUM_HTTP_RESPONSE_BODY_SIZE") { 191 | Some(n) => n, 192 | None => "axum_http_response_body_size", 193 | }; 194 | 195 | #[doc(hidden)] 196 | pub static PREFIXED_HTTP_REQUESTS_TOTAL: OnceLock = OnceLock::new(); 197 | #[doc(hidden)] 198 | pub static PREFIXED_HTTP_REQUESTS_DURATION_SECONDS: OnceLock = OnceLock::new(); 199 | #[doc(hidden)] 200 | pub static PREFIXED_HTTP_REQUESTS_PENDING: OnceLock = OnceLock::new(); 201 | #[doc(hidden)] 202 | pub static PREFIXED_HTTP_RESPONSE_BODY_SIZE: OnceLock = OnceLock::new(); 203 | 204 | use std::borrow::Cow; 205 | use std::collections::HashMap; 206 | use std::marker::PhantomData; 207 | use std::sync::atomic::AtomicBool; 208 | use std::sync::{Arc, OnceLock}; 209 | use std::time::Duration; 210 | use std::time::Instant; 211 | 212 | mod builder; 213 | pub mod lifecycle; 214 | pub mod utils; 215 | use axum::extract::MatchedPath; 216 | pub use builder::EndpointLabel; 217 | pub use builder::MetricLayerBuilder; 218 | #[cfg(feature = "prometheus")] 219 | pub use builder::PrometheusMetricLayerBuilder; 220 | use builder::{LayerOnly, Paired}; 221 | use lifecycle::layer::LifeCycleLayer; 222 | use lifecycle::OnBodyChunk; 223 | use lifecycle::{service::LifeCycle, Callbacks}; 224 | use metrics::{counter, gauge, histogram, Gauge}; 225 | use tower::Layer; 226 | use tower_http::classify::{ClassifiedResponse, SharedClassifier, StatusInRangeAsFailures}; 227 | 228 | #[cfg(feature = "prometheus")] 229 | use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; 230 | 231 | pub use metrics; 232 | #[cfg(feature = "prometheus")] 233 | pub use metrics_exporter_prometheus; 234 | 235 | /// Use a prefix for the metrics instead of `axum`. This will use the following 236 | /// metric names: 237 | /// - `{prefix}_http_requests_total` 238 | /// - `{prefix}_http_requests_pending` 239 | /// - `{prefix}_http_requests_duration_seconds` 240 | /// 241 | /// Note that this will take precedence over environment variables, and can only 242 | /// be called once. Attempts to call this a second time will panic. 243 | fn set_prefix(prefix: impl AsRef) { 244 | PREFIXED_HTTP_REQUESTS_TOTAL 245 | .set(format!("{}_http_requests_total", prefix.as_ref())) 246 | .expect("the prefix has already been set, and can only be set once."); 247 | PREFIXED_HTTP_REQUESTS_DURATION_SECONDS 248 | .set(format!( 249 | "{}_http_requests_duration_seconds", 250 | prefix.as_ref() 251 | )) 252 | .expect("the prefix has already been set, and can only be set once."); 253 | PREFIXED_HTTP_REQUESTS_PENDING 254 | .set(format!("{}_http_requests_pending", prefix.as_ref())) 255 | .expect("the prefix has already been set, and can only be set once."); 256 | PREFIXED_HTTP_RESPONSE_BODY_SIZE 257 | .set(format!("{}_http_response_body_size", prefix.as_ref())) 258 | .expect("the prefix has already been set, and can only be set once."); 259 | } 260 | 261 | /// A marker struct that implements the [`lifecycle::Callbacks`] trait. 262 | #[derive(Clone, Default)] 263 | pub struct Traffic<'a> { 264 | ignore_patterns: matchit::Router<()>, 265 | group_patterns: HashMap<&'a str, matchit::Router<()>>, 266 | endpoint_label: EndpointLabel, 267 | } 268 | 269 | impl<'a> Traffic<'a> { 270 | pub(crate) fn new() -> Self { 271 | Traffic::default() 272 | } 273 | 274 | pub(crate) fn with_ignore_pattern(&mut self, ignore_pattern: &'a str) { 275 | self.ignore_patterns 276 | .insert(ignore_pattern, ()) 277 | .expect("good route specs"); 278 | } 279 | 280 | pub(crate) fn with_ignore_patterns(&mut self, ignore_patterns: &'a [&'a str]) { 281 | for pattern in ignore_patterns { 282 | self.with_ignore_pattern(pattern); 283 | } 284 | } 285 | 286 | pub(crate) fn with_group_patterns_as(&mut self, group_pattern: &'a str, patterns: &'a [&str]) { 287 | self.group_patterns 288 | .entry(group_pattern) 289 | .and_modify(|router| { 290 | for pattern in patterns { 291 | router.insert(*pattern, ()).expect("good route specs"); 292 | } 293 | }) 294 | .or_insert_with(|| { 295 | let mut inner_router = matchit::Router::new(); 296 | for pattern in patterns { 297 | inner_router.insert(*pattern, ()).expect("good route specs"); 298 | } 299 | inner_router 300 | }); 301 | } 302 | 303 | pub(crate) fn ignores(&self, path: &str) -> bool { 304 | self.ignore_patterns.at(path).is_ok() 305 | } 306 | 307 | pub(crate) fn apply_group_pattern(&self, path: &'a str) -> &'a str { 308 | self.group_patterns 309 | .iter() 310 | .find_map(|(&group, router)| router.at(path).ok().and(Some(group))) 311 | .unwrap_or(path) 312 | } 313 | 314 | pub(crate) fn with_endpoint_label_type(&mut self, endpoint_label: EndpointLabel) { 315 | self.endpoint_label = endpoint_label; 316 | } 317 | } 318 | 319 | /// Struct used for storing and calculating information about the current request. 320 | #[derive(Debug, Clone)] 321 | pub struct MetricsData { 322 | pub endpoint: String, 323 | pub start: Instant, 324 | pub method: &'static str, 325 | pub body_size: f64, 326 | // FIXME: Unclear at the moment, maybe just a simple bool could suffice here? 327 | pub(crate) exact_body_size_called: Arc, 328 | } 329 | 330 | #[doc(hidden)] 331 | pub struct Pending(Gauge); 332 | 333 | impl Drop for Pending { 334 | fn drop(&mut self) { 335 | self.0.decrement(1); 336 | } 337 | } 338 | 339 | // The `Pending` struct is behind an Arc to make sure we only drop it once (since we're cloning this across the lifecycle). 340 | type DefaultCallbackData = Option<(MetricsData, Arc)>; 341 | 342 | /// A marker struct that implements [`lifecycle::OnBodyChunk`], so it can be used to track response body sizes. 343 | #[derive(Clone)] 344 | pub struct BodySizeRecorder; 345 | 346 | impl OnBodyChunk for BodySizeRecorder 347 | where 348 | B: bytes::Buf, 349 | { 350 | type Data = DefaultCallbackData; 351 | 352 | #[inline] 353 | fn call(&mut self, body: &B, body_size: Option, data: &mut Self::Data) { 354 | let Some((metrics_data, _pending_guard)) = data else { 355 | return; 356 | }; 357 | // If the exact body size is known ahead of time, we'll just call this whole thing once. 358 | if let Some(exact_size) = body_size { 359 | if !metrics_data 360 | .exact_body_size_called 361 | .swap(true, std::sync::atomic::Ordering::Relaxed) 362 | { 363 | // If the body size is enormous, we lose some precision. It shouldn't matter really. 364 | metrics_data.body_size = exact_size as f64; 365 | body_size_histogram(metrics_data); 366 | } 367 | } else { 368 | // Otherwise, sum all the chunks. 369 | metrics_data.body_size += body.remaining() as f64; 370 | body_size_histogram(metrics_data); 371 | } 372 | } 373 | } 374 | 375 | impl OnBodyChunk for Option 376 | where 377 | T: OnBodyChunk, 378 | B: bytes::Buf, 379 | { 380 | type Data = T::Data; 381 | 382 | fn call(&mut self, body: &B, body_size: Option, data: &mut Self::Data) { 383 | if let Some(this) = self { 384 | T::call(this, body, body_size, data); 385 | } 386 | } 387 | } 388 | 389 | fn body_size_histogram(metrics_data: &MetricsData) { 390 | let labels = &[ 391 | ("method", metrics_data.method.to_owned()), 392 | ("endpoint", metrics_data.endpoint.clone()), 393 | ]; 394 | let response_body_size = PREFIXED_HTTP_RESPONSE_BODY_SIZE 395 | .get() 396 | .map_or(AXUM_HTTP_RESPONSE_BODY_SIZE, |s| s.as_str()); 397 | metrics::histogram!(response_body_size, labels).record(metrics_data.body_size); 398 | } 399 | 400 | impl<'a, FailureClass> Callbacks for Traffic<'a> { 401 | type Data = DefaultCallbackData; 402 | 403 | fn prepare(&mut self, request: &http::Request) -> Self::Data { 404 | let now = std::time::Instant::now(); 405 | let exact_endpoint = request.uri().path(); 406 | if self.ignores(exact_endpoint) { 407 | return None; 408 | } 409 | let endpoint = match self.endpoint_label { 410 | EndpointLabel::Exact => Cow::from(exact_endpoint), 411 | EndpointLabel::MatchedPath => Cow::from( 412 | request 413 | .extensions() 414 | .get::() 415 | .map_or(exact_endpoint, MatchedPath::as_str), 416 | ), 417 | EndpointLabel::MatchedPathWithFallbackFn(fallback_fn) => { 418 | if let Some(mp) = request 419 | .extensions() 420 | .get::() 421 | .map(MatchedPath::as_str) 422 | { 423 | Cow::from(mp) 424 | } else { 425 | Cow::from(fallback_fn(exact_endpoint)) 426 | } 427 | } 428 | }; 429 | let endpoint = self.apply_group_pattern(&endpoint).to_owned(); 430 | let method = utils::as_label(request.method()); 431 | 432 | let pending = gauge!( 433 | utils::requests_pending_name(), 434 | &[ 435 | ("method", method.to_owned()), 436 | ("endpoint", endpoint.clone()), 437 | ] 438 | ); 439 | pending.increment(1); 440 | 441 | Some(( 442 | MetricsData { 443 | endpoint, 444 | start: now, 445 | method, 446 | body_size: 0.0, 447 | exact_body_size_called: Arc::new(AtomicBool::new(false)), 448 | }, 449 | Arc::new(Pending(pending)), 450 | )) 451 | } 452 | 453 | fn on_response( 454 | &mut self, 455 | res: &http::Response, 456 | _cls: ClassifiedResponse, 457 | data: &mut Self::Data, 458 | ) { 459 | if let Some((data, _pending_guard)) = data { 460 | let duration_seconds = data.start.elapsed().as_secs_f64(); 461 | 462 | let labels = [ 463 | ("method", data.method.to_string()), 464 | ("status", res.status().as_u16().to_string()), 465 | ("endpoint", data.endpoint.to_string()), 466 | ]; 467 | 468 | let requests_total = PREFIXED_HTTP_REQUESTS_TOTAL 469 | .get() 470 | .map_or(AXUM_HTTP_REQUESTS_TOTAL, |s| s.as_str()); 471 | counter!(requests_total, &labels).increment(1); 472 | 473 | let requests_duration = PREFIXED_HTTP_REQUESTS_DURATION_SECONDS 474 | .get() 475 | .map_or(AXUM_HTTP_REQUESTS_DURATION_SECONDS, |s| s.as_str()); 476 | histogram!(requests_duration, &labels).record(duration_seconds); 477 | } 478 | } 479 | } 480 | 481 | /// The tower middleware layer for recording HTTP metrics. 482 | /// 483 | /// Unlike [`GenericMetricLayer`], this struct __does not__ know about the metrics exporter, or the recorder. It will only emit 484 | /// metrics via the `metrics` crate's macros. It's entirely up to the user to set the global metrics recorder/exporter before using this. 485 | /// 486 | /// You may use this if `GenericMetricLayer`'s requirements are too strict for your use case. 487 | #[derive(Clone)] 488 | pub struct BaseMetricLayer<'a> { 489 | pub(crate) inner_layer: LifeCycleLayer< 490 | SharedClassifier, 491 | Traffic<'a>, 492 | Option, 493 | >, 494 | } 495 | 496 | impl<'a> BaseMetricLayer<'a> { 497 | /// Construct a new `BaseMetricLayer`. 498 | /// 499 | /// # Example 500 | /// ``` 501 | /// use axum::{routing::get, Router}; 502 | /// use axum_prometheus::{AXUM_HTTP_REQUESTS_DURATION_SECONDS, utils::SECONDS_DURATION_BUCKETS, BaseMetricLayer}; 503 | /// use metrics_exporter_prometheus::{Matcher, PrometheusBuilder}; 504 | /// use std::net::SocketAddr; 505 | /// 506 | /// #[tokio::main] 507 | /// async fn main() { 508 | /// // Initialize the recorder as you like. 509 | /// let metric_handle = PrometheusBuilder::new() 510 | /// .set_buckets_for_metric( 511 | /// Matcher::Full(AXUM_HTTP_REQUESTS_DURATION_SECONDS.to_string()), 512 | /// SECONDS_DURATION_BUCKETS, 513 | /// ) 514 | /// .unwrap() 515 | /// .install_recorder() 516 | /// .unwrap(); 517 | /// 518 | /// let app = Router::<()>::new() 519 | /// .route("/fast", get(|| async {})) 520 | /// .route( 521 | /// "/slow", 522 | /// get(|| async { 523 | /// tokio::time::sleep(std::time::Duration::from_secs(1)).await; 524 | /// }), 525 | /// ) 526 | /// // Expose the metrics somehow to the outer world. 527 | /// .route("/metrics", get(|| async move { metric_handle.render() })) 528 | /// // Only need to add this layer at the end. 529 | /// .layer(BaseMetricLayer::new()); 530 | /// 531 | /// // Run the server as usual: 532 | /// // let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 533 | /// // .await 534 | /// // .unwrap(); 535 | /// // axum::serve(listener, app).await.unwrap() 536 | /// } 537 | /// ``` 538 | pub fn new() -> Self { 539 | let make_classifier = 540 | StatusInRangeAsFailures::new_for_client_and_server_errors().into_make_classifier(); 541 | let inner_layer = LifeCycleLayer::new(make_classifier, Traffic::new(), None); 542 | Self { inner_layer } 543 | } 544 | 545 | /// Construct a new `BaseMetricLayer` with response body size tracking enabled. 546 | pub fn with_response_body_size() -> Self { 547 | let mut this = Self::new(); 548 | this.inner_layer.on_body_chunk(Some(BodySizeRecorder)); 549 | this 550 | } 551 | } 552 | 553 | impl<'a> Default for BaseMetricLayer<'a> { 554 | fn default() -> Self { 555 | Self::new() 556 | } 557 | } 558 | 559 | impl<'a, S> Layer for BaseMetricLayer<'a> { 560 | type Service = LifeCycle< 561 | S, 562 | SharedClassifier, 563 | Traffic<'a>, 564 | Option, 565 | >; 566 | 567 | fn layer(&self, inner: S) -> Self::Service { 568 | self.inner_layer.layer(inner) 569 | } 570 | } 571 | 572 | /// The tower middleware layer for recording http metrics with different exporters. 573 | pub struct GenericMetricLayer<'a, T, M> { 574 | pub(crate) inner_layer: LifeCycleLayer< 575 | SharedClassifier, 576 | Traffic<'a>, 577 | Option, 578 | >, 579 | _marker: PhantomData<(T, M)>, 580 | } 581 | 582 | // We don't require that `T` nor `M` is `Clone`, since none of them is actually contained in this type. 583 | impl<'a, T, M> std::clone::Clone for GenericMetricLayer<'a, T, M> { 584 | fn clone(&self) -> Self { 585 | GenericMetricLayer { 586 | inner_layer: self.inner_layer.clone(), 587 | _marker: self._marker, 588 | } 589 | } 590 | } 591 | 592 | impl<'a, T, M> GenericMetricLayer<'a, T, M> 593 | where 594 | M: MakeDefaultHandle, 595 | { 596 | /// Create a new tower middleware that can be used to track metrics. 597 | /// 598 | /// By default, this __will not__ "install" the exporter which sets it as the 599 | /// global recorder for all `metrics` calls. 600 | /// If you're using Prometheus, here you can use [`metrics_exporter_prometheus::PrometheusBuilder`] 601 | /// to build your own customized metrics exporter. 602 | /// 603 | /// This middleware is using the following constants for identifying different HTTP metrics: 604 | /// 605 | /// - [`AXUM_HTTP_REQUESTS_PENDING`] 606 | /// - [`AXUM_HTTP_REQUESTS_TOTAL`] 607 | /// - [`AXUM_HTTP_REQUESTS_DURATION_SECONDS`]. 608 | /// 609 | /// In terms of setup, the most important one is [`AXUM_HTTP_REQUESTS_DURATION_SECONDS`], which is a histogram metric 610 | /// used for request latency. You may set customized buckets tailored for your used case here. 611 | /// 612 | /// # Example 613 | /// ``` 614 | /// use axum::{routing::get, Router}; 615 | /// use axum_prometheus::{AXUM_HTTP_REQUESTS_DURATION_SECONDS, utils::SECONDS_DURATION_BUCKETS, PrometheusMetricLayer}; 616 | /// use metrics_exporter_prometheus::{Matcher, PrometheusBuilder}; 617 | /// use std::net::SocketAddr; 618 | /// 619 | /// #[tokio::main] 620 | /// async fn main() { 621 | /// let metric_layer = PrometheusMetricLayer::new(); 622 | /// // This is the default if you use `PrometheusMetricLayer::pair`. 623 | /// let metric_handle = PrometheusBuilder::new() 624 | /// .set_buckets_for_metric( 625 | /// Matcher::Full(AXUM_HTTP_REQUESTS_DURATION_SECONDS.to_string()), 626 | /// SECONDS_DURATION_BUCKETS, 627 | /// ) 628 | /// .unwrap() 629 | /// .install_recorder() 630 | /// .unwrap(); 631 | /// 632 | /// let app = Router::<()>::new() 633 | /// .route("/fast", get(|| async {})) 634 | /// .route( 635 | /// "/slow", 636 | /// get(|| async { 637 | /// tokio::time::sleep(std::time::Duration::from_secs(1)).await; 638 | /// }), 639 | /// ) 640 | /// .route("/metrics", get(|| async move { metric_handle.render() })) 641 | /// .layer(metric_layer); 642 | /// 643 | /// // Run the server as usual: 644 | /// // let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 645 | /// // .await 646 | /// // .unwrap(); 647 | /// // axum::serve(listener, app).await.unwrap() 648 | /// } 649 | /// ``` 650 | pub fn new() -> Self { 651 | let make_classifier = 652 | StatusInRangeAsFailures::new_for_client_and_server_errors().into_make_classifier(); 653 | let inner_layer = LifeCycleLayer::new(make_classifier, Traffic::new(), None); 654 | Self { 655 | inner_layer, 656 | _marker: PhantomData, 657 | } 658 | } 659 | 660 | pub(crate) fn from_builder(builder: MetricLayerBuilder<'a, T, M, LayerOnly>) -> Self { 661 | let make_classifier = 662 | StatusInRangeAsFailures::new_for_client_and_server_errors().into_make_classifier(); 663 | let inner_layer = if builder.enable_body_size { 664 | LifeCycleLayer::new(make_classifier, builder.traffic, Some(BodySizeRecorder)) 665 | } else { 666 | LifeCycleLayer::new(make_classifier, builder.traffic, None) 667 | }; 668 | Self { 669 | inner_layer, 670 | _marker: PhantomData, 671 | } 672 | } 673 | 674 | /// Enable tracking response body sizes. 675 | pub fn enable_response_body_size(&mut self) { 676 | self.inner_layer.on_body_chunk(Some(BodySizeRecorder)); 677 | } 678 | 679 | /// Crate a new tower middleware and a default exporter from the provided value of the passed in argument. 680 | /// 681 | /// This function is useful when additional data needs to be injected into `MakeDefaultHandle::make_default_handle`. 682 | /// 683 | /// # Example 684 | /// 685 | /// ```rust,no_run 686 | /// use axum_prometheus::{GenericMetricLayer, MakeDefaultHandle}; 687 | /// 688 | /// struct Recorder { host: String } 689 | /// 690 | /// impl MakeDefaultHandle for Recorder { 691 | /// type Out = (); 692 | /// 693 | /// fn make_default_handle(self) -> Self::Out { 694 | /// // Perform the initialization. `self` is passed in by value. 695 | /// todo!(); 696 | /// } 697 | /// } 698 | /// 699 | /// fn main() { 700 | /// let (metric_layer, metric_handle) = GenericMetricLayer::pair_from( 701 | /// Recorder { host: "0.0.0.0".to_string() } 702 | /// ); 703 | /// } 704 | /// ``` 705 | pub fn pair_from(m: M) -> (Self, T) { 706 | (Self::new(), M::make_default_handle(m)) 707 | } 708 | } 709 | 710 | impl<'a, T, M> GenericMetricLayer<'a, T, M> 711 | where 712 | M: MakeDefaultHandle + Default, 713 | { 714 | pub(crate) fn pair_from_builder(builder: MetricLayerBuilder<'a, T, M, Paired>) -> (Self, T) { 715 | let make_classifier = 716 | StatusInRangeAsFailures::new_for_client_and_server_errors().into_make_classifier(); 717 | let inner_layer = if builder.enable_body_size { 718 | LifeCycleLayer::new(make_classifier, builder.traffic, Some(BodySizeRecorder)) 719 | } else { 720 | LifeCycleLayer::new(make_classifier, builder.traffic, None) 721 | }; 722 | 723 | ( 724 | Self { 725 | inner_layer, 726 | _marker: PhantomData, 727 | }, 728 | builder 729 | .metric_handle 730 | .unwrap_or_else(|| M::make_default_handle(M::default())), 731 | ) 732 | } 733 | 734 | /// Crate a new tower middleware and a default global Prometheus exporter with sensible defaults. 735 | /// 736 | /// If used with a custom exporter that's different from Prometheus, the exporter struct 737 | /// must implement `MakeDefaultHandle + Default`. 738 | /// 739 | /// # Example 740 | /// ``` 741 | /// use axum::{routing::get, Router}; 742 | /// use axum_prometheus::PrometheusMetricLayer; 743 | /// use std::net::SocketAddr; 744 | /// 745 | /// #[tokio::main] 746 | /// async fn main() { 747 | /// let (metric_layer, metric_handle) = PrometheusMetricLayer::pair(); 748 | /// 749 | /// let app = Router::<()>::new() 750 | /// .route("/fast", get(|| async {})) 751 | /// .route( 752 | /// "/slow", 753 | /// get(|| async { 754 | /// tokio::time::sleep(std::time::Duration::from_secs(1)).await; 755 | /// }), 756 | /// ) 757 | /// .route("/metrics", get(|| async move { metric_handle.render() })) 758 | /// .layer(metric_layer); 759 | /// 760 | /// // Run the server as usual: 761 | /// // let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) 762 | /// // .await 763 | /// // .unwrap(); 764 | /// // axum::serve(listener, app).await.unwrap() 765 | /// } 766 | /// ``` 767 | pub fn pair() -> (Self, T) { 768 | (Self::new(), M::make_default_handle(M::default())) 769 | } 770 | } 771 | 772 | impl<'a, T, M> Default for GenericMetricLayer<'a, T, M> 773 | where 774 | M: MakeDefaultHandle, 775 | { 776 | fn default() -> Self { 777 | Self::new() 778 | } 779 | } 780 | 781 | impl<'a, S, T, M> Layer for GenericMetricLayer<'a, T, M> { 782 | type Service = LifeCycle< 783 | S, 784 | SharedClassifier, 785 | Traffic<'a>, 786 | Option, 787 | >; 788 | 789 | fn layer(&self, inner: S) -> Self::Service { 790 | self.inner_layer.layer(inner) 791 | } 792 | } 793 | 794 | /// The trait that allows to use a metrics exporter in `GenericMetricLayer`. 795 | pub trait MakeDefaultHandle { 796 | /// The type of the metrics handle to return from [`MetricLayerBuilder`]. 797 | type Out; 798 | 799 | /// The function that defines how to initialize a metric exporter by default. 800 | /// 801 | /// # Example 802 | /// 803 | /// ```rust, no_run 804 | /// use axum_prometheus::{MakeDefaultHandle, GenericMetricLayer}; 805 | /// 806 | /// pub struct MyHandle(pub String); 807 | /// 808 | /// impl MakeDefaultHandle for MyHandle { 809 | /// type Out = (); 810 | /// 811 | /// fn make_default_handle(self) -> Self::Out { 812 | /// // This is where you initialize and register everything you need. 813 | /// // Notice that self is passed in by value. 814 | /// } 815 | /// } 816 | /// ``` 817 | /// and then, to use it: 818 | /// ```rust,ignore 819 | /// // Initialize the struct, then use `pair_from`. 820 | /// let my_handle = MyHandle(String::from("localhost")); 821 | /// let (layer, handle) = GenericMetricLayer::pair_from(my_handle); 822 | /// 823 | /// // Or optionally if your custom struct implements `Default` too, you may call `pair`. 824 | /// // That's going to use `MyHandle::default()`. 825 | /// let (layer, handle) = GenericMetricLayer::<'_, _, MyHandle>::pair(); 826 | /// ``` 827 | fn make_default_handle(self) -> Self::Out; 828 | } 829 | 830 | /// The default handle for the Prometheus exporter. 831 | #[cfg(feature = "prometheus")] 832 | #[derive(Clone)] 833 | pub struct Handle(pub PrometheusHandle); 834 | 835 | #[cfg(feature = "prometheus")] 836 | impl Default for Handle { 837 | fn default() -> Self { 838 | let recorder = PrometheusBuilder::new() 839 | .set_buckets_for_metric( 840 | Matcher::Full( 841 | PREFIXED_HTTP_REQUESTS_DURATION_SECONDS 842 | .get() 843 | .map_or(AXUM_HTTP_REQUESTS_DURATION_SECONDS, |s| s.as_str()) 844 | .to_string(), 845 | ), 846 | utils::SECONDS_DURATION_BUCKETS, 847 | ) 848 | .unwrap() 849 | .build_recorder(); 850 | let handle = recorder.handle(); 851 | let recorder_handle = handle.clone(); 852 | tokio::spawn(async move { 853 | loop { 854 | tokio::time::sleep(Duration::from_secs(5)).await; 855 | recorder_handle.run_upkeep(); 856 | } 857 | }); 858 | metrics::set_global_recorder(recorder).expect("Failed to set global recorder"); 859 | Self(handle) 860 | } 861 | } 862 | 863 | #[cfg(feature = "prometheus")] 864 | impl MakeDefaultHandle for Handle { 865 | type Out = PrometheusHandle; 866 | 867 | fn make_default_handle(self) -> Self::Out { 868 | self.0 869 | } 870 | } 871 | 872 | #[cfg(feature = "prometheus")] 873 | /// The tower middleware layer for recording http metrics with Prometheus. 874 | pub type PrometheusMetricLayer<'a> = GenericMetricLayer<'a, PrometheusHandle, Handle>; 875 | -------------------------------------------------------------------------------- /src/lifecycle/body.rs: -------------------------------------------------------------------------------- 1 | use super::{Callbacks, FailedAt, OnBodyChunk}; 2 | use futures_core::ready; 3 | use http::HeaderValue; 4 | use http_body::{Body, Frame}; 5 | use pin_project_lite::pin_project; 6 | use std::{ 7 | fmt, 8 | pin::Pin, 9 | task::{Context, Poll}, 10 | }; 11 | use tower_http::classify::ClassifyEos; 12 | 13 | pin_project! { 14 | /// Response body for [`LifeCycle`]. 15 | pub struct ResponseBody { 16 | #[pin] 17 | pub(super) inner: B, 18 | pub(super) parts: Option<(C, Callbacks)>, 19 | pub(super) callbacks_data: CallbacksData, 20 | pub(super) on_body_chunk: OnBodyChunk, 21 | pub(super) content_length: Option, 22 | } 23 | } 24 | 25 | impl Body 26 | for ResponseBody 27 | where 28 | B: Body, 29 | B::Error: fmt::Display + 'static, 30 | C: ClassifyEos, 31 | CallbacksT: Callbacks, 32 | OnBodyChunkT: OnBodyChunk, 33 | CallbacksData: Clone, 34 | { 35 | type Data = B::Data; 36 | type Error = B::Error; 37 | 38 | fn poll_frame( 39 | self: Pin<&mut Self>, 40 | cx: &mut Context<'_>, 41 | ) -> Poll, Self::Error>>> { 42 | let this = self.project(); 43 | 44 | let body_size = this.inner.size_hint().exact().or_else(|| { 45 | this.content_length 46 | .as_ref() 47 | .and_then(|cl| cl.to_str().ok()) 48 | .and_then(|cl| cl.parse().ok()) 49 | }); 50 | let result = ready!(this.inner.poll_frame(cx)); 51 | 52 | match result { 53 | Some(Ok(frame)) => { 54 | let frame = match frame.into_data() { 55 | Ok(chunk) => { 56 | this.on_body_chunk 57 | .call(&chunk, body_size, this.callbacks_data); 58 | Frame::data(chunk) 59 | } 60 | Err(frame) => frame, 61 | }; 62 | 63 | let frame = match frame.into_trailers() { 64 | Ok(trailers) => { 65 | if let Some((classify_eos, callbacks)) = this.parts.take() { 66 | let classification = classify_eos.classify_eos(Some(&trailers)); 67 | callbacks.on_eos( 68 | Some(&trailers), 69 | classification, 70 | this.callbacks_data.clone(), 71 | ); 72 | } 73 | Frame::trailers(trailers) 74 | } 75 | Err(frame) => frame, 76 | }; 77 | 78 | Poll::Ready(Some(Ok(frame))) 79 | } 80 | Some(Err(err)) => { 81 | if let Some((classify_eos, callbacks)) = this.parts.take() { 82 | let classification = classify_eos.classify_error(&err); 83 | callbacks.on_failure(FailedAt::Body, classification, this.callbacks_data); 84 | } 85 | 86 | Poll::Ready(Some(Err(err))) 87 | } 88 | None => { 89 | if let Some((classify_eos, callbacks)) = this.parts.take() { 90 | let classification = classify_eos.classify_eos(None); 91 | callbacks.on_eos(None, classification, this.callbacks_data.clone()); 92 | } 93 | Poll::Ready(None) 94 | } 95 | } 96 | } 97 | 98 | fn is_end_stream(&self) -> bool { 99 | self.inner.is_end_stream() 100 | } 101 | 102 | fn size_hint(&self) -> http_body::SizeHint { 103 | self.inner.size_hint() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/lifecycle/future.rs: -------------------------------------------------------------------------------- 1 | use futures_core::ready; 2 | use http::response::Response; 3 | use http_body::Body; 4 | use pin_project_lite::pin_project; 5 | use std::{ 6 | future::Future, 7 | pin::Pin, 8 | task::{Context, Poll}, 9 | }; 10 | use tower_http::classify::{ClassifiedResponse, ClassifyResponse}; 11 | 12 | use super::{body::ResponseBody, Callbacks, FailedAt, OnBodyChunk}; 13 | 14 | pin_project! { 15 | pub struct ResponseFuture { 16 | #[pin] 17 | pub(super) inner: F, 18 | pub(super) classifier: Option, 19 | pub(super) callbacks: Option, 20 | pub(super) on_body_chunk: Option, 21 | pub(super) callbacks_data: Option, 22 | }} 23 | 24 | impl Future 25 | for ResponseFuture 26 | where 27 | F: Future, E>>, 28 | ResBody: Body, 29 | C: ClassifyResponse, 30 | CallbacksT: Callbacks, 31 | E: std::fmt::Display + 'static, 32 | OnBodyChunkT: OnBodyChunk, 33 | CallbacksData: Clone, 34 | { 35 | type Output = Result< 36 | Response>, 37 | E, 38 | >; 39 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 40 | let this = self.project(); 41 | let result = ready!(this.inner.poll(cx)); 42 | 43 | let classifier = this 44 | .classifier 45 | .take() 46 | .expect("polled future after completion"); 47 | let mut callbacks = this 48 | .callbacks 49 | .take() 50 | .expect("polled future after completion"); 51 | let mut callbacks_data = this 52 | .callbacks_data 53 | .take() 54 | .expect("polled future after completion"); 55 | let on_body_chunk = this 56 | .on_body_chunk 57 | .take() 58 | .expect("polled future after completion"); 59 | 60 | match result { 61 | Ok(res) => { 62 | let content_length = res.headers().get(http::header::CONTENT_LENGTH).cloned(); 63 | let classification = classifier.classify_response(&res); 64 | 65 | match classification { 66 | ClassifiedResponse::Ready(classification) => { 67 | callbacks.on_response( 68 | &res, 69 | ClassifiedResponse::Ready(classification), 70 | &mut callbacks_data, 71 | ); 72 | let res = res.map(|body| ResponseBody { 73 | inner: body, 74 | parts: None, 75 | on_body_chunk, 76 | callbacks_data: callbacks_data.clone(), 77 | content_length, 78 | }); 79 | Poll::Ready(Ok(res)) 80 | } 81 | ClassifiedResponse::RequiresEos(classify_eos) => { 82 | callbacks.on_response( 83 | &res, 84 | ClassifiedResponse::RequiresEos(()), 85 | &mut callbacks_data, 86 | ); 87 | let res = res.map(|body| ResponseBody { 88 | inner: body, 89 | callbacks_data: callbacks_data.clone(), 90 | on_body_chunk, 91 | parts: Some((classify_eos, callbacks)), 92 | content_length, 93 | }); 94 | Poll::Ready(Ok(res)) 95 | } 96 | } 97 | } 98 | Err(err) => { 99 | let classification = classifier.classify_error(&err); 100 | callbacks.on_failure(FailedAt::Response, classification, &mut callbacks_data); 101 | Poll::Ready(Err(err)) 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/lifecycle/layer.rs: -------------------------------------------------------------------------------- 1 | use tower::Layer; 2 | 3 | use super::service::LifeCycle; 4 | 5 | /// [`Layer`] for adding callbacks to the lifecycle of request. 6 | /// 7 | /// See the [module docs](crate::lifecycle) for more details. 8 | /// 9 | /// [`Layer`]: tower::Layer 10 | #[derive(Debug, Clone)] 11 | pub struct LifeCycleLayer { 12 | pub(super) make_classifier: MC, 13 | pub(super) callbacks: Callbacks, 14 | pub(super) on_body_chunk: OnBodyChunk, 15 | } 16 | 17 | impl LifeCycleLayer { 18 | /// Create a new `LifeCycleLayer`. 19 | pub fn new(make_classifier: MC, callbacks: Callbacks, on_body_chunk: OnBodyChunk) -> Self { 20 | LifeCycleLayer { 21 | make_classifier, 22 | callbacks, 23 | on_body_chunk, 24 | } 25 | } 26 | 27 | pub(crate) fn on_body_chunk(&mut self, on_body_chunk: OnBodyChunk) { 28 | self.on_body_chunk = on_body_chunk; 29 | } 30 | } 31 | 32 | impl Layer for LifeCycleLayer 33 | where 34 | MC: Clone, 35 | Callbacks: Clone, 36 | OnBodyChunk: Clone, 37 | { 38 | type Service = LifeCycle; 39 | 40 | fn layer(&self, inner: S) -> Self::Service { 41 | LifeCycle { 42 | inner, 43 | make_classifier: self.make_classifier.clone(), 44 | callbacks: self.callbacks.clone(), 45 | on_body_chunk: self.on_body_chunk.clone(), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lifecycle/mod.rs: -------------------------------------------------------------------------------- 1 | //! Request lifecycle hooks that can be used to further customize how and what callbacks to run 2 | //! on events. 3 | //! 4 | //! `axum-prometheus` is built on top of lifecycle hooks. Using this module allows you to customize 5 | //! behavior even more. 6 | use bytes::Buf; 7 | use http::{HeaderMap, Request, Response}; 8 | use tower_http::classify::ClassifiedResponse; 9 | 10 | mod body; 11 | mod future; 12 | pub mod layer; 13 | pub mod service; 14 | 15 | /// Trait that defines callbacks for [`LifeCycle`] to call. 16 | /// 17 | /// [`LifeCycle`]: service::LifeCycle 18 | pub trait Callbacks: Sized { 19 | /// Additional data to attach to callbacks. 20 | type Data; 21 | 22 | /// Create an instance of `Self::Data` from the request. 23 | /// 24 | /// This method is called immediately after the request is received by [`Service::call`]. 25 | /// 26 | /// The value returned here will be passed to the other methods in this trait. 27 | /// 28 | /// [`Service::call`]: tower::Service::call 29 | fn prepare(&mut self, request: &Request) -> Self::Data; 30 | 31 | /// Perform some action when a response has been generated. 32 | /// 33 | /// This method is called when the inner [`Service`]'s response future 34 | /// completes with `Ok(response)`, regardless if the response is classified 35 | /// as a success or a failure. 36 | /// 37 | /// If the response is the start of a stream (as determined by the 38 | /// classifier passed to [`LifeCycle::new`] or [`LifeCycleLayer::new`]) then 39 | /// `classification` will be [`ClassifiedResponse::RequiresEos(())`], 40 | /// otherwise it will be [`ClassifiedResponse::Ready`]. 41 | /// 42 | /// The default implementation does nothing and returns immediately. 43 | /// 44 | /// [`ClassifiedResponse::RequiresEos(())`]: tower_http::classify::ClassifiedResponse::RequiresEos 45 | /// [`Service`]: tower::Service 46 | /// [`LifeCycle::new`]: service::LifeCycle::new 47 | /// [`LifeCycleLayer::new`]: layer::LifeCycleLayer::new 48 | #[inline] 49 | fn on_response( 50 | &mut self, 51 | _response: &Response, 52 | _classification: ClassifiedResponse, 53 | _data: &mut Self::Data, 54 | ) { 55 | } 56 | 57 | /// Perform some action when a stream has ended. 58 | /// 59 | /// This is called when [`Body::poll_frame`] produces `Ok(trailers)` with the [`Frame::into_trailers`] method, 60 | /// regardless if the trailers are classified as a failure. 61 | /// 62 | /// A stream that ends successfully will trigger two callbacks. 63 | /// [`on_response`] will be called once the response has been generated and 64 | /// the stream has started and [`on_eos`] will be called once the stream has 65 | /// ended. 66 | /// 67 | /// If the trailers were classified as a success then `classification` will 68 | /// be `Ok(())` otherwise `Err(failure_class)`. 69 | /// 70 | /// The default implementation does nothing and returns immediately. 71 | /// 72 | /// [`on_response`]: Callbacks::on_response 73 | /// [`on_eos`]: Callbacks::on_eos 74 | /// [`Body::poll_frame`]: http_body::Body::poll_frame 75 | /// [`Frame::into_trailers`]: http_body::Frame::into_trailers 76 | #[inline] 77 | fn on_eos( 78 | self, 79 | _trailers: Option<&HeaderMap>, 80 | _classification: Result<(), FailureClass>, 81 | _data: Self::Data, 82 | ) { 83 | } 84 | 85 | /// Perform some action when an error has been encountered. 86 | /// 87 | /// This method is only called in the following scenarios: 88 | /// 89 | /// - The inner [`Service`]'s response future resolves to an error. 90 | /// - [`Body::poll_frame`] returns an error. 91 | /// 92 | /// That means this method is _not_ called if a response is classified as a 93 | /// failure (then [`on_response`] is called) or an end-of-stream is 94 | /// classified as a failure (then [`on_eos`] is called). 95 | /// 96 | /// `failed_at` specifies where the error happened. 97 | /// 98 | /// The default implementation does nothing and returns immediately. 99 | /// 100 | /// [`Service`]: tower::Service 101 | /// [`on_response`]: Callbacks::on_response 102 | /// [`on_eos`]: Callbacks::on_eos 103 | /// [`Service::call`]: tower::Service::call 104 | /// [`Body::poll_frame`]: http_body::Body::poll_frame 105 | fn on_failure( 106 | self, 107 | _failed_at: FailedAt, 108 | _failure_classification: FailureClass, 109 | _data: &mut Self::Data, 110 | ) { 111 | } 112 | } 113 | 114 | /// A trait that allows to hook into [`http_body::Body::poll_frame`]'s lifecycle. 115 | pub trait OnBodyChunk { 116 | type Data; 117 | 118 | /// Perform some action when a response body chunk has been generated. 119 | /// 120 | /// This is called when [`Body::poll_frame`] returns `Some(Ok(frame))`, and [`Frame::into_data`] returns with `Some(chunk)`, 121 | /// regardless if the chunk is empty or not. 122 | /// 123 | /// The default implementation does nothing and returns immediately. 124 | /// 125 | /// [`Body::poll_frame`]: http_body::Body::poll_frame 126 | /// [`Frame::into_data`]: http_body::Frame::into_data 127 | #[inline] 128 | fn call(&mut self, _body: &B, _exact_body_size: Option, _data: &mut Self::Data) {} 129 | } 130 | 131 | /// Enum used to specify where an error was encountered. 132 | #[derive(Debug)] 133 | pub enum FailedAt { 134 | /// Generating the response failed. 135 | Response, 136 | /// Generating the response body failed. 137 | Body, 138 | /// Generating the response trailers failed. 139 | Trailers, 140 | } 141 | -------------------------------------------------------------------------------- /src/lifecycle/service.rs: -------------------------------------------------------------------------------- 1 | use std::task::{Context, Poll}; 2 | 3 | use http::{Request, Response}; 4 | use http_body::Body; 5 | use tower::Service; 6 | use tower_http::classify::MakeClassifier; 7 | 8 | use super::{ 9 | body::ResponseBody, future::ResponseFuture, layer::LifeCycleLayer, Callbacks, OnBodyChunk, 10 | }; 11 | 12 | #[derive(Clone, Debug)] 13 | pub struct LifeCycle { 14 | pub(super) inner: S, 15 | pub(super) make_classifier: MC, 16 | pub(super) callbacks: Callbacks, 17 | pub(super) on_body_chunk: OnBodyChunk, 18 | } 19 | 20 | impl LifeCycle { 21 | pub fn new( 22 | inner: S, 23 | make_classifier: MC, 24 | callbacks: Callbacks, 25 | on_body_chunk: OnBodyChunk, 26 | ) -> Self { 27 | Self { 28 | inner, 29 | make_classifier, 30 | callbacks, 31 | on_body_chunk, 32 | } 33 | } 34 | 35 | pub fn layer( 36 | make_classifier: MC, 37 | callbacks: Callbacks, 38 | on_body_chunk: OnBodyChunk, 39 | ) -> LifeCycleLayer { 40 | LifeCycleLayer::new(make_classifier, callbacks, on_body_chunk) 41 | } 42 | 43 | /// Gets a reference to the underlying service. 44 | pub fn get_ref(&self) -> &S { 45 | &self.inner 46 | } 47 | 48 | /// Gets a mutable reference to the underlying service. 49 | pub fn get_mut(&mut self) -> &mut S { 50 | &mut self.inner 51 | } 52 | 53 | /// Consumes `self`, returning the underlying service. 54 | pub fn into_inner(self) -> S { 55 | self.inner 56 | } 57 | } 58 | 59 | impl Service> 60 | for LifeCycle 61 | where 62 | S: Service, Response = Response>, 63 | ResBody: Body, 64 | MC: MakeClassifier, 65 | CallbacksT: Callbacks + Clone, 66 | S::Error: std::fmt::Display + 'static, 67 | OnBodyChunkT: OnBodyChunk + Clone, 68 | CallbacksT::Data: Clone, 69 | { 70 | type Response = Response< 71 | ResponseBody, 72 | >; 73 | type Error = S::Error; 74 | type Future = 75 | ResponseFuture; 76 | 77 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 78 | self.inner.poll_ready(cx) 79 | } 80 | 81 | fn call(&mut self, req: Request) -> Self::Future { 82 | let callbacks_data = self.callbacks.prepare(&req); 83 | 84 | let classifier = self.make_classifier.make_classifier(&req); 85 | 86 | ResponseFuture { 87 | inner: self.inner.call(req), 88 | classifier: Some(classifier), 89 | callbacks: Some(self.callbacks.clone()), 90 | callbacks_data: Some(callbacks_data), 91 | on_body_chunk: Some(self.on_body_chunk.clone()), 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for getting metric names at runtime, and other helpers. 2 | use http::Method; 3 | 4 | use crate::{ 5 | AXUM_HTTP_REQUESTS_DURATION_SECONDS, AXUM_HTTP_REQUESTS_PENDING, AXUM_HTTP_REQUESTS_TOTAL, 6 | AXUM_HTTP_RESPONSE_BODY_SIZE, PREFIXED_HTTP_REQUESTS_DURATION_SECONDS, 7 | PREFIXED_HTTP_REQUESTS_PENDING, PREFIXED_HTTP_REQUESTS_TOTAL, PREFIXED_HTTP_RESPONSE_BODY_SIZE, 8 | }; 9 | 10 | /// Standard HTTP request duration buckets measured in seconds. The default buckets are tailored to broadly 11 | /// measure the response time of a network service. Most likely, however, you will be required to define 12 | /// buckets customized to your use case. 13 | pub const SECONDS_DURATION_BUCKETS: &[f64; 11] = &[ 14 | 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 15 | ]; 16 | 17 | pub(super) const fn as_label(method: &Method) -> &'static str { 18 | match *method { 19 | Method::OPTIONS => "OPTIONS", 20 | Method::GET => "GET", 21 | Method::POST => "POST", 22 | Method::PUT => "PUT", 23 | Method::DELETE => "DELETE", 24 | Method::HEAD => "HEAD", 25 | Method::TRACE => "TRACE", 26 | Method::CONNECT => "CONNECT", 27 | Method::PATCH => "PATCH", 28 | _ => "", 29 | } 30 | } 31 | 32 | /// The name of the requests total metric. By default, it's the same as [`AXUM_HTTP_REQUESTS_TOTAL`], but 33 | /// can be changed via the [`with_prefix`] function. 34 | /// 35 | /// [`with_prefix`]: crate::MetricLayerBuilder::with_prefix 36 | pub fn requests_total_name() -> &'static str { 37 | PREFIXED_HTTP_REQUESTS_TOTAL 38 | .get() 39 | .map_or(AXUM_HTTP_REQUESTS_TOTAL, |s| s.as_str()) 40 | } 41 | 42 | /// The name of the requests duration metric. By default, it's the same as [`AXUM_HTTP_REQUESTS_DURATION_SECONDS`], but 43 | /// can be changed via the [`with_prefix`] function. 44 | /// 45 | /// [`with_prefix`]: crate::MetricLayerBuilder::with_prefix 46 | pub fn requests_duration_name() -> &'static str { 47 | PREFIXED_HTTP_REQUESTS_DURATION_SECONDS 48 | .get() 49 | .map_or(AXUM_HTTP_REQUESTS_DURATION_SECONDS, |s| s.as_str()) 50 | } 51 | 52 | /// The name of the requests pending metric. By default, it's the same as [`AXUM_HTTP_REQUESTS_PENDING`], but 53 | /// can be changed via the [`with_prefix`] function. 54 | /// 55 | /// [`with_prefix`]: crate::MetricLayerBuilder::with_prefix 56 | pub fn requests_pending_name() -> &'static str { 57 | PREFIXED_HTTP_REQUESTS_PENDING 58 | .get() 59 | .map_or(AXUM_HTTP_REQUESTS_PENDING, |s| s.as_str()) 60 | } 61 | 62 | /// The name of the response body size metric. By default, it's the same as [`AXUM_HTTP_RESPONSE_BODY_SIZE`], but 63 | /// can be changed via the [`with_prefix`] function. 64 | /// 65 | /// [`with_prefix`]: crate::MetricLayerBuilder::with_prefix 66 | pub fn response_body_size_name() -> &'static str { 67 | PREFIXED_HTTP_RESPONSE_BODY_SIZE 68 | .get() 69 | .map_or(AXUM_HTTP_RESPONSE_BODY_SIZE, |s| s.as_str()) 70 | } 71 | -------------------------------------------------------------------------------- /tests/base.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::{echo, BoxBody}; 3 | use http::Request; 4 | 5 | use tower::{Service, ServiceBuilder, ServiceExt}; 6 | 7 | #[tokio::test] 8 | async fn metric_handle_rendered_correctly() { 9 | let (layer, handle) = axum_prometheus::PrometheusMetricLayer::pair(); 10 | 11 | let mut service = ServiceBuilder::new().layer(layer).service_fn(echo); 12 | let req = Request::builder().body(BoxBody::default()).unwrap(); 13 | let _res = service.ready().await.unwrap().call(req).await.unwrap(); 14 | insta::with_settings!({ 15 | filters => vec![ 16 | ( 17 | r"\b[-+]?[0-9]*\.?[0-9]+\b\\naxum_http_requests_duration_seconds_count", 18 | "", 19 | ) 20 | ] 21 | }, 22 | { 23 | 24 | insta::assert_yaml_snapshot!(handle.render()); 25 | } 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use http::{Request, Response}; 3 | use http_body_util::BodyExt; 4 | use tower::BoxError; 5 | 6 | pub async fn echo(req: Request) -> Result, BoxError> { 7 | Ok(Response::new(req.into_body())) 8 | } 9 | 10 | pub type BoxBody = http_body_util::combinators::UnsyncBoxBody; 11 | 12 | #[derive(Debug)] 13 | pub struct Body(BoxBody); 14 | 15 | impl Body { 16 | pub(crate) fn new(body: B) -> Self 17 | where 18 | B: http_body::Body + Send + 'static, 19 | B::Error: Into, 20 | { 21 | Self(body.map_err(Into::into).boxed_unsync()) 22 | } 23 | 24 | pub(crate) fn empty() -> Self { 25 | Self::new(http_body_util::Empty::new()) 26 | } 27 | } 28 | 29 | impl Default for Body { 30 | fn default() -> Self { 31 | Self::empty() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/prefix.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::{echo, BoxBody}; 3 | 4 | use http::Request; 5 | use tower::{Service, ServiceBuilder, ServiceExt}; 6 | 7 | #[tokio::test] 8 | async fn metric_handle_rendered_correctly_with_prefix() { 9 | let (layer, handle) = axum_prometheus::PrometheusMetricLayerBuilder::new() 10 | .with_prefix("pref") 11 | .with_default_metrics() 12 | .build_pair(); 13 | 14 | let mut service = ServiceBuilder::new().layer(layer).service_fn(echo); 15 | 16 | let req = Request::builder().body(BoxBody::default()).unwrap(); 17 | let _res = service.ready().await.unwrap().call(req).await.unwrap(); 18 | insta::with_settings!({ 19 | filters => 20 | vec![ 21 | ( 22 | r"\b[-+]?[0-9]*\.?[0-9]+\b\\npref_http_requests_duration_seconds_count", 23 | "", 24 | ) 25 | ] 26 | }, 27 | { 28 | 29 | insta::assert_yaml_snapshot!(handle.render()); 30 | } 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /tests/snapshots/base__metric_handle_rendered_correctly.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/base.rs 3 | expression: handle.render() 4 | --- 5 | "# TYPE axum_http_requests_total counter\naxum_http_requests_total{method=\"GET\",status=\"200\",endpoint=\"/\"} 1\n\n# TYPE axum_http_requests_pending gauge\naxum_http_requests_pending{method=\"GET\",endpoint=\"/\"} 1\n\n# TYPE axum_http_requests_duration_seconds histogram\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.005\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.01\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.025\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.05\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.1\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.25\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.5\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"1\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"2.5\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"5\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"10\"} 1\naxum_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"+Inf\"} 1\naxum_http_requests_duration_seconds_sum{method=\"GET\",status=\"200\",endpoint=\"/\"} {method=\"GET\",status=\"200\",endpoint=\"/\"} 1\n\n" 6 | -------------------------------------------------------------------------------- /tests/snapshots/prefix__metric_handle_rendered_correctly_with_prefix.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/prefix.rs 3 | expression: handle.render() 4 | --- 5 | "# TYPE pref_http_requests_total counter\npref_http_requests_total{method=\"GET\",status=\"200\",endpoint=\"/\"} 1\n\n# TYPE pref_http_requests_pending gauge\npref_http_requests_pending{method=\"GET\",endpoint=\"/\"} 1\n\n# TYPE pref_http_requests_duration_seconds histogram\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.005\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.01\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.025\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.05\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.1\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.25\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"0.5\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"1\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"2.5\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"5\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"10\"} 1\npref_http_requests_duration_seconds_bucket{method=\"GET\",status=\"200\",endpoint=\"/\",le=\"+Inf\"} 1\npref_http_requests_duration_seconds_sum{method=\"GET\",status=\"200\",endpoint=\"/\"} {method=\"GET\",status=\"200\",endpoint=\"/\"} 1\n\n" 6 | --------------------------------------------------------------------------------