├── .gitignore ├── trace.png ├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── src ├── middleware │ ├── mod.rs │ ├── route_formatter.rs │ ├── trace.rs │ └── metrics.rs ├── util.rs ├── lib.rs └── client.rs ├── LICENSE.txt ├── examples ├── client.rs └── server.rs ├── README.md ├── Cargo.toml └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OutThereLabs/actix-web-opentelemetry/HEAD/trace.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::InstrumentationScope; 2 | 3 | #[cfg(feature = "metrics")] 4 | #[cfg_attr(docsrs, doc(cfg(feature = "metrics")))] 5 | pub(crate) mod metrics; 6 | pub(crate) mod route_formatter; 7 | pub(crate) mod trace; 8 | 9 | pub(crate) fn get_scope() -> InstrumentationScope { 10 | InstrumentationScope::builder("actix-web-opentelemetry") 11 | .with_version(env!("CARGO_PKG_VERSION")) 12 | .with_schema_url(opentelemetry_semantic_conventions::SCHEMA_URL) 13 | .build() 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | args: 10 | - --all-features 11 | - --no-default-features 12 | - --no-default-features --features=metrics 13 | - --no-default-features --features=metrics-prometheus 14 | - --no-default-features --features=sync-middleware 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | components: clippy 23 | - uses: actions-rs/clippy-check@v1 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | args: ${{ matrix.args }} 27 | - name: Run tests 28 | run: cargo test ${{ matrix.args }} --verbose 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Out There Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/middleware/route_formatter.rs: -------------------------------------------------------------------------------- 1 | //! # Route Formatter 2 | //! 3 | //! Format routes from paths. 4 | 5 | /// Interface for formatting routes from paths. 6 | /// 7 | /// This crate will render the actix web [match pattern] by default. E.g. for 8 | /// path `/users/123/profile` the route for this span would be 9 | /// `/users/{id}/profile`. 10 | /// 11 | /// [match pattern]: actix_web::HttpRequest::match_pattern 12 | /// 13 | /// # Custom Formatter Examples 14 | /// 15 | /// ``` 16 | /// use actix_web_opentelemetry::RouteFormatter; 17 | /// 18 | /// // A formatter to ensure all paths are reported as lowercase. 19 | /// #[derive(Debug)] 20 | /// struct MyLowercaseFormatter; 21 | /// 22 | /// impl RouteFormatter for MyLowercaseFormatter { 23 | /// fn format(&self, path: &str) -> String { 24 | /// path.to_lowercase() 25 | /// } 26 | /// } 27 | /// 28 | /// // now a match with pattern `/USERS/{id}` would be recorded as `/users/{id}` 29 | /// ``` 30 | pub trait RouteFormatter: std::fmt::Debug { 31 | /// Function from path to route. 32 | /// e.g. /users/123 -> /users/:id 33 | fn format(&self, path: &str) -> String; 34 | } 35 | -------------------------------------------------------------------------------- /examples/client.rs: -------------------------------------------------------------------------------- 1 | use actix_web_opentelemetry::ClientExt; 2 | use opentelemetry::{global, KeyValue}; 3 | use opentelemetry_sdk::propagation::TraceContextPropagator; 4 | use opentelemetry_sdk::trace::SdkTracerProvider; 5 | use opentelemetry_sdk::Resource; 6 | use std::error::Error; 7 | use std::io; 8 | 9 | async fn execute_request(client: awc::Client) -> io::Result { 10 | let mut response = client 11 | .get("http://127.0.0.1:8080/users/103240ba-3d8d-4695-a176-e19cbc627483?a=1") 12 | .trace_request() 13 | .send() 14 | .await 15 | .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?; 16 | 17 | let bytes = response 18 | .body() 19 | .await 20 | .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?; 21 | 22 | std::str::from_utf8(&bytes) 23 | .map(|s| s.to_owned()) 24 | .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) 25 | } 26 | 27 | #[actix_web::main] 28 | async fn main() -> Result<(), Box> { 29 | // Start a new OTLP trace pipeline 30 | global::set_text_map_propagator(TraceContextPropagator::new()); 31 | 32 | let service_name_resource = Resource::builder_empty() 33 | .with_attribute(KeyValue::new("service.name", "actix_client")) 34 | .build(); 35 | 36 | let tracer = SdkTracerProvider::builder() 37 | .with_batch_exporter( 38 | opentelemetry_otlp::SpanExporter::builder() 39 | .with_tonic() 40 | .build()?, 41 | ) 42 | .with_resource(service_name_resource) 43 | .build(); 44 | 45 | let client = awc::Client::new(); 46 | let response = execute_request(client).await?; 47 | 48 | println!("Response: {}", response); 49 | 50 | tracer.shutdown()?; 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Actix Web OpenTelemetry 2 | 3 | [![Build Status](https://github.com/OutThereLabs/actix-web-opentelemetry/workflows/CI/badge.svg)](https://github.com/OutThereLabs/actix-web-opentelemetry/actions?query=workflow%3ACI) 4 | [![Crates.io: actix-web-opentelemetry](https://img.shields.io/crates/v/actix-web-opentelemetry.svg)](https://crates.io/crates/actix-web-opentelemetry) 5 | [![Documentation](https://docs.rs/actix-web-opentelemetry/badge.svg)](https://docs.rs/actix-web-opentelemetry) 6 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt) 7 | 8 | [OpenTelemetry](https://opentelemetry.io/) integration for [Actix Web](https://actix.rs/). 9 | 10 | ### Exporter configuration 11 | 12 | [`actix-web`] uses [`tokio`] as the underlying executor, so exporters should be 13 | configured to be non-blocking: 14 | 15 | ```toml 16 | [dependencies] 17 | # if exporting to jaeger, use the `tokio` feature. 18 | opentelemetry-jaeger = { version = "..", features = ["rt-tokio-current-thread"] } 19 | 20 | # if exporting to zipkin, use the `tokio` based `reqwest-client` feature. 21 | opentelemetry-zipkin = { version = "..", features = ["reqwest-client"], default-features = false } 22 | 23 | # ... ensure the same same for any other exporters 24 | ``` 25 | 26 | [`actix-web`]: https://crates.io/crates/actix-web 27 | [`tokio`]: https://crates.io/crates/tokio 28 | 29 | ### Execute client and server example 30 | 31 | ```console 32 | # Run jaeger in background 33 | $ docker run -d -p6831:6831/udp -p6832:6832/udp -p16686:16686 jaegertracing/all-in-one:latest 34 | 35 | # Run server example with tracing middleware 36 | $ cargo run --example server 37 | # (In other tab) Run client example with request tracing 38 | $ cargo run --example client --features awc 39 | 40 | # View spans (see the image below) 41 | $ firefox http://localhost:16686/ 42 | ``` 43 | 44 | ![Jaeger UI](trace.png) 45 | 46 | ### Features 47 | 48 | - `awc` -- enable support for tracing the `awc` http client. 49 | - `metrics` -- enable support for opentelemetry metrics (only traces are enabled by default) 50 | - `metrics-prometheus` -- enable support for prometheus metrics (requires `metrics` feature) 51 | - `sync-middleware` -- enable tracing on actix-web middlewares that do synchronous work before returning a future. Adds a small amount of overhead to every request. 52 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-opentelemetry" 3 | version = "0.21.0" 4 | authors = ["Julian Tescher "] 5 | description = "OpenTelemetry integration for Actix Web apps" 6 | homepage = "https://github.com/OutThereLabs/actix-web-opentelemetry" 7 | repository = "https://github.com/OutThereLabs/actix-web-opentelemetry" 8 | readme = "README.md" 9 | categories = ["api-bindings"] 10 | keywords = ["actix", "actix-web", "opentelemetry", "jaeger", "prometheus"] 11 | license = "MIT" 12 | edition = "2021" 13 | 14 | [features] 15 | metrics = ["opentelemetry/metrics"] 16 | metrics-prometheus = [ 17 | "metrics", 18 | "opentelemetry-prometheus", 19 | "prometheus", 20 | "dep:opentelemetry_sdk", 21 | "dep:tracing", 22 | ] 23 | sync-middleware = [] 24 | 25 | [dependencies] 26 | actix-http = { version = "3.0", default-features = false, features = [ 27 | "compress-zstd", 28 | ] } 29 | actix-web = { version = "4.0", default-features = false, features = [ 30 | "compress-zstd", 31 | ] } 32 | awc = { version = "3.0", optional = true, default-features = false, features = [ 33 | "compress-zstd", 34 | ] } 35 | futures-util = { version = "0.3", default-features = false, features = [ 36 | "alloc", 37 | ] } 38 | opentelemetry = { version = "0.28", default-features = false, features = [ 39 | "trace", 40 | ] } 41 | opentelemetry-prometheus = { version = "0.28", optional = true } 42 | opentelemetry-semantic-conventions = { version = "0.28", features = [ 43 | "semconv_experimental", 44 | ] } 45 | opentelemetry_sdk = { version = "0.28", optional = true, features = [ 46 | "metrics", 47 | "rt-tokio-current-thread", 48 | ] } 49 | prometheus = { version = "0.13", default-features = false, optional = true } 50 | serde = "1.0" 51 | tracing = { version = "0.1.41", optional = true } 52 | 53 | [dev-dependencies] 54 | actix-web = { version = "4.0", features = ["macros"] } 55 | actix-web-opentelemetry = { path = ".", features = [ 56 | "metrics-prometheus", 57 | "sync-middleware", 58 | "awc", 59 | ] } 60 | opentelemetry_sdk = { version = "0.28", features = [ 61 | "spec_unstable_metrics_views", 62 | "metrics", 63 | "rt-tokio-current-thread", 64 | ] } 65 | opentelemetry-otlp = { version = "0.28", features = ["grpc-tonic"] } 66 | opentelemetry-stdout = { version = "0.28", features = ["trace", "metrics"] } 67 | 68 | [package.metadata.docs.rs] 69 | all-features = true 70 | rustdoc-args = ["--cfg", "docsrs"] 71 | -------------------------------------------------------------------------------- /examples/server.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, App, HttpRequest, HttpServer}; 2 | use actix_web_opentelemetry::{PrometheusMetricsHandler, RequestMetrics, RequestTracing}; 3 | use opentelemetry::{global, KeyValue}; 4 | use opentelemetry_otlp::WithExportConfig; 5 | use opentelemetry_sdk::{ 6 | metrics::{Aggregation, Instrument, SdkMeterProvider, Stream}, 7 | propagation::TraceContextPropagator, 8 | trace::SdkTracerProvider, 9 | Resource, 10 | }; 11 | 12 | async fn index(_req: HttpRequest, _path: actix_web::web::Path) -> &'static str { 13 | "Hello world!" 14 | } 15 | 16 | #[actix_web::main] 17 | async fn main() -> Result<(), Box> { 18 | // Start a new OTLP trace pipeline 19 | global::set_text_map_propagator(TraceContextPropagator::new()); 20 | 21 | let service_name_resource = Resource::builder_empty() 22 | .with_attribute(KeyValue::new("service.name", "actix_server")) 23 | .build(); 24 | 25 | let tracer = SdkTracerProvider::builder() 26 | .with_batch_exporter( 27 | opentelemetry_otlp::SpanExporter::builder() 28 | .with_tonic() 29 | .with_endpoint("http://127.0.0.1:6565") 30 | .build()?, 31 | ) 32 | .with_resource(service_name_resource) 33 | .build(); 34 | 35 | global::set_tracer_provider(tracer.clone()); 36 | 37 | // Start a new prometheus metrics pipeline if --features metrics-prometheus is used 38 | #[cfg(feature = "metrics-prometheus")] 39 | let (metrics_handler, meter_provider) = { 40 | let registry = prometheus::Registry::new(); 41 | let exporter = opentelemetry_prometheus::exporter() 42 | .with_registry(registry.clone()) 43 | .build()?; 44 | 45 | let provider = SdkMeterProvider::builder() 46 | .with_reader(exporter) 47 | .with_resource( 48 | Resource::builder_empty() 49 | .with_attribute(KeyValue::new("service.name", "my_app")) 50 | .build(), 51 | ) 52 | .with_view( 53 | opentelemetry_sdk::metrics::new_view( 54 | Instrument::new().name("http.server.duration"), 55 | Stream::new().aggregation(Aggregation::ExplicitBucketHistogram { 56 | boundaries: vec![ 57 | 0.0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 58 | 5.0, 7.5, 10.0, 59 | ], 60 | record_min_max: true, 61 | }), 62 | ) 63 | .unwrap(), 64 | ) 65 | .build(); 66 | global::set_meter_provider(provider.clone()); 67 | 68 | (PrometheusMetricsHandler::new(registry), provider) 69 | }; 70 | 71 | HttpServer::new(move || { 72 | let app = App::new() 73 | .wrap(RequestTracing::new()) 74 | .wrap(RequestMetrics::default()) 75 | .service(web::resource("/users/{id}").to(index)); 76 | 77 | #[cfg(feature = "metrics-prometheus")] 78 | let app = app.route("/metrics", web::get().to(metrics_handler.clone())); 79 | 80 | app 81 | }) 82 | .bind("127.0.0.1:8080")? 83 | .run() 84 | .await?; 85 | 86 | // Ensure all spans have been reported 87 | tracer.shutdown()?; 88 | 89 | #[cfg(feature = "metrics-prometheus")] 90 | meter_provider.shutdown()?; 91 | 92 | Ok(()) 93 | } 94 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use actix_http::header::{self, CONTENT_LENGTH}; 2 | use actix_web::{ 3 | dev::ServiceRequest, 4 | http::{Method, Version}, 5 | }; 6 | use opentelemetry::{KeyValue, Value}; 7 | use opentelemetry_semantic_conventions::trace::{ 8 | CLIENT_ADDRESS, NETWORK_PEER_ADDRESS, MESSAGING_MESSAGE_BODY_SIZE, HTTP_REQUEST_METHOD, HTTP_ROUTE, 9 | NETWORK_PROTOCOL_VERSION, SERVER_ADDRESS, SERVER_PORT, URL_PATH, URL_QUERY, URL_SCHEME, 10 | USER_AGENT_ORIGINAL, 11 | }; 12 | 13 | #[cfg(feature = "awc")] 14 | #[inline] 15 | pub(super) fn http_url(uri: &actix_web::http::Uri) -> String { 16 | let scheme = uri.scheme().map(|s| s.as_str()).unwrap_or_default(); 17 | let host = uri.host().unwrap_or_default(); 18 | let path = uri.path(); 19 | let port = uri.port_u16().filter(|&p| p != 80 && p != 443); 20 | let (query, query_delimiter) = if let Some(query) = uri.query() { 21 | (query, "?") 22 | } else { 23 | ("", "") 24 | }; 25 | 26 | if let Some(port) = port { 27 | format!("{scheme}://{host}:{port}{path}{query_delimiter}{query}") 28 | } else { 29 | format!("{scheme}://{host}{path}{query_delimiter}{query}") 30 | } 31 | } 32 | 33 | #[inline] 34 | pub(super) fn http_method_str(method: &Method) -> Value { 35 | match method { 36 | &Method::OPTIONS => "OPTIONS".into(), 37 | &Method::GET => "GET".into(), 38 | &Method::POST => "POST".into(), 39 | &Method::PUT => "PUT".into(), 40 | &Method::DELETE => "DELETE".into(), 41 | &Method::HEAD => "HEAD".into(), 42 | &Method::TRACE => "TRACE".into(), 43 | &Method::CONNECT => "CONNECT".into(), 44 | &Method::PATCH => "PATCH".into(), 45 | other => other.to_string().into(), 46 | } 47 | } 48 | 49 | #[inline] 50 | pub(super) fn protocol_version(version: Version) -> Value { 51 | match version { 52 | Version::HTTP_09 => "0.9".into(), 53 | Version::HTTP_10 => "1.0".into(), 54 | Version::HTTP_11 => "1.1".into(), 55 | Version::HTTP_2 => "2".into(), 56 | Version::HTTP_3 => "3".into(), 57 | other => format!("{:?}", other).into(), 58 | } 59 | } 60 | 61 | #[inline] 62 | pub(super) fn url_scheme(scheme: &str) -> Value { 63 | match scheme { 64 | "http" => "http".into(), 65 | "https" => "https".into(), 66 | other => other.to_string().into(), 67 | } 68 | } 69 | 70 | pub(super) fn trace_attributes_from_request( 71 | req: &ServiceRequest, 72 | http_route: &str, 73 | ) -> Vec { 74 | let conn_info = req.connection_info(); 75 | let remote_addr = conn_info.realip_remote_addr(); 76 | 77 | let mut attributes = Vec::with_capacity(14); 78 | 79 | // Server attrs 80 | // 81 | attributes.push(KeyValue::new(HTTP_ROUTE, http_route.to_owned())); 82 | if let Some(remote) = remote_addr { 83 | attributes.push(KeyValue::new(CLIENT_ADDRESS, remote.to_string())); 84 | } 85 | if let Some(peer_addr) = req.peer_addr().map(|socket| socket.ip().to_string()) { 86 | if Some(peer_addr.as_str()) != remote_addr { 87 | // Client is going through a proxy 88 | attributes.push(KeyValue::new(NETWORK_PEER_ADDRESS, peer_addr)); 89 | } 90 | } 91 | let mut host_parts = conn_info.host().split_terminator(':'); 92 | if let Some(host) = host_parts.next() { 93 | attributes.push(KeyValue::new(SERVER_ADDRESS, host.to_string())); 94 | } 95 | if let Some(port) = host_parts.next().and_then(|port| port.parse::().ok()) { 96 | if port != 80 && port != 443 { 97 | attributes.push(KeyValue::new(SERVER_PORT, port)); 98 | } 99 | } 100 | if let Some(path_query) = req.uri().path_and_query() { 101 | if path_query.path() != "/" { 102 | attributes.push(KeyValue::new(URL_PATH, path_query.path().to_string())); 103 | } 104 | if let Some(query) = path_query.query() { 105 | attributes.push(KeyValue::new(URL_QUERY, query.to_string())); 106 | } 107 | } 108 | attributes.push(KeyValue::new(URL_SCHEME, url_scheme(conn_info.scheme()))); 109 | 110 | // Common attrs 111 | // 112 | attributes.push(KeyValue::new( 113 | HTTP_REQUEST_METHOD, 114 | http_method_str(req.method()), 115 | )); 116 | attributes.push(KeyValue::new( 117 | NETWORK_PROTOCOL_VERSION, 118 | protocol_version(req.version()), 119 | )); 120 | 121 | if let Some(content_length) = req 122 | .headers() 123 | .get(CONTENT_LENGTH) 124 | .and_then(|len| len.to_str().ok().and_then(|s| s.parse::().ok())) 125 | .filter(|&len| len > 0) 126 | { 127 | attributes.push(KeyValue::new(MESSAGING_MESSAGE_BODY_SIZE, content_length)); 128 | } 129 | 130 | if let Some(user_agent) = req 131 | .headers() 132 | .get(header::USER_AGENT) 133 | .and_then(|s| s.to_str().ok()) 134 | { 135 | attributes.push(KeyValue::new(USER_AGENT_ORIGINAL, user_agent.to_string())); 136 | } 137 | 138 | attributes 139 | } 140 | 141 | /// Create metric attributes for the given request 142 | #[cfg(feature = "metrics")] 143 | pub fn metrics_attributes_from_request( 144 | req: &ServiceRequest, 145 | http_route: std::borrow::Cow<'static, str>, 146 | ) -> Vec { 147 | let conn_info = req.connection_info(); 148 | 149 | let mut attributes = Vec::with_capacity(7); 150 | attributes.push(KeyValue::new(HTTP_ROUTE, http_route)); 151 | attributes.push(KeyValue::new( 152 | HTTP_REQUEST_METHOD, 153 | http_method_str(req.method()), 154 | )); 155 | attributes.push(KeyValue::new( 156 | NETWORK_PROTOCOL_VERSION, 157 | protocol_version(req.version()), 158 | )); 159 | 160 | let mut host_parts = conn_info.host().split_terminator(':'); 161 | if let Some(host) = host_parts.next() { 162 | attributes.push(KeyValue::new(SERVER_ADDRESS, host.to_string())); 163 | } 164 | if let Some(port) = host_parts.next().and_then(|port| port.parse::().ok()) { 165 | attributes.push(KeyValue::new(SERVER_PORT, port)) 166 | } 167 | attributes.push(KeyValue::new(URL_SCHEME, url_scheme(conn_info.scheme()))); 168 | 169 | attributes 170 | } 171 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [OpenTelemetry] integration for [Actix Web]. 2 | //! 3 | //! This crate allows you to easily instrument client and server requests. 4 | //! 5 | //! * Server requests can be traced by using the [`RequestTracing`] middleware. 6 | //! 7 | //! The `awc` feature allows you to instrument client requests made by the [awc] crate. 8 | //! 9 | //! * Client requests can be traced by using the [`ClientExt::trace_request`] method. 10 | //! 11 | //! The `metrics` feature allows you to expose request metrics to [Prometheus]. 12 | //! 13 | //! * Metrics can be tracked using the [`RequestMetrics`] middleware. 14 | //! 15 | //! [OpenTelemetry]: https://opentelemetry.io 16 | //! [Actix Web]: https://actix.rs 17 | //! [awc]: https://docs.rs/awc 18 | //! [Prometheus]: https://prometheus.io 19 | //! 20 | //! ### Client Request Examples: 21 | //! 22 | //! Note: this requires the `awc` feature to be enabled. 23 | //! 24 | //! ```no_run 25 | //! # #[cfg(feature="awc")] 26 | //! # { 27 | //! use awc::{Client, error::SendRequestError}; 28 | //! use actix_web_opentelemetry::ClientExt; 29 | //! 30 | //! async fn execute_request(client: &Client) -> Result<(), SendRequestError> { 31 | //! let res = client 32 | //! .get("http://localhost:8080") 33 | //! // Add `trace_request` before `send` to any awc request to add instrumentation 34 | //! .trace_request() 35 | //! .send() 36 | //! .await?; 37 | //! 38 | //! println!("Response: {:?}", res); 39 | //! Ok(()) 40 | //! } 41 | //! # } 42 | //! ``` 43 | //! 44 | //! ### Server middleware examples: 45 | //! 46 | //! Tracing and metrics middleware can be used together or independently. 47 | //! 48 | //! Tracing server example: 49 | //! 50 | //! ```no_run 51 | //! use actix_web::{web, App, HttpServer}; 52 | //! use actix_web_opentelemetry::RequestTracing; 53 | //! use opentelemetry::global; 54 | //! use opentelemetry_sdk::trace::SdkTracerProvider; 55 | //! 56 | //! async fn index() -> &'static str { 57 | //! "Hello world!" 58 | //! } 59 | //! 60 | //! #[actix_web::main] 61 | //! async fn main() -> std::io::Result<()> { 62 | //! // Install an OpenTelemetry trace pipeline. 63 | //! // Swap for https://docs.rs/opentelemetry-jaeger or other compatible 64 | //! // exporter to send trace information to your collector. 65 | //! let exporter = opentelemetry_stdout::SpanExporter::default(); 66 | //! 67 | //! // Configure your tracer provider with your exporter(s) 68 | //! let provider = SdkTracerProvider::builder() 69 | //! .with_simple_exporter(exporter) 70 | //! .build(); 71 | //! global::set_tracer_provider(provider); 72 | //! 73 | //! // add the request tracing middleware to create spans for each request 74 | //! HttpServer::new(|| { 75 | //! App::new() 76 | //! .wrap(RequestTracing::new()) 77 | //! .service(web::resource("/").to(index)) 78 | //! }) 79 | //! .bind("127.0.0.1:8080")? 80 | //! .run() 81 | //! .await 82 | //! } 83 | //! ``` 84 | //! 85 | //! Request metrics middleware (requires the `metrics` feature): 86 | //! 87 | //! ```no_run 88 | //! use actix_web::{dev, http, web, App, HttpRequest, HttpServer}; 89 | //! # #[cfg(feature = "metrics-prometheus")] 90 | //! use actix_web_opentelemetry::{PrometheusMetricsHandler, RequestMetrics, RequestTracing}; 91 | //! use opentelemetry::global; 92 | //! use opentelemetry_sdk::metrics::SdkMeterProvider; 93 | //! 94 | //! # #[cfg(feature = "metrics-prometheus")] 95 | //! #[actix_web::main] 96 | //! async fn main() -> Result<(), Box> { 97 | //! // Configure prometheus or your preferred metrics service 98 | //! let registry = prometheus::Registry::new(); 99 | //! let exporter = opentelemetry_prometheus::exporter() 100 | //! .with_registry(registry.clone()) 101 | //! .build()?; 102 | //! 103 | //! // set up your meter provider with your exporter(s) 104 | //! let provider = SdkMeterProvider::builder() 105 | //! .with_reader(exporter) 106 | //! .build(); 107 | //! global::set_meter_provider(provider); 108 | //! 109 | //! // Run actix server, metrics are now available at http://localhost:8080/metrics 110 | //! HttpServer::new(move || { 111 | //! App::new() 112 | //! .wrap(RequestTracing::new()) 113 | //! .wrap(RequestMetrics::default()) 114 | //! .route("/metrics", web::get().to(PrometheusMetricsHandler::new(registry.clone()))) 115 | //! }) 116 | //! .bind("localhost:8080")? 117 | //! .run() 118 | //! .await; 119 | //! 120 | //! Ok(()) 121 | //! } 122 | //! # #[cfg(not(feature = "metrics-prometheus"))] 123 | //! # fn main() {} 124 | //! ``` 125 | //! 126 | //! ### Exporter configuration 127 | //! 128 | //! [`actix-web`] uses [`tokio`] as the underlying executor, so exporters should be 129 | //! configured to be non-blocking: 130 | //! 131 | //! ```toml 132 | //! [dependencies] 133 | //! ## if exporting to jaeger, use the `tokio` feature. 134 | //! opentelemetry-jaeger = { version = "..", features = ["rt-tokio-current-thread"] } 135 | //! 136 | //! ## if exporting to zipkin, use the `tokio` based `reqwest-client` feature. 137 | //! opentelemetry-zipkin = { version = "..", features = ["reqwest-client"], default-features = false } 138 | //! 139 | //! ## ... ensure the same same for any other exporters 140 | //! ``` 141 | //! 142 | //! [`actix-web`]: https://crates.io/crates/actix-web 143 | //! [`tokio`]: https://crates.io/crates/tokio 144 | #![deny(missing_docs, unreachable_pub, missing_debug_implementations)] 145 | #![cfg_attr(docsrs, feature(doc_cfg), deny(rustdoc::broken_intra_doc_links))] 146 | 147 | #[cfg(feature = "awc")] 148 | mod client; 149 | mod middleware; 150 | mod util; 151 | 152 | #[cfg(feature = "awc")] 153 | #[cfg_attr(docsrs, doc(cfg(feature = "awc")))] 154 | pub use client::{ClientExt, InstrumentedClientRequest}; 155 | 156 | #[cfg(feature = "metrics-prometheus")] 157 | #[cfg_attr(docsrs, doc(cfg(feature = "metrics-prometheus")))] 158 | pub use middleware::metrics::prometheus::PrometheusMetricsHandler; 159 | #[cfg(feature = "metrics")] 160 | #[cfg_attr(docsrs, doc(cfg(feature = "metrics")))] 161 | pub use middleware::metrics::{RequestMetrics, RequestMetricsBuilder, RequestMetricsMiddleware}; 162 | #[cfg(feature = "metrics")] 163 | #[cfg_attr(docsrs, doc(cfg(feature = "metrics")))] 164 | pub use util::metrics_attributes_from_request; 165 | 166 | pub use { 167 | middleware::route_formatter::RouteFormatter, 168 | middleware::trace::{RequestTracing, RequestTracingMiddleware}, 169 | }; 170 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.21.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.21.0..v0.20.1) 4 | 5 | ### Changed 6 | 7 | * Update opentelemetry packages to 0.28 (#190) 8 | 9 | ## [v0.20.1](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.20.0..v0.20.1) 10 | 11 | ### Added 12 | 13 | * add `RequestMetricsBuilder::with_metric_attrs_from_req` (#179) 14 | 15 | ## [v0.20.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.19.0..v0.20.0) 16 | 17 | ### Changed 18 | 19 | * Update opentelemetry packages to 0.27 (#180) 20 | 21 | ## [v0.19.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.18.0..v0.19.0) 22 | 23 | ### Changed 24 | 25 | * Update opentelemetry packages to 0.24 (#172) 26 | 27 | ## [v0.18.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.17.0..v0.18.0) 28 | 29 | ### Changed 30 | 31 | * Update to otel v0.23 (#157) 32 | 33 | ## [v0.17.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.16.0..v0.17.0) 34 | 35 | ### Changed 36 | 37 | * Update to otel v0.22 (#147) 38 | 39 | ### Fixed 40 | 41 | * Fix typo for http_server_response_size metric (#139) 42 | * Fix http_server_response_size metric (#140) 43 | 44 | ## [v0.16.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.15.0..v0.16.0) 45 | 46 | ### Changed 47 | 48 | * Update to otel v0.21 (#135) 49 | * Remove active request units until bug is resolved (#136) 50 | 51 | ## [v0.15.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.14.0..v0.15.0) 52 | 53 | ### Changed 54 | 55 | * Update to otel v0.20 (#131) 56 | * Update to semantic conventions spec v1.21 (#131) 57 | 58 | See the [semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/README.md) 59 | documentation for details about instrument and span updates. 60 | 61 | ## [v0.14.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.13.0..v0.14.0) 62 | 63 | ### Changed 64 | 65 | * Update to otel v0.19 (#126) 66 | 67 | ## [v0.13.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.13.0-alpha.1..v0.13.0) 68 | 69 | ### Changed 70 | 71 | * Update to otel v0.18 (#115) 72 | 73 | ## [v0.13.0-alpha.1](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.12.0..v0.13.0-alpha.1) 74 | 75 | ### Added 76 | 77 | * Export RequestTracingMiddleware (#106) 78 | * Allow customisation of client span names (#111) 79 | 80 | ### Changed 81 | 82 | * Update semantic conventions for client and server traces (#113) 83 | * Reduce default span namer cardinality (#112) 84 | * Remove http.client_ip from metrics (#110) 85 | * Use proper metric semantic conventions (#109) 86 | * Use Otel semantic conventions for metrics (#105) 87 | * Simplify PrometheusMetricsHandler (#104) 88 | * Refactor to make Prometheus optional (#103) 89 | 90 | ## [v0.12.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.11.0-beta.8..v0.12.0) 91 | 92 | ### Changed 93 | 94 | * Update to 2021 edition (#99) 95 | * Update to actix-web v4 (#97) 96 | 97 | ## [v0.11.0-beta.8](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.11.0-beta.7..v0.11.0-beta.8) 98 | 99 | ### Changed 100 | 101 | - Update to opentelemetry v0.17.x (#94) 102 | - Fix metric names to be aligned with prometheus standards (#95) 103 | 104 | ## [v0.11.0-beta.7](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.11.0-beta.6..v0.11.0-beta.7) 105 | 106 | ### Added 107 | 108 | - Add `with_attributes` method to client extension (#91) 109 | 110 | ### Changed 111 | 112 | - Add http status code handling for client (#88) 113 | - Update to actix-http beta.17, actix-web beta.16, awc beta.15 (#89) 114 | - Make `awc` client tracing an optional feature (#92) 115 | 116 | ## [v0.11.0-beta.6](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.11.0-beta.5..v0.11.0-beta.6) 117 | 118 | ### Changed 119 | 120 | - Update actix-web and actix-http requirements to beta.13 (#84) 121 | 122 | ## [v0.11.0-beta.5](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.11.0-beta.4..v0.11.0-beta.5) 123 | 124 | ### Changed 125 | 126 | - Update to opentelemetry v0.16.x #77 127 | 128 | ## [v0.11.0-beta.4](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.11.0-beta.3..v0.11.0-beta.4) 129 | 130 | ### Changed 131 | 132 | - Update to opentelemetry v0.15.x and actix-web 4.0.0-beta.8 #76 133 | 134 | ## [v0.11.0-beta.3](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.11.0-beta.2..v0.11.0-beta.3) 135 | 136 | ### Changed 137 | 138 | - Update to opentelemetry v0.13.x #64 139 | 140 | ## [v0.11.0-beta.2](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.11.0-beta.1..v0.11.0-beta.2) 141 | 142 | ### Changed 143 | 144 | - Update to actix-web `4.0.0-beta.4` and awc `3.0.0-beta.3` (#57) 145 | 146 | ## [v0.11.0-beta.1](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.10.0...v0.11.0-beta.1) 147 | 148 | ### Changed 149 | 150 | - Update to tokio `1.0` and actix-web `4.0.0-beta.3` (#51) 151 | 152 | ## [v0.10.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.9.0...v0.10.0) 153 | 154 | ### Changed 155 | 156 | Note: optentelemetry `v0.12.x` uses tokio 1.0. See the 157 | [updated examples](https://github.com/OutThereLabs/actix-web-opentelemetry/blob/e29c77312d6a906571286f78cc26ca72cf3a0b6f/examples/server.rs#L17-L40) 158 | for compatible setup until actix-web supports tokio 1.0. 159 | 160 | - Update to OpenTelemetry v0.12.x #48 161 | 162 | ## [v0.9.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.8.0...v0.9.0) 163 | 164 | ### Changed 165 | 166 | - Update to OpenTelemetry v0.11.x #41 167 | 168 | ## [v0.8.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.7.0...v0.8.0) 169 | 170 | Be sure to set a trace propagator via [`global::set_text_map_propagator`](https://docs.rs/opentelemetry/0.10.0/opentelemetry/global/fn.set_text_map_propagator.html) 171 | as the default is now a no-op. 172 | 173 | ### Changed 174 | 175 | - Update to OpenTelemetry v0.10.x #38 176 | 177 | ## [v0.7.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.6.0...v0.7.0) 178 | 179 | ### Changed 180 | 181 | - Remove default features from actix-web #30 182 | - Update to OpenTelemetry v0.9.x #30 183 | - Move metrics behind a feature flag #30 184 | - Change default route name from unmatched to default #30 185 | 186 | ### Removed 187 | 188 | - Remove deprecated `with_tracing` function. #30 189 | 190 | ## [v0.6.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.5.0...v0.6.0) 191 | 192 | ### Changed 193 | 194 | - Upgrade `actix-web` to version 3 #24 195 | - `RequestMetrics` constructor longer accept a route_formatter. Can be added via `with_route_formatter` #24 196 | 197 | ### Removed 198 | 199 | - Remove obsolute `UuidWildcardFormatter` as actix 3 supports match patterns #24 200 | 201 | ### Fixed 202 | 203 | - Client will now properly inject context using the globally configured 204 | propagator. 205 | 206 | ## [v0.5.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.4.0...v0.5.0) 207 | 208 | ### Added 209 | 210 | - Trace actix client requests with `ClientExt::trace_request` or 211 | `ClientExt::trace_request_with_context`. #17 212 | 213 | ### Changed 214 | 215 | - Update to OpenTelemetry v0.8.0 #18 216 | - Deprecated `with_tracing` fn. Use `ClientExt` instead. #17 217 | 218 | ## [v0.4.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.3.0...v0.4.0) 219 | 220 | ### Changed 221 | 222 | - Update to OpenTelemetry v0.7.0 #11 223 | 224 | ## [v0.3.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.2.0...v0.3.0) 225 | 226 | ### Changed 227 | 228 | - Update to OpenTelemetry v0.6.0 #10 229 | 230 | ## [v0.2.0](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.1.2...v0.2.0) 231 | 232 | ### Changed 233 | 234 | - Update to OpenTelemetry v0.2.0 #6 235 | 236 | ## [v0.1.2](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.1.1...v0.1.2) 237 | 238 | ### Changed 239 | 240 | - Make client span name match otel spec #3 241 | 242 | ## [v0.1.1](https://github.com/OutThereLabs/actix-web-opentelemetry/compare/v0.1.0...v0.1.1) 243 | 244 | ### Added 245 | 246 | - Add option for route formatter #1 247 | - Add metrics middleware #2 248 | 249 | ## [v0.1.0](https://github.com/OutThereLabs/actix-web-opentelemetry/tree/v0.1.0) 250 | 251 | Initial debug alpha 252 | -------------------------------------------------------------------------------- /src/middleware/trace.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, rc::Rc, task::Poll}; 2 | 3 | use actix_web::{ 4 | dev::{Service, ServiceRequest, ServiceResponse, Transform}, 5 | http::header::HeaderMap, 6 | Error, 7 | }; 8 | use futures_util::future::{ok, FutureExt as _, LocalBoxFuture, Ready}; 9 | use opentelemetry::{ 10 | global::{self}, 11 | propagation::Extractor, 12 | trace::{FutureExt as OtelFutureExt, SpanKind, Status, TraceContextExt, Tracer}, 13 | KeyValue, 14 | }; 15 | use opentelemetry_semantic_conventions::trace::HTTP_RESPONSE_STATUS_CODE; 16 | 17 | use super::{get_scope, route_formatter::RouteFormatter}; 18 | use crate::util::trace_attributes_from_request; 19 | 20 | /// Request tracing middleware. 21 | /// 22 | /// # Examples: 23 | /// 24 | /// ```no_run 25 | /// use actix_web::{web, App, HttpServer}; 26 | /// use actix_web_opentelemetry::RequestTracing; 27 | /// use opentelemetry::global; 28 | /// use opentelemetry_sdk::trace::SdkTracerProvider; 29 | /// 30 | /// async fn index() -> &'static str { 31 | /// "Hello world!" 32 | /// } 33 | /// 34 | /// #[actix_web::main] 35 | /// async fn main() -> std::io::Result<()> { 36 | /// // Install an OpenTelemetry trace pipeline. 37 | /// // Swap for https://docs.rs/opentelemetry-jaeger or other compatible 38 | /// // exporter to send trace information to your collector. 39 | /// let exporter = opentelemetry_stdout::SpanExporter::default(); 40 | /// 41 | /// // Configure your tracer provider with your exporter(s) 42 | /// let provider = SdkTracerProvider::builder() 43 | /// .with_simple_exporter(exporter) 44 | /// .build(); 45 | /// global::set_tracer_provider(provider); 46 | /// 47 | /// HttpServer::new(|| { 48 | /// App::new() 49 | /// .wrap(RequestTracing::new()) 50 | /// .service(web::resource("/").to(index)) 51 | /// }) 52 | /// .bind("127.0.0.1:8080")? 53 | /// .run() 54 | /// .await 55 | /// } 56 | ///``` 57 | #[derive(Default, Debug)] 58 | pub struct RequestTracing { 59 | route_formatter: Option>, 60 | } 61 | 62 | impl RequestTracing { 63 | /// Actix web middleware to trace each request in an OpenTelemetry span. 64 | pub fn new() -> RequestTracing { 65 | RequestTracing::default() 66 | } 67 | 68 | /// Actix web middleware to trace each request in an OpenTelemetry span with 69 | /// formatted routes. 70 | /// 71 | /// # Examples 72 | /// 73 | /// ```no_run 74 | /// use actix_web::{web, App, HttpServer}; 75 | /// use actix_web_opentelemetry::{RouteFormatter, RequestTracing}; 76 | /// 77 | /// # #[actix_web::main] 78 | /// # async fn main() -> std::io::Result<()> { 79 | /// 80 | /// 81 | /// #[derive(Debug)] 82 | /// struct MyLowercaseFormatter; 83 | /// 84 | /// impl RouteFormatter for MyLowercaseFormatter { 85 | /// fn format(&self, path: &str) -> String { 86 | /// path.to_lowercase() 87 | /// } 88 | /// } 89 | /// 90 | /// // report /users/{id} as /users/:id 91 | /// HttpServer::new(move || { 92 | /// App::new() 93 | /// .wrap(RequestTracing::with_formatter(MyLowercaseFormatter)) 94 | /// .service(web::resource("/users/{id}").to(|| async { "ok" })) 95 | /// }) 96 | /// .bind("127.0.0.1:8080")? 97 | /// .run() 98 | /// .await 99 | /// # } 100 | /// ``` 101 | pub fn with_formatter(route_formatter: T) -> Self { 102 | RequestTracing { 103 | route_formatter: Some(Rc::new(route_formatter)), 104 | } 105 | } 106 | } 107 | 108 | impl Transform for RequestTracing 109 | where 110 | S: Service, Error = Error>, 111 | S::Future: 'static, 112 | B: 'static, 113 | { 114 | type Response = ServiceResponse; 115 | type Error = Error; 116 | type Transform = RequestTracingMiddleware; 117 | type InitError = (); 118 | type Future = Ready>; 119 | 120 | fn new_transform(&self, service: S) -> Self::Future { 121 | ok(RequestTracingMiddleware::new( 122 | global::tracer_with_scope(get_scope()), 123 | service, 124 | self.route_formatter.clone(), 125 | )) 126 | } 127 | } 128 | 129 | /// Request tracing middleware 130 | #[derive(Debug)] 131 | pub struct RequestTracingMiddleware { 132 | tracer: global::BoxedTracer, 133 | service: S, 134 | route_formatter: Option>, 135 | } 136 | 137 | impl RequestTracingMiddleware 138 | where 139 | S: Service, Error = Error>, 140 | S::Future: 'static, 141 | B: 'static, 142 | { 143 | fn new( 144 | tracer: global::BoxedTracer, 145 | service: S, 146 | route_formatter: Option>, 147 | ) -> Self { 148 | RequestTracingMiddleware { 149 | tracer, 150 | service, 151 | route_formatter, 152 | } 153 | } 154 | } 155 | 156 | impl Service for RequestTracingMiddleware 157 | where 158 | S: Service, Error = Error>, 159 | S::Future: 'static, 160 | B: 'static, 161 | { 162 | type Response = ServiceResponse; 163 | type Error = Error; 164 | type Future = LocalBoxFuture<'static, Result>; 165 | 166 | fn poll_ready(&self, cx: &mut std::task::Context<'_>) -> Poll> { 167 | self.service.poll_ready(cx) 168 | } 169 | 170 | fn call(&self, mut req: ServiceRequest) -> Self::Future { 171 | let parent_context = global::get_text_map_propagator(|propagator| { 172 | propagator.extract(&RequestHeaderCarrier::new(req.headers_mut())) 173 | }); 174 | let mut http_route: Cow<'static, str> = req 175 | .match_pattern() 176 | .map(Into::into) 177 | .unwrap_or_else(|| "default".into()); 178 | if let Some(formatter) = &self.route_formatter { 179 | http_route = formatter.format(&http_route).into(); 180 | } 181 | 182 | let mut builder = self.tracer.span_builder(http_route.clone()); 183 | builder.span_kind = Some(SpanKind::Server); 184 | builder.attributes = Some(trace_attributes_from_request(&req, &http_route)); 185 | 186 | let span = self.tracer.build_with_context(builder, &parent_context); 187 | let cx = parent_context.with_span(span); 188 | 189 | #[cfg(feature = "sync-middleware")] 190 | let attachment = cx.clone().attach(); 191 | 192 | let fut = self 193 | .service 194 | .call(req) 195 | .with_context(cx.clone()) 196 | .map(move |res| match res { 197 | Ok(ok_res) => { 198 | let span = cx.span(); 199 | span.set_attribute(KeyValue::new( 200 | HTTP_RESPONSE_STATUS_CODE, 201 | ok_res.status().as_u16() as i64, 202 | )); 203 | if ok_res.status().is_server_error() { 204 | span.set_status(Status::error( 205 | ok_res 206 | .status() 207 | .canonical_reason() 208 | .map(ToString::to_string) 209 | .unwrap_or_default(), 210 | )); 211 | }; 212 | span.end(); 213 | Ok(ok_res) 214 | } 215 | Err(err) => { 216 | let span = cx.span(); 217 | span.set_status(Status::error(format!("{:?}", err))); 218 | span.end(); 219 | Err(err) 220 | } 221 | }); 222 | 223 | #[cfg(feature = "sync-middleware")] 224 | drop(attachment); 225 | 226 | Box::pin(fut) 227 | } 228 | } 229 | 230 | struct RequestHeaderCarrier<'a> { 231 | headers: &'a HeaderMap, 232 | } 233 | 234 | impl<'a> RequestHeaderCarrier<'a> { 235 | fn new(headers: &'a HeaderMap) -> Self { 236 | RequestHeaderCarrier { headers } 237 | } 238 | } 239 | 240 | impl Extractor for RequestHeaderCarrier<'_> { 241 | fn get(&self, key: &str) -> Option<&str> { 242 | self.headers.get(key).and_then(|v| v.to_str().ok()) 243 | } 244 | 245 | fn keys(&self) -> Vec<&str> { 246 | self.headers.keys().map(|header| header.as_str()).collect() 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | middleware::get_scope, 3 | util::{http_method_str, http_url}, 4 | }; 5 | use actix_http::{encoding::Decoder, BoxedPayloadStream, Error, Payload}; 6 | use actix_web::{ 7 | body::MessageBody, 8 | http::{ 9 | self, 10 | header::{HeaderName, HeaderValue}, 11 | }, 12 | web::Bytes, 13 | }; 14 | use awc::{ 15 | error::SendRequestError, 16 | http::header::{CONTENT_LENGTH, USER_AGENT}, 17 | ClientRequest, ClientResponse, 18 | }; 19 | use futures_util::{future::TryFutureExt as _, Future, Stream}; 20 | use opentelemetry::{ 21 | global, 22 | propagation::Injector, 23 | trace::{SpanKind, Status, TraceContextExt, Tracer}, 24 | Context, KeyValue, 25 | }; 26 | use opentelemetry_semantic_conventions::trace::{ 27 | HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, MESSAGING_MESSAGE_BODY_SIZE, SERVER_ADDRESS, 28 | SERVER_PORT, URL_FULL, USER_AGENT_ORIGINAL, 29 | }; 30 | use serde::Serialize; 31 | use std::mem; 32 | use std::str::FromStr; 33 | use std::{ 34 | borrow::Cow, 35 | fmt::{self, Debug}, 36 | }; 37 | 38 | /// A wrapper for the actix-web [awc::ClientRequest]. 39 | pub struct InstrumentedClientRequest { 40 | cx: Context, 41 | attrs: Vec, 42 | span_namer: fn(&ClientRequest) -> String, 43 | request: ClientRequest, 44 | } 45 | 46 | impl Debug for InstrumentedClientRequest { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | let span_namer = fmt::Pointer::fmt(&(self.span_namer as usize as *const ()), f); 49 | f.debug_struct("InstrumentedClientRequest") 50 | .field("cx", &self.cx) 51 | .field("attrs", &self.attrs) 52 | .field("span_namer", &span_namer) 53 | .field("request", &self.request) 54 | .finish() 55 | } 56 | } 57 | 58 | fn default_span_namer(request: &ClientRequest) -> String { 59 | format!( 60 | "{} {}", 61 | request.get_method(), 62 | request.get_uri().host().unwrap_or_default() 63 | ) 64 | } 65 | 66 | /// OpenTelemetry extensions for actix-web's [awc::Client]. 67 | pub trait ClientExt { 68 | /// Trace an [awc::Client] request using the current context. 69 | /// 70 | /// Example: 71 | /// ```no_run 72 | /// use actix_web_opentelemetry::ClientExt; 73 | /// use awc::{Client, error::SendRequestError}; 74 | /// 75 | /// async fn execute_request(client: &Client) -> Result<(), SendRequestError> { 76 | /// let res = client.get("http://localhost:8080") 77 | /// // Add `trace_request` before `send` to any awc request to add instrumentation 78 | /// .trace_request() 79 | /// .send() 80 | /// .await?; 81 | /// 82 | /// println!("Response: {:?}", res); 83 | /// Ok(()) 84 | /// } 85 | /// ``` 86 | fn trace_request(self) -> InstrumentedClientRequest 87 | where 88 | Self: Sized, 89 | { 90 | self.trace_request_with_context(Context::current()) 91 | } 92 | 93 | /// Trace an [awc::Client] request using the given span context. 94 | /// 95 | /// Example: 96 | /// ```no_run 97 | /// use actix_web_opentelemetry::ClientExt; 98 | /// use awc::{Client, error::SendRequestError}; 99 | /// use opentelemetry::Context; 100 | /// 101 | /// async fn execute_request(client: &Client) -> Result<(), SendRequestError> { 102 | /// let res = client.get("http://localhost:8080") 103 | /// // Add `trace_request_with_context` before `send` to any awc request to 104 | /// // add instrumentation 105 | /// .trace_request_with_context(Context::current()) 106 | /// .send() 107 | /// .await?; 108 | /// 109 | /// println!("Response: {:?}", res); 110 | /// Ok(()) 111 | /// } 112 | /// ``` 113 | fn trace_request_with_context(self, cx: Context) -> InstrumentedClientRequest; 114 | } 115 | 116 | impl ClientExt for ClientRequest { 117 | fn trace_request_with_context(self, cx: Context) -> InstrumentedClientRequest { 118 | InstrumentedClientRequest { 119 | cx, 120 | attrs: Vec::with_capacity(8), 121 | span_namer: default_span_namer, 122 | request: self, 123 | } 124 | } 125 | } 126 | 127 | type AwcResult = Result>>, SendRequestError>; 128 | 129 | impl InstrumentedClientRequest { 130 | /// Generate an [`awc::ClientResponse`] from a traced request with an empty body. 131 | pub async fn send(self) -> AwcResult { 132 | self.trace_request(|request| request.send()).await 133 | } 134 | 135 | /// Generate an [awc::ClientResponse] from a traced request with the given body. 136 | pub async fn send_body(self, body: B) -> AwcResult 137 | where 138 | B: MessageBody + 'static, 139 | { 140 | self.trace_request(|request| request.send_body(body)).await 141 | } 142 | 143 | /// Generate an [awc::ClientResponse] from a traced request with the given form 144 | /// body. 145 | pub async fn send_form(self, value: &T) -> AwcResult { 146 | self.trace_request(|request| request.send_form(value)).await 147 | } 148 | 149 | /// Generate an [awc::ClientResponse] from a traced request with the given JSON 150 | /// body. 151 | pub async fn send_json(self, value: &T) -> AwcResult { 152 | self.trace_request(|request| request.send_json(value)).await 153 | } 154 | 155 | /// Generate an [awc::ClientResponse] from a traced request with the given stream 156 | /// body. 157 | pub async fn send_stream(self, stream: S) -> AwcResult 158 | where 159 | S: Stream> + Unpin + 'static, 160 | E: std::error::Error + Into + 'static, 161 | { 162 | self.trace_request(|request| request.send_stream(stream)) 163 | .await 164 | } 165 | 166 | async fn trace_request(mut self, f: F) -> AwcResult 167 | where 168 | F: FnOnce(ClientRequest) -> R, 169 | R: Future, 170 | { 171 | let tracer = global::tracer_with_scope(get_scope()); 172 | 173 | // Client attributes 174 | // https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md#http-client 175 | self.attrs.extend( 176 | &mut [ 177 | KeyValue::new( 178 | SERVER_ADDRESS, 179 | self.request 180 | .get_uri() 181 | .host() 182 | .map(|u| Cow::Owned(u.to_string())) 183 | .unwrap_or(Cow::Borrowed("unknown")), 184 | ), 185 | KeyValue::new( 186 | HTTP_REQUEST_METHOD, 187 | http_method_str(self.request.get_method()), 188 | ), 189 | KeyValue::new(URL_FULL, http_url(self.request.get_uri())), 190 | ] 191 | .into_iter(), 192 | ); 193 | 194 | if let Some(peer_port) = self.request.get_uri().port_u16() { 195 | if peer_port != 80 && peer_port != 443 { 196 | self.attrs 197 | .push(KeyValue::new(SERVER_PORT, peer_port as i64)); 198 | } 199 | } 200 | 201 | if let Some(user_agent) = self 202 | .request 203 | .headers() 204 | .get(USER_AGENT) 205 | .and_then(|len| len.to_str().ok()) 206 | { 207 | self.attrs 208 | .push(KeyValue::new(USER_AGENT_ORIGINAL, user_agent.to_string())) 209 | } 210 | 211 | if let Some(content_length) = self.request.headers().get(CONTENT_LENGTH).and_then(|len| { 212 | len.to_str() 213 | .ok() 214 | .and_then(|str_len| str_len.parse::().ok()) 215 | }) { 216 | self.attrs 217 | .push(KeyValue::new(MESSAGING_MESSAGE_BODY_SIZE, content_length)) 218 | } 219 | 220 | let span = tracer 221 | .span_builder((self.span_namer)(&self.request)) 222 | .with_kind(SpanKind::Client) 223 | .with_attributes(mem::take(&mut self.attrs)) 224 | .start_with_context(&tracer, &self.cx); 225 | let cx = self.cx.with_span(span); 226 | 227 | global::get_text_map_propagator(|injector| { 228 | injector.inject_context(&cx, &mut ActixClientCarrier::new(&mut self.request)); 229 | }); 230 | 231 | f(self.request) 232 | .inspect_ok(|res| record_response(res, &cx)) 233 | .inspect_err(|err| record_err(err, &cx)) 234 | .await 235 | } 236 | 237 | /// Add additional attributes to the instrumented span for a given request. 238 | /// 239 | /// The standard otel attributes will still be tracked. 240 | /// 241 | /// Example: 242 | /// ``` 243 | /// use actix_web_opentelemetry::ClientExt; 244 | /// use awc::{Client, error::SendRequestError}; 245 | /// use opentelemetry::KeyValue; 246 | /// 247 | /// async fn execute_request(client: &Client) -> Result<(), SendRequestError> { 248 | /// let attrs = [KeyValue::new("dye-key", "dye-value")]; 249 | /// let res = client.get("http://localhost:8080") 250 | /// // Add `trace_request` before `send` to any awc request to add instrumentation 251 | /// .trace_request() 252 | /// .with_attributes(attrs) 253 | /// .send() 254 | /// .await?; 255 | /// 256 | /// println!("Response: {:?}", res); 257 | /// Ok(()) 258 | /// } 259 | /// ``` 260 | pub fn with_attributes( 261 | mut self, 262 | attrs: impl IntoIterator, 263 | ) -> InstrumentedClientRequest { 264 | self.attrs.extend(&mut attrs.into_iter()); 265 | self 266 | } 267 | 268 | /// Customise the Span Name, for example to reduce cardinality 269 | /// 270 | /// Example: 271 | /// ``` 272 | /// use actix_web_opentelemetry::ClientExt; 273 | /// use awc::{Client, error::SendRequestError}; 274 | /// 275 | /// async fn execute_request(client: &Client) -> Result<(), SendRequestError> { 276 | /// let res = client.get("http://localhost:8080") 277 | /// // Add `trace_request` before `send` to any awc request to add instrumentation 278 | /// .trace_request() 279 | /// .with_span_namer(|r| format!("HTTP {}", r.get_method())) 280 | /// .send() 281 | /// .await?; 282 | /// 283 | /// println!("Response: {:?}", res); 284 | /// Ok(()) 285 | /// } 286 | /// ``` 287 | pub fn with_span_namer( 288 | mut self, 289 | span_namer: fn(&ClientRequest) -> String, 290 | ) -> InstrumentedClientRequest { 291 | self.span_namer = span_namer; 292 | self 293 | } 294 | } 295 | 296 | // convert http status code to span status following the rules described by the spec: 297 | // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status 298 | fn convert_status(status: http::StatusCode) -> Status { 299 | match status.as_u16() { 300 | 100..=399 => Status::Unset, 301 | // since we are the client, we MUST treat 4xx as error 302 | 400..=599 => Status::error("Unexpected status code"), 303 | code => Status::error(format!("Invalid HTTP status code {}", code)), 304 | } 305 | } 306 | 307 | fn record_response(response: &ClientResponse, cx: &Context) { 308 | let span = cx.span(); 309 | let status = convert_status(response.status()); 310 | span.set_status(status); 311 | span.set_attribute(KeyValue::new( 312 | HTTP_RESPONSE_STATUS_CODE, 313 | response.status().as_u16() as i64, 314 | )); 315 | span.end(); 316 | } 317 | 318 | fn record_err(err: T, cx: &Context) { 319 | let span = cx.span(); 320 | span.set_status(Status::error(format!("{:?}", err))); 321 | span.end(); 322 | } 323 | 324 | struct ActixClientCarrier<'a> { 325 | request: &'a mut ClientRequest, 326 | } 327 | 328 | impl<'a> ActixClientCarrier<'a> { 329 | fn new(request: &'a mut ClientRequest) -> Self { 330 | ActixClientCarrier { request } 331 | } 332 | } 333 | 334 | impl Injector for ActixClientCarrier<'_> { 335 | fn set(&mut self, key: &str, value: String) { 336 | let header_name = HeaderName::from_str(key).expect("Must be header name"); 337 | let header_value = HeaderValue::from_str(&value).expect("Must be a header value"); 338 | self.request.headers_mut().insert(header_name, header_value); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/middleware/metrics.rs: -------------------------------------------------------------------------------- 1 | //! # Metrics Middleware 2 | 3 | use actix_http::{ 4 | body::{BodySize, MessageBody}, 5 | header::CONTENT_LENGTH, 6 | }; 7 | use actix_web::dev; 8 | use futures_util::future::{self, FutureExt as _, LocalBoxFuture}; 9 | use opentelemetry::{ 10 | global, 11 | metrics::{Histogram, Meter, MeterProvider, UpDownCounter}, 12 | KeyValue, 13 | }; 14 | use std::borrow::Cow; 15 | use std::{sync::Arc, time::SystemTime}; 16 | 17 | use super::get_scope; 18 | use crate::util::metrics_attributes_from_request; 19 | use crate::RouteFormatter; 20 | 21 | // Follows the experimental semantic conventions for HTTP metrics: 22 | // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md 23 | use opentelemetry_semantic_conventions::trace::HTTP_RESPONSE_STATUS_CODE; 24 | 25 | const HTTP_SERVER_DURATION: &str = "http.server.duration"; 26 | const HTTP_SERVER_ACTIVE_REQUESTS: &str = "http.server.active_requests"; 27 | const HTTP_SERVER_REQUEST_SIZE: &str = "http.server.request.size"; 28 | const HTTP_SERVER_RESPONSE_SIZE: &str = "http.server.response.size"; 29 | 30 | /// Records http server metrics 31 | /// 32 | /// See the [spec] for details. 33 | /// 34 | /// [spec]: https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-metrics.md#http-server 35 | #[derive(Clone, Debug)] 36 | struct Metrics { 37 | http_server_duration: Histogram, 38 | http_server_active_requests: UpDownCounter, 39 | http_server_request_size: Histogram, 40 | http_server_response_size: Histogram, 41 | } 42 | 43 | impl Metrics { 44 | /// Create a new [`RequestMetrics`] 45 | fn new(meter: Meter) -> Self { 46 | let http_server_duration = meter 47 | .f64_histogram(HTTP_SERVER_DURATION) 48 | .with_description("Measures the duration of inbound HTTP requests.") 49 | .with_unit("s") 50 | .build(); 51 | 52 | let http_server_active_requests = meter 53 | .i64_up_down_counter(HTTP_SERVER_ACTIVE_REQUESTS) 54 | .with_description( 55 | "Measures the number of concurrent HTTP requests that are currently in-flight.", 56 | ) 57 | .build(); 58 | 59 | let http_server_request_size = meter 60 | .u64_histogram(HTTP_SERVER_REQUEST_SIZE) 61 | .with_description("Measures the size of HTTP request messages (compressed).") 62 | .with_unit("By") 63 | .build(); 64 | 65 | let http_server_response_size = meter 66 | .u64_histogram(HTTP_SERVER_RESPONSE_SIZE) 67 | .with_description("Measures the size of HTTP response messages (compressed).") 68 | .with_unit("By") 69 | .build(); 70 | 71 | Metrics { 72 | http_server_active_requests, 73 | http_server_duration, 74 | http_server_request_size, 75 | http_server_response_size, 76 | } 77 | } 78 | } 79 | 80 | /// Builder for [RequestMetrics] 81 | #[derive(Clone, Debug, Default)] 82 | pub struct RequestMetricsBuilder { 83 | route_formatter: Option>, 84 | meter: Option, 85 | metric_attrs_from_req: Option) -> Vec>, 86 | } 87 | 88 | impl RequestMetricsBuilder { 89 | /// Create a new `RequestMetricsBuilder` 90 | pub fn new() -> Self { 91 | Self::default() 92 | } 93 | 94 | /// Add a route formatter to customize metrics match patterns 95 | pub fn with_route_formatter(mut self, route_formatter: R) -> Self 96 | where 97 | R: RouteFormatter + Send + Sync + 'static, 98 | { 99 | self.route_formatter = Some(Arc::new(route_formatter)); 100 | self 101 | } 102 | 103 | /// Set the meter provider this middleware should use to construct meters 104 | pub fn with_meter_provider(mut self, meter_provider: impl MeterProvider) -> Self { 105 | self.meter = Some(meter_provider.meter_with_scope(get_scope())); 106 | self 107 | } 108 | 109 | /// Set a metric attrs function that the middleware will use to create metric attributes 110 | pub fn with_metric_attrs_from_req( 111 | mut self, 112 | metric_attrs_from_req: fn(&dev::ServiceRequest, Cow<'static, str>) -> Vec, 113 | ) -> Self { 114 | self.metric_attrs_from_req = Some(metric_attrs_from_req); 115 | self 116 | } 117 | 118 | /// Build the `RequestMetrics` middleware 119 | pub fn build(self) -> RequestMetrics { 120 | let meter = self 121 | .meter 122 | .unwrap_or_else(|| global::meter_provider().meter_with_scope(get_scope())); 123 | 124 | RequestMetrics { 125 | route_formatter: self.route_formatter, 126 | metrics: Arc::new(Metrics::new(meter)), 127 | metric_attrs_from_req: self 128 | .metric_attrs_from_req 129 | .unwrap_or(metrics_attributes_from_request), 130 | } 131 | } 132 | } 133 | 134 | /// Request metrics tracking 135 | /// 136 | /// # Examples 137 | /// 138 | /// ```no_run 139 | /// use actix_web::{dev, http, web, App, HttpRequest, HttpServer}; 140 | /// use actix_web_opentelemetry::{PrometheusMetricsHandler, RequestMetrics, RequestTracing}; 141 | /// use opentelemetry::global; 142 | /// use opentelemetry_sdk::metrics::SdkMeterProvider; 143 | /// 144 | /// #[actix_web::main] 145 | /// async fn main() -> Result<(), Box> { 146 | /// // Configure prometheus or your preferred metrics service 147 | /// let registry = prometheus::Registry::new(); 148 | /// let exporter = opentelemetry_prometheus::exporter() 149 | /// .with_registry(registry.clone()) 150 | /// .build()?; 151 | /// 152 | /// // set up your meter provider with your exporter(s) 153 | /// let provider = SdkMeterProvider::builder() 154 | /// .with_reader(exporter) 155 | /// .build(); 156 | /// global::set_meter_provider(provider); 157 | /// 158 | /// // Run actix server, metrics are now available at http://localhost:8080/metrics 159 | /// HttpServer::new(move || { 160 | /// App::new() 161 | /// .wrap(RequestTracing::new()) 162 | /// .wrap(RequestMetrics::default()) 163 | /// .route("/metrics", web::get().to(PrometheusMetricsHandler::new(registry.clone()))) 164 | /// }) 165 | /// .bind("localhost:8080")? 166 | /// .run() 167 | /// .await?; 168 | /// 169 | /// Ok(()) 170 | /// } 171 | /// ``` 172 | #[derive(Clone, Debug)] 173 | pub struct RequestMetrics { 174 | route_formatter: Option>, 175 | metrics: Arc, 176 | metric_attrs_from_req: fn(&dev::ServiceRequest, Cow<'static, str>) -> Vec, 177 | } 178 | 179 | impl RequestMetrics { 180 | /// Create a builder to configure this middleware 181 | pub fn builder() -> RequestMetricsBuilder { 182 | RequestMetricsBuilder::new() 183 | } 184 | } 185 | 186 | impl Default for RequestMetrics { 187 | fn default() -> Self { 188 | RequestMetrics::builder().build() 189 | } 190 | } 191 | 192 | impl dev::Transform for RequestMetrics 193 | where 194 | S: dev::Service< 195 | dev::ServiceRequest, 196 | Response = dev::ServiceResponse, 197 | Error = actix_web::Error, 198 | >, 199 | S::Future: 'static, 200 | B: MessageBody + 'static, 201 | { 202 | type Response = dev::ServiceResponse; 203 | type Error = actix_web::Error; 204 | type Transform = RequestMetricsMiddleware; 205 | type InitError = (); 206 | type Future = future::Ready>; 207 | 208 | fn new_transform(&self, service: S) -> Self::Future { 209 | let service = RequestMetricsMiddleware { 210 | service, 211 | metrics: self.metrics.clone(), 212 | route_formatter: self.route_formatter.clone(), 213 | metric_attrs_from_req: self.metric_attrs_from_req.clone(), 214 | }; 215 | 216 | future::ok(service) 217 | } 218 | } 219 | 220 | /// Request metrics middleware 221 | #[allow(missing_debug_implementations)] 222 | pub struct RequestMetricsMiddleware { 223 | service: S, 224 | metrics: Arc, 225 | route_formatter: Option>, 226 | metric_attrs_from_req: fn(&dev::ServiceRequest, Cow<'static, str>) -> Vec, 227 | } 228 | 229 | impl dev::Service for RequestMetricsMiddleware 230 | where 231 | S: dev::Service< 232 | dev::ServiceRequest, 233 | Response = dev::ServiceResponse, 234 | Error = actix_web::Error, 235 | >, 236 | S::Future: 'static, 237 | B: MessageBody + 'static, 238 | { 239 | type Response = dev::ServiceResponse; 240 | type Error = actix_web::Error; 241 | type Future = LocalBoxFuture<'static, Result>; 242 | 243 | dev::forward_ready!(service); 244 | 245 | fn call(&self, req: dev::ServiceRequest) -> Self::Future { 246 | let timer = SystemTime::now(); 247 | 248 | let mut http_target = req 249 | .match_pattern() 250 | .map(Cow::Owned) 251 | .unwrap_or(Cow::Borrowed("default")); 252 | 253 | if let Some(formatter) = &self.route_formatter { 254 | http_target = Cow::Owned(formatter.format(&http_target)); 255 | } 256 | 257 | let mut attributes = (self.metric_attrs_from_req)(&req, http_target); 258 | self.metrics.http_server_active_requests.add(1, &attributes); 259 | 260 | let content_length = req 261 | .headers() 262 | .get(CONTENT_LENGTH) 263 | .and_then(|len| len.to_str().ok().and_then(|s| s.parse().ok())) 264 | .unwrap_or(0); 265 | self.metrics 266 | .http_server_request_size 267 | .record(content_length, &attributes); 268 | 269 | let request_metrics = self.metrics.clone(); 270 | Box::pin(self.service.call(req).map(move |res| { 271 | request_metrics 272 | .http_server_active_requests 273 | .add(-1, &attributes); 274 | 275 | // Ignore actix errors for metrics 276 | if let Ok(res) = res { 277 | attributes.push(KeyValue::new( 278 | HTTP_RESPONSE_STATUS_CODE, 279 | res.status().as_u16() as i64, 280 | )); 281 | let response_size = match res.response().body().size() { 282 | BodySize::Sized(size) => size, 283 | _ => 0, 284 | }; 285 | request_metrics 286 | .http_server_response_size 287 | .record(response_size, &attributes); 288 | 289 | request_metrics.http_server_duration.record( 290 | timer.elapsed().map(|t| t.as_secs_f64()).unwrap_or_default(), 291 | &attributes, 292 | ); 293 | 294 | Ok(res) 295 | } else { 296 | res 297 | } 298 | })) 299 | } 300 | } 301 | 302 | #[cfg(feature = "metrics-prometheus")] 303 | #[cfg_attr(docsrs, doc(cfg(feature = "metrics-prometheus")))] 304 | pub(crate) mod prometheus { 305 | use actix_web::{dev, http::StatusCode}; 306 | use futures_util::future::{self, LocalBoxFuture}; 307 | use opentelemetry_sdk::metrics::MetricError; 308 | use prometheus::{Encoder, Registry, TextEncoder}; 309 | 310 | /// Prometheus request metrics service 311 | #[derive(Clone, Debug)] 312 | pub struct PrometheusMetricsHandler { 313 | prometheus_registry: Registry, 314 | } 315 | 316 | impl PrometheusMetricsHandler { 317 | /// Build a route to serve Prometheus metrics 318 | pub fn new(registry: Registry) -> Self { 319 | Self { 320 | prometheus_registry: registry, 321 | } 322 | } 323 | } 324 | 325 | impl PrometheusMetricsHandler { 326 | fn metrics(&self) -> String { 327 | let encoder = TextEncoder::new(); 328 | let metric_families = self.prometheus_registry.gather(); 329 | let mut buf = Vec::new(); 330 | if let Err(err) = encoder.encode(&metric_families[..], &mut buf) { 331 | tracing::error!( 332 | name: "encode_failure", 333 | target: env!("CARGO_PKG_NAME"), 334 | name = "encode_failure", 335 | error = MetricError::Other(err.to_string()).to_string(), 336 | "" 337 | ); 338 | } 339 | 340 | String::from_utf8(buf).unwrap_or_default() 341 | } 342 | } 343 | 344 | impl dev::Handler for PrometheusMetricsHandler { 345 | type Output = Result, actix_web::error::Error>; 346 | type Future = LocalBoxFuture<'static, Self::Output>; 347 | 348 | fn call(&self, _req: actix_web::HttpRequest) -> Self::Future { 349 | Box::pin(future::ok(actix_web::HttpResponse::with_body( 350 | StatusCode::OK, 351 | self.metrics(), 352 | ))) 353 | } 354 | } 355 | } 356 | --------------------------------------------------------------------------------