├── .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 |
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