1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-27 17:22:57 +01:00

Merge remote-tracking branch 'tracing/actix-extras' into tracing-actix-web

This commit is contained in:
Luca Palmieri 2024-09-29 10:22:21 +02:00
commit b3a26979a1
23 changed files with 2049 additions and 2 deletions

View File

@ -5,3 +5,4 @@ ci-check-min-examples = "hack check --workspace --no-default-features --examples
ci-check = "check --workspace --tests --examples --bins"
ci-test = "test --workspace --lib --tests --all-features --examples --bins --no-fail-fast"
ci-doctest = "test --workspace --doc --all-features --no-fail-fast"
ci-otel-test = "hack test --package tracing-actix-web --each-feature --exclude-all-features"

View File

@ -69,7 +69,10 @@ jobs:
- name: tests
timeout-minutes: 40
run: cargo ci-test
run: cargo ci-test --exclude tracing-actix-web
- name: otel
run: cargo ci-otel-test
- name: CI cache clean
run: cargo-ci-cache-clean
@ -127,7 +130,10 @@ jobs:
- name: tests
timeout-minutes: 40
run: cargo ci-test --exclude=actix-session --exclude=actix-limitation
run: cargo ci-test --exclude=tracing-actix-web --exclude=actix-session --exclude=actix-limitation
- name: otel
run: cargo ci-otel-test
- name: CI cache clean
run: cargo-ci-cache-clean

View File

@ -9,6 +9,10 @@ members = [
"actix-settings",
"actix-web-httpauth",
"actix-ws",
"tracing-actix-web",
"tracing-actix-web/examples/opentelemetry",
"tracing-actix-web/examples/custom-root-span",
"tracing-actix-web/examples/request-id-response-header",
]
[workspace.package]

View File

@ -0,0 +1,111 @@
[package]
name = "tracing-actix-web"
version = "0.7.13"
authors = ["Luca Palmieri <rust@lpalmieri.com>"]
repository.workspace = true
homepage.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
documentation = "https://docs.rs/tracing-actix-web/"
readme = "README.md"
description = "Structured logging middleware for actix-web."
keywords = ["http", "actix-web", "tracing", "logging"]
categories = ["asynchronous", "web-programming"]
[features]
default = ["emit_event_on_error"]
opentelemetry_0_13 = [
"opentelemetry_0_13_pkg",
"tracing-opentelemetry_0_12_pkg",
]
opentelemetry_0_14 = [
"opentelemetry_0_14_pkg",
"tracing-opentelemetry_0_13_pkg",
]
opentelemetry_0_15 = [
"opentelemetry_0_15_pkg",
"tracing-opentelemetry_0_14_pkg",
]
opentelemetry_0_16 = [
"opentelemetry_0_16_pkg",
"tracing-opentelemetry_0_16_pkg",
]
opentelemetry_0_17 = [
"opentelemetry_0_17_pkg",
"tracing-opentelemetry_0_17_pkg",
]
opentelemetry_0_18 = [
"opentelemetry_0_18_pkg",
"tracing-opentelemetry_0_18_pkg",
]
opentelemetry_0_19 = [
"opentelemetry_0_19_pkg",
"tracing-opentelemetry_0_19_pkg",
]
opentelemetry_0_20 = [
"opentelemetry_0_20_pkg",
"tracing-opentelemetry_0_21_pkg",
]
opentelemetry_0_21 = [
"opentelemetry_0_21_pkg",
"tracing-opentelemetry_0_22_pkg",
]
opentelemetry_0_22 = [
"opentelemetry_0_22_pkg",
"tracing-opentelemetry_0_23_pkg",
]
opentelemetry_0_23 = [
"opentelemetry_0_23_pkg",
"tracing-opentelemetry_0_24_pkg",
]
opentelemetry_0_24 = [
"opentelemetry_0_24_pkg",
"tracing-opentelemetry_0_25_pkg",
]
opentelemetry_0_25 = [
"opentelemetry_0_25_pkg",
"tracing-opentelemetry_0_26_pkg",
]
emit_event_on_error = []
uuid_v7 = ["uuid/v7"]
[dependencies]
actix-web = { version = "4", default-features = false }
pin-project = "1.0.0"
tracing = "0.1.36"
uuid = { version = "1.6", features = ["v4"] }
mutually_exclusive_features = "0.1"
opentelemetry_0_13_pkg = { package = "opentelemetry", version = "0.13", optional = true }
opentelemetry_0_14_pkg = { package = "opentelemetry", version = "0.14", optional = true }
opentelemetry_0_15_pkg = { package = "opentelemetry", version = "0.15", optional = true }
opentelemetry_0_16_pkg = { package = "opentelemetry", version = "0.16", optional = true }
opentelemetry_0_17_pkg = { package = "opentelemetry", version = "0.17", optional = true }
opentelemetry_0_18_pkg = { package = "opentelemetry", version = "0.18", optional = true }
opentelemetry_0_19_pkg = { package = "opentelemetry", version = "0.19", optional = true }
opentelemetry_0_20_pkg = { package = "opentelemetry", version = "0.20", optional = true }
opentelemetry_0_21_pkg = { package = "opentelemetry", version = "0.21", optional = true }
opentelemetry_0_22_pkg = { package = "opentelemetry", version = "0.22", optional = true }
opentelemetry_0_23_pkg = { package = "opentelemetry", version = "0.23", optional = true }
opentelemetry_0_24_pkg = { package = "opentelemetry", version = "0.24", optional = true }
opentelemetry_0_25_pkg = { package = "opentelemetry", version = "0.25", optional = true }
tracing-opentelemetry_0_12_pkg = { package = "tracing-opentelemetry", version = "0.12", optional = true }
tracing-opentelemetry_0_13_pkg = { package = "tracing-opentelemetry", version = "0.13", optional = true }
tracing-opentelemetry_0_14_pkg = { package = "tracing-opentelemetry", version = "0.14", optional = true }
tracing-opentelemetry_0_16_pkg = { package = "tracing-opentelemetry", version = "0.16", optional = true }
tracing-opentelemetry_0_17_pkg = { package = "tracing-opentelemetry", version = "0.17", optional = true }
tracing-opentelemetry_0_18_pkg = { package = "tracing-opentelemetry", version = "0.18", optional = true }
tracing-opentelemetry_0_19_pkg = { package = "tracing-opentelemetry", version = "0.19", optional = true }
tracing-opentelemetry_0_21_pkg = { package = "tracing-opentelemetry", version = "0.21", optional = true }
tracing-opentelemetry_0_22_pkg = { package = "tracing-opentelemetry", version = "0.22", optional = true }
tracing-opentelemetry_0_23_pkg = { package = "tracing-opentelemetry", version = "0.23", optional = true }
tracing-opentelemetry_0_24_pkg = { package = "tracing-opentelemetry", version = "0.24", optional = true }
tracing-opentelemetry_0_25_pkg = { package = "tracing-opentelemetry", version = "0.25", optional = true }
tracing-opentelemetry_0_26_pkg = { package = "tracing-opentelemetry", version = "0.26", optional = true }
[dev-dependencies]
actix-web = { version = "4", default-features = false, features = ["macros"] }
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
tracing-bunyan-formatter = "0.3.0"
tracing-log = "0.2"

View File

@ -0,0 +1 @@
../LICENSE-APACHE

View File

@ -0,0 +1 @@
../LICENSE-MIT

331
tracing-actix-web/README.md Normal file
View File

@ -0,0 +1,331 @@
<h1 align="center">tracing-actix-web</h1>
<div align="center">
<strong>
Structured diagnostics for actix-web applications.
</strong>
</div>
<br />
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/tracing-actix-web">
<img src="https://img.shields.io/crates/v/tracing-actix-web.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/tracing-actix-web">
<img src="https://img.shields.io/crates/d/tracing-actix-web.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs.rs docs -->
<a href="https://docs.rs/tracing-actix-web">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
</div>
<br/>
`tracing-actix-web` provides [`TracingLogger`], a middleware to collect telemetry data from applications built on top of the [`actix-web`] framework.
> `tracing-actix-web` was initially developed for the telemetry chapter of [Zero to Production In Rust](https://zero2prod.com), a hands-on introduction to backend development using the Rust programming language.
# Getting started
## How to install
Add `tracing-actix-web` to your dependencies:
```toml
[dependencies]
# ...
tracing-actix-web = "0.7"
tracing = "0.1"
actix-web = "4"
```
`tracing-actix-web` exposes these feature flags:
- `opentelemetry_0_13`: attach [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-rust)'s context to the root span using `opentelemetry` 0.13;
- `opentelemetry_0_14`: same as above but using `opentelemetry` 0.14;
- `opentelemetry_0_15`: same as above but using `opentelemetry` 0.15;
- `opentelemetry_0_16`: same as above but using `opentelemetry` 0.16;
- `opentelemetry_0_17`: same as above but using `opentelemetry` 0.17;
- `opentelemetry_0_18`: same as above but using `opentelemetry` 0.18;
- `opentelemetry_0_19`: same as above but using `opentelemetry` 0.19;
- `opentelemetry_0_20`: same as above but using `opentelemetry` 0.20;
- `opentelemetry_0_21`: same as above but using `opentelemetry` 0.21;
- `opentelemetry_0_22`: same as above but using `opentelemetry` 0.22;
- `opentelemetry_0_23`: same as above but using `opentelemetry` 0.23;
- `opentelemetry_0_24`: same as above but using `opentelemetry` 0.24;
- `opentelemetry_0_25`: same as above but using `opentelemetry` 0.25;
- `emit_event_on_error`: emit a [`tracing`] event when request processing fails with an error (enabled by default).
- `uuid_v7`: use the UUID v7 implementation inside [`RequestId`] instead of UUID v4 (disabled by default).
## Quickstart
```rust,compile_fail
use actix_web::{App, web, HttpServer};
use tracing_actix_web::TracingLogger;
fn main() {
// Init your `tracing` subscriber here!
let server = HttpServer::new(|| {
App::new()
// Mount `TracingLogger` as a middleware
.wrap(TracingLogger::default())
.service( /* */ )
});
}
```
Check out [the examples on GitHub](https://github.com/LukeMathWalker/tracing-actix-web/tree/main/examples) to get a taste of how [`TracingLogger`] can be used to observe and monitor your
application.
# From zero to hero: a crash course in observability
## `tracing`: who art thou?
[`TracingLogger`] is built on top of [`tracing`], a modern instrumentation framework with
[a vibrant ecosystem](https://github.com/tokio-rs/tracing#related-crates).
`tracing-actix-web`'s documentation provides a crash course in how to use [`tracing`] to instrument an `actix-web` application.
If you want to learn more check out ["Are we observable yet?"](https://www.lpalmieri.com/posts/2020-09-27-zero-to-production-4-are-we-observable-yet/) -
it provides an in-depth introduction to the crate and the problems it solves within the bigger picture of [observability](https://docs.honeycomb.io/learning-about-observability/).
## The root span
[`tracing::Span`] is the key abstraction in [`tracing`]: it represents a unit of work in your system.
A [`tracing::Span`] has a beginning and an end. It can include one or more **child spans** to represent sub-unit
of works within a larger task.
When your application receives a request, [`TracingLogger`] creates a new span - we call it the **[root span]**.
All the spans created _while_ processing the request will be children of the root span.
[`tracing`] empowers us to attach structured properties to a span as a collection of key-value pairs.
Those properties can then be queried in a variety of tools (e.g. ElasticSearch, Honeycomb, DataDog) to
understand what is happening in your system.
## Customisation via [`RootSpanBuilder`]
Troubleshooting becomes much easier when the root span has a _rich context_ - e.g. you can understand most of what
happened when processing the request just by looking at the properties attached to the corresponding root span.
You might have heard of this technique as the [canonical log line pattern](https://stripe.com/blog/canonical-log-lines),
popularised by Stripe. It is more recently discussed in terms of [high-cardinality events](https://www.honeycomb.io/blog/observability-a-manifesto/)
by Honeycomb and other vendors in the observability space.
[`TracingLogger`] gives you a chance to use the very same pattern: you can customise the properties attached
to the root span in order to capture the context relevant to your specific domain.
[`TracingLogger::default`] is equivalent to:
```rust
use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder};
// Two ways to initialise TracingLogger with the default root span builder
let default = TracingLogger::default();
let another_way = TracingLogger::<DefaultRootSpanBuilder>::new();
```
We are delegating the construction of the root span to [`DefaultRootSpanBuilder`].
[`DefaultRootSpanBuilder`] captures, out of the box, several dimensions that are usually relevant when looking at an HTTP
API: method, version, route, etc. - check out its documentation for an extensive list.
You can customise the root span by providing your own implementation of the [`RootSpanBuilder`] trait.
Let's imagine, for example, that our system cares about a client identifier embedded inside an authorization header.
We could add a `client_id` property to the root span using a custom builder, `DomainRootSpanBuilder`:
```rust
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceResponse, ServiceRequest};
use actix_web::Error;
use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder, RootSpanBuilder};
use tracing::Span;
pub struct DomainRootSpanBuilder;
impl RootSpanBuilder for DomainRootSpanBuilder {
fn on_request_start(request: &ServiceRequest) -> Span {
let client_id: &str = todo!("Somehow extract it from the authorization header");
tracing::info_span!("Request", client_id)
}
fn on_request_end<B: MessageBody>(_span: Span, _outcome: &Result<ServiceResponse<B>, Error>) {}
}
let custom_middleware = TracingLogger::<DomainRootSpanBuilder>::new();
```
There is an issue, though: `client_id` is the _only_ property we are capturing.
With `DomainRootSpanBuilder`, as it is, we do not get any of that useful HTTP-related information provided by
[`DefaultRootSpanBuilder`].
We can do better!
```rust
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceResponse, ServiceRequest};
use actix_web::Error;
use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder, RootSpanBuilder};
use tracing::Span;
pub struct DomainRootSpanBuilder;
impl RootSpanBuilder for DomainRootSpanBuilder {
fn on_request_start(request: &ServiceRequest) -> Span {
let client_id: &str = todo!("Somehow extract it from the authorization header");
tracing_actix_web::root_span!(request, client_id)
}
fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
DefaultRootSpanBuilder::on_request_end(span, outcome);
}
}
let custom_middleware = TracingLogger::<DomainRootSpanBuilder>::new();
```
[`root_span!`] is a macro provided by `tracing-actix-web`: it creates a new span by combining all the HTTP properties tracked
by [`DefaultRootSpanBuilder`] with the custom ones you specify when calling it (e.g. `client_id` in our example).
We need to use a macro because `tracing` requires all the properties attached to a span to be declared upfront, when the span is created.
You cannot add new ones afterwards. This makes it extremely fast, but it pushes us to reach for macros when we need some level of
composition.
[`root_span!`] exposes more or less the same knob you can find on `tracing`'s `span!` macro. You can, for example, customise
the span level:
```rust
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceResponse, ServiceRequest};
use actix_web::Error;
use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder, RootSpanBuilder, Level};
use tracing::Span;
pub struct CustomLevelRootSpanBuilder;
impl RootSpanBuilder for CustomLevelRootSpanBuilder {
fn on_request_start(request: &ServiceRequest) -> Span {
let level = if request.path() == "/health_check" {
Level::DEBUG
} else {
Level::INFO
};
tracing_actix_web::root_span!(level = level, request)
}
fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
DefaultRootSpanBuilder::on_request_end(span, outcome);
}
}
let custom_middleware = TracingLogger::<CustomLevelRootSpanBuilder>::new();
```
## The [`RootSpan`] extractor
It often happens that not all information about a task is known upfront, encoded in the incoming request.
You can use the [`RootSpan`] extractor to grab the root span in your handlers and attach more information
to your root span as it becomes available:
```rust
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceResponse, ServiceRequest};
use actix_web::{Error, HttpResponse};
use tracing_actix_web::{RootSpan, DefaultRootSpanBuilder, RootSpanBuilder};
use tracing::Span;
use actix_web::get;
use tracing_actix_web::RequestId;
use uuid::Uuid;
#[get("/")]
async fn handler(root_span: RootSpan) -> HttpResponse {
let application_id: &str = todo!("Some domain logic");
// Record the property value against the root span
root_span.record("application_id", &application_id);
// [...]
# todo!()
}
pub struct DomainRootSpanBuilder;
impl RootSpanBuilder for DomainRootSpanBuilder {
fn on_request_start(request: &ServiceRequest) -> Span {
let client_id: &str = todo!("Somehow extract it from the authorization header");
// All fields you want to capture must be declared upfront.
// If you don't know the value (yet), use tracing's `Empty`
tracing_actix_web::root_span!(
request,
client_id, application_id = tracing::field::Empty
)
}
fn on_request_end<B: MessageBody>(span: Span, response: &Result<ServiceResponse<B>, Error>) {
DefaultRootSpanBuilder::on_request_end(span, response);
}
}
```
# Unique identifiers
## Request Id
`tracing-actix-web` generates a unique identifier for each incoming request, the **request id**.
You can extract the request id using the [`RequestId`] extractor:
```rust
use actix_web::get;
use tracing_actix_web::RequestId;
use uuid::Uuid;
#[get("/")]
async fn index(request_id: RequestId) -> String {
format!("{}", request_id)
}
```
The request id is meant to identify all operations related to a particular request **within the boundary of your API**.
If you need to **trace** a request across multiple services (e.g. in a microservice architecture), you want to look at the `trace_id` field - see the next section on OpenTelemetry for more details.
Optionally, using the `uuid_v7` feature flag will allow [`RequestId`] to use UUID v7 instead of the currently used UUID v4.
## Trace Id
To fulfill a request you often have to perform additional I/O operations - e.g. calls to other REST or gRPC APIs, database queries, etc.
**Distributed tracing** is the standard approach to **trace** a single request across the entirety of your stack.
`tracing-actix-web` provides support for distributed tracing by supporting the [OpenTelemetry standard](https://opentelemetry.io/).
`tracing-actix-web` follows [OpenTelemetry's semantic convention](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext)
for field names.
Furthermore, it provides an `opentelemetry_0_17` feature flag to automatically performs trace propagation: it tries to extract the OpenTelemetry context out of the headers of incoming requests and, when it finds one, it sets it as the remote context for the current root span. The context is then propagated to your downstream dependencies if your HTTP or gRPC clients are OpenTelemetry-aware - e.g. using [`reqwest-middleware` and `reqwest-tracing`](https://github.com/TrueLayer/reqwest-middleware) if you are using `reqwest` as your HTTP client.
You can then find all logs for the same request across all the services it touched by looking for the `trace_id`, automatically logged by `tracing-actix-web`.
If you add [`tracing-opentelemetry::OpenTelemetryLayer`](https://docs.rs/tracing-opentelemetry/0.17.0/tracing_opentelemetry/struct.OpenTelemetryLayer.html)
in your `tracing::Subscriber` you will be able to export the root span (and all its children) as OpenTelemetry spans.
Check out the [relevant example in the GitHub repository](https://github.com/LukeMathWalker/tracing-actix-web/tree/main/examples/opentelemetry) for reference.
# License
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version 2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in `tracing-actix-web` by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.
[`TracingLogger`]: https://docs.rs/tracing-actix-web/4.0.0-beta.1/tracing_actix_web/struct.TracingLogger.html
[`RequestId`]: https://docs.rs/tracing-actix-web/4.0.0-beta.1/tracing_actix_web/struct.RequestId.html
[`RootSpan`]: https://docs.rs/tracing-actix-web/4.0.0-beta.1/tracing_actix_web/struct.RootSpan.html
[`RootSpanBuilder`]: https://docs.rs/tracing-actix-web/4.0.0-beta.1/tracing_actix_web/trait.RootSpanBuilder.html
[`DefaultRootSpanBuilder`]: https://docs.rs/tracing-actix-web/4.0.0-beta.1/tracing_actix_web/struct.DefaultRootSpanBuilder.html
[`DefaultRootSpanBuilder::default`]: https://docs.rs/tracing-actix-web/4.0.0-beta.1/tracing_actix_web/struct.DefaultRootSpanBuilder.html#method.default
[`tracing`]: https://docs.rs/tracing
[`tracing::Span`]: https://docs.rs/tracing/latest/tracing/struct.Span.html
[`root_span!`]: https://docs.rs/tracing-actix-web/4.0.0-beta.1/tracing_actix_web/macro.root_span.html
[root span]: https://docs.rs/tracing-actix-web/4.0.0-beta.1/tracing_actix_web/struct.RootSpan.html
[`actix-web`]: https://docs.rs/actix-web/4.0.0-beta.6/actix_web/index.html
[`uuid`]: https://docs.rs/uuid

View File

@ -0,0 +1,17 @@
[package]
name = "custom-root-span"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
actix-web = "4"
opentelemetry = "0.25"
opentelemetry-otlp = "0.25"
opentelemetry_sdk = { version = "0.25", features = ["rt-tokio-current-thread"] }
opentelemetry-semantic-conventions = "0.25"
tracing-opentelemetry = { version = "0.26" }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
tracing-bunyan-formatter = "0.3"
tracing-actix-web = { path = "../..", features = ["opentelemetry_0_25"] }

View File

@ -0,0 +1,38 @@
# Custom root span
## Running
You can launch this example with
```bash
cargo run
```
An `actix-web` application will be listening on port `8080`.
You can fire requests to it with:
```bash
curl -v http://localhost:8080/hello
```
```text
Hello world!
```
or
```bash
curl -v http://localhost:8080/hello/my-name
```
```text
Hello my-name!
```
## Visualising traces
Spans will be also printed to the console in JSON format, as structured log records.
You can look at the exported spans in your browser by visiting [http://localhost:16686](http://localhost:16686) if you launch a Jaeger instance:
```bash
docker run -d -p6831:6831/udp -p6832:6832/udp -p16686:16686 jaegertracing/all-in-one:latest
```

View File

@ -0,0 +1,116 @@
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{web, App, Error, HttpServer};
use opentelemetry::trace::TracerProvider;
use opentelemetry::{global, KeyValue};
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::{
propagation::TraceContextPropagator, runtime::TokioCurrentThread, trace::Config, Resource,
};
use opentelemetry_semantic_conventions::resource;
use std::io;
use std::sync::LazyLock;
use tracing::Span;
use tracing_actix_web::{DefaultRootSpanBuilder, RootSpan, RootSpanBuilder, TracingLogger};
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
/// We will define a custom root span builder to capture additional fields, specific
/// to our application, on top of the ones provided by `DefaultRootSpanBuilder` out of the box.
pub struct CustomRootSpanBuilder;
impl RootSpanBuilder for CustomRootSpanBuilder {
fn on_request_start(request: &ServiceRequest) -> Span {
// Not sure why you'd be keen to capture this, but it's an example and we try to keep it simple
let n_headers = request.headers().len();
// We set `cloud_provider` to a constant value.
//
// `name` is not known at this point - we delegate the responsibility to populate it
// to the `personal_hello` handler. We MUST declare the field though, otherwise
// `span.record("caller_name", XXX)` will just be silently ignored by `tracing`.
tracing_actix_web::root_span!(
request,
n_headers,
cloud_provider = "localhost",
caller_name = tracing::field::Empty
)
}
fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
// Capture the standard fields when the request finishes.
DefaultRootSpanBuilder::on_request_end(span, outcome);
}
}
async fn hello() -> &'static str {
"Hello world!"
}
async fn personal_hello(root_span: RootSpan, name: web::Path<String>) -> String {
// Add more context to the root span of the request.
root_span.record("caller_name", &name.as_str());
format!("Hello {}!", name)
}
#[actix_web::main]
async fn main() -> io::Result<()> {
init_telemetry();
HttpServer::new(move || {
App::new()
.wrap(TracingLogger::<CustomRootSpanBuilder>::new())
.service(web::resource("/hello").to(hello))
.service(web::resource("/hello/{name}").to(personal_hello))
})
.bind("127.0.0.1:8080")?
.run()
.await?;
// Ensure all spans have been shipped to Jaeger.
opentelemetry::global::shutdown_tracer_provider();
Ok(())
}
const APP_NAME: &str = "tracing-actix-web-demo";
static RESOURCE: LazyLock<Resource> =
LazyLock::new(|| Resource::new(vec![KeyValue::new(resource::SERVICE_NAME, APP_NAME)]));
/// Init a `tracing` subscriber that prints spans to stdout as well as
/// ships them to Jaeger.
///
/// Check the `opentelemetry` example for more details.
fn init_telemetry() {
// Start a new otlp trace pipeline.
// Spans are exported in batch - recommended setup for a production application.
global::set_text_map_propagator(TraceContextPropagator::new());
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint("http://localhost:4317"),
)
.with_trace_config(Config::default().with_resource(RESOURCE.clone()))
.install_batch(TokioCurrentThread)
.expect("Failed to install OpenTelemetry tracer.")
.tracer_builder(APP_NAME)
.build();
// Filter based on level - trace, debug, info, warn, error
// Tunable via `RUST_LOG` env variable
let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("info"));
// Create a `tracing` layer using the otlp tracer
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
// Create a `tracing` layer to emit spans as structured logs to stdout
let formatting_layer = BunyanFormattingLayer::new(APP_NAME.into(), std::io::stdout);
// Combined them all together in a `tracing` subscriber
let subscriber = Registry::default()
.with(env_filter)
.with(telemetry)
.with(JsonStorageLayer)
.with(formatting_layer);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to install `tracing` subscriber.")
}

View File

@ -0,0 +1,17 @@
[package]
name = "otel"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
actix-web = "4"
opentelemetry = "0.25"
opentelemetry_sdk = { version = "0.25", features = ["rt-tokio-current-thread"] }
opentelemetry-otlp = "0.25"
opentelemetry-semantic-conventions = "0.25"
tracing-opentelemetry = "0.26"
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
tracing-bunyan-formatter = "0.3"
tracing-actix-web = { path = "../..", features = ["opentelemetry_0_25"] }
tracing = "0.1.40"

View File

@ -0,0 +1,33 @@
# OpenTelemetry integration
## Prerequisites
To execute this example you need a running Jaeger instance.
You can launch one using Docker:
```bash
docker run -d -p6831:6831/udp -p6832:6832/udp -p16686:16686 jaegertracing/all-in-one:latest
```
## Running
You can launch this example with
```bash
cargo run
```
An `actix-web` application will be listening on port `8080`.
You can fire requests to it with:
```bash
curl -v http://localhost:8080/hello
```
```text
Hello world!
```
## Traces
You can look at the exported traces in your browser by visiting [http://localhost:16686](http://localhost:16686).
Spans will be also printed to the console in JSON format, as structured log records.

View File

@ -0,0 +1,75 @@
use actix_web::{web, App, HttpServer};
use opentelemetry::trace::TracerProvider;
use opentelemetry::{global, KeyValue};
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::{
propagation::TraceContextPropagator, runtime::TokioCurrentThread, trace::Config, Resource,
};
use opentelemetry_semantic_conventions::resource;
use std::io;
use std::sync::LazyLock;
use tracing_actix_web::TracingLogger;
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
const APP_NAME: &str = "tracing-actix-web-demo";
static RESOURCE: LazyLock<Resource> =
LazyLock::new(|| Resource::new(vec![KeyValue::new(resource::SERVICE_NAME, APP_NAME)]));
async fn hello() -> &'static str {
"Hello world!"
}
fn init_telemetry() {
// Start a new otlp trace pipeline.
// Spans are exported in batch - recommended setup for a production application.
global::set_text_map_propagator(TraceContextPropagator::new());
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint("http://localhost:4317"),
)
.with_trace_config(Config::default().with_resource(RESOURCE.clone()))
.install_batch(TokioCurrentThread)
.expect("Failed to install OpenTelemetry tracer.")
.tracer_builder(APP_NAME)
.build();
// Filter based on level - trace, debug, info, warn, error
// Tunable via `RUST_LOG` env variable
let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("info"));
// Create a `tracing` layer using the otlp tracer
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
// Create a `tracing` layer to emit spans as structured logs to stdout
let formatting_layer = BunyanFormattingLayer::new(APP_NAME.into(), std::io::stdout);
// Combined them all together in a `tracing` subscriber
let subscriber = Registry::default()
.with(env_filter)
.with(telemetry)
.with(JsonStorageLayer)
.with(formatting_layer);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to install `tracing` subscriber.")
}
#[actix_web::main]
async fn main() -> io::Result<()> {
init_telemetry();
HttpServer::new(move || {
App::new()
.wrap(TracingLogger::default())
.service(web::resource("/hello").to(hello))
})
.bind("127.0.0.1:8080")?
.run()
.await?;
// Ensure all spans have been shipped to Jaeger.
opentelemetry::global::shutdown_tracer_provider();
Ok(())
}

View File

@ -0,0 +1,9 @@
[package]
name = "request-id-response-header"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
actix-web = "4"
tracing-actix-web = { path = "../.." }

View File

@ -0,0 +1,25 @@
# Request ID in Response Header
This example shows how to set the `RequestId` as a response header.
## Running
You can launch this example with
```bash
cargo run
```
An `actix-web` application will be listening on port `8080`.
You can fire requests to it with:
```bash
curl -v http://localhost:8080/hello
```
```text
...
< HTTP/1.1 200 OK
< content-length: 12
< x-request-id: 1d5c5448-44d2-4051-ab59-985868875f94
...
```

View File

@ -0,0 +1,41 @@
use actix_web::{
dev::Service,
http::header::{HeaderName, HeaderValue},
web, App, HttpMessage, HttpServer,
};
use std::io;
use tracing_actix_web::{RequestId, TracingLogger};
async fn hello() -> &'static str {
"Hello world!"
}
#[actix_web::main]
async fn main() -> io::Result<()> {
HttpServer::new(move || {
App::new()
// set the request id in the `x-request-id` response header
.wrap_fn(|req, srv| {
let request_id = req.extensions().get::<RequestId>().copied();
let res = srv.call(req);
async move {
let mut res = res.await?;
if let Some(request_id) = request_id {
res.headers_mut().insert(
HeaderName::from_static("x-request-id"),
// this unwrap never fails, since UUIDs are valid ASCII strings
HeaderValue::from_str(&request_id.to_string()).unwrap(),
);
}
Ok(res)
}
})
.wrap(TracingLogger::default())
.service(web::resource("/hello").to(hello))
})
.bind("127.0.0.1:8080")?
.run()
.await?;
Ok(())
}

View File

@ -0,0 +1,329 @@
//! `tracing-actix-web` provides [`TracingLogger`], a middleware to collect telemetry data from applications
//! built on top of the [`actix-web`] framework.
//!
//! > `tracing-actix-web` was initially developed for the telemetry chapter of [Zero to Production In Rust](https://zero2prod.com), a hands-on introduction to backend development using the Rust programming language.
//!
//! # Getting started
//!
//! ## How to install
//!
//! Add `tracing-actix-web` to your dependencies:
//!
//! ```toml
//! [dependencies]
//! # ...
//! tracing-actix-web = "0.7"
//! tracing = "0.1"
//! actix-web = "4"
//! ```
//!
//! `tracing-actix-web` exposes three feature flags:
//!
//! - `opentelemetry_0_13`: attach [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-rust)'s context to the root span using `opentelemetry` 0.13;
//! - `opentelemetry_0_14`: same as above but using `opentelemetry` 0.14;
//! - `opentelemetry_0_15`: same as above but using `opentelemetry` 0.15;
//! - `opentelemetry_0_16`: same as above but using `opentelemetry` 0.16;
//! - `opentelemetry_0_17`: same as above but using `opentelemetry` 0.17;
//! - `opentelemetry_0_18`: same as above but using `opentelemetry` 0.18;
//! - `opentelemetry_0_19`: same as above but using `opentelemetry` 0.19;
//! - `opentelemetry_0_20`: same as above but using `opentelemetry` 0.20;
//! - `opentelemetry_0_21`: same as above but using `opentelemetry` 0.21;
//! - `opentelemetry_0_22`: same as above but using `opentelemetry` 0.22;
//! - `opentelemetry_0_23`: same as above but using `opentelemetry` 0.23;
//! - `opentelemetry_0_24`: same as above but using `opentelemetry` 0.24;
//! - `opentelemetry_0_25`: same as above but using `opentelemetry` 0.25;
//! - `emit_event_on_error`: emit a [`tracing`] event when request processing fails with an error (enabled by default).
//! - `uuid_v7`: use the UUID v7 implementation inside [`RequestId`] instead of UUID v4 (disabled by default).
//!
//! ## Quickstart
//!
//! ```rust,compile_fail
//! use actix_web::{App, web, HttpServer};
//! use tracing_actix_web::TracingLogger;
//!
//! let server = HttpServer::new(|| {
//! App::new()
//! // Mount `TracingLogger` as a middleware
//! .wrap(TracingLogger::default())
//! .service( /* */ )
//! });
//! ```
//!
//! Check out [the examples on GitHub](https://github.com/LukeMathWalker/tracing-actix-web/tree/main/examples) to get a taste of how [`TracingLogger`] can be used to observe and monitor your
//! application.
//!
//! # From zero to hero: a crash course in observability
//!
//! ## `tracing`: who art thou?
//!
//! [`TracingLogger`] is built on top of [`tracing`], a modern instrumentation framework with
//! [a vibrant ecosystem](https://github.com/tokio-rs/tracing#related-crates).
//!
//! `tracing-actix-web`'s documentation provides a crash course in how to use [`tracing`] to instrument an `actix-web` application.
//! If you want to learn more check out ["Are we observable yet?"](https://www.lpalmieri.com/posts/2020-09-27-zero-to-production-4-are-we-observable-yet/) -
//! it provides an in-depth introduction to the crate and the problems it solves within the bigger picture of [observability](https://docs.honeycomb.io/learning-about-observability/).
//!
//! ## The root span
//!
//! [`tracing::Span`] is the key abstraction in [`tracing`]: it represents a unit of work in your system.
//! A [`tracing::Span`] has a beginning and an end. It can include one or more **child spans** to represent sub-unit
//! of works within a larger task.
//!
//! When your application receives a request, [`TracingLogger`] creates a new span - we call it the **[root span]**.
//! All the spans created _while_ processing the request will be children of the root span.
//!
//! [`tracing`] empowers us to attach structured properties to a span as a collection of key-value pairs.
//! Those properties can then be queried in a variety of tools (e.g. ElasticSearch, Honeycomb, DataDog) to
//! understand what is happening in your system.
//!
//! ## Customisation via [`RootSpanBuilder`]
//!
//! Troubleshooting becomes much easier when the root span has a _rich context_ - e.g. you can understand most of what
//! happened when processing the request just by looking at the properties attached to the corresponding root span.
//!
//! You might have heard of this technique as the [canonical log line pattern](https://stripe.com/blog/canonical-log-lines),
//! popularised by Stripe. It is more recently discussed in terms of [high-cardinality events](https://www.honeycomb.io/blog/observability-a-manifesto/)
//! by Honeycomb and other vendors in the observability space.
//!
//! [`TracingLogger`] gives you a chance to use the very same pattern: you can customise the properties attached
//! to the root span in order to capture the context relevant to your specific domain.
//!
//! [`TracingLogger::default`] is equivalent to:
//!
//! ```rust
//! use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder};
//!
//! // Two ways to initialise TracingLogger with the default root span builder
//! let default = TracingLogger::default();
//! let another_way = TracingLogger::<DefaultRootSpanBuilder>::new();
//! ```
//!
//! We are delegating the construction of the root span to [`DefaultRootSpanBuilder`].
//! [`DefaultRootSpanBuilder`] captures, out of the box, several dimensions that are usually relevant when looking at an HTTP
//! API: method, version, route, etc. - check out its documentation for an extensive list.
//!
//! You can customise the root span by providing your own implementation of the [`RootSpanBuilder`] trait.
//! Let's imagine, for example, that our system cares about a client identifier embedded inside an authorization header.
//! We could add a `client_id` property to the root span using a custom builder, `DomainRootSpanBuilder`:
//!
//! ```rust
//! use actix_web::body::MessageBody;
//! use actix_web::dev::{ServiceResponse, ServiceRequest};
//! use actix_web::Error;
//! use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder, RootSpanBuilder};
//! use tracing::Span;
//!
//! pub struct DomainRootSpanBuilder;
//!
//! impl RootSpanBuilder for DomainRootSpanBuilder {
//! fn on_request_start(request: &ServiceRequest) -> Span {
//! let client_id: &str = todo!("Somehow extract it from the authorization header");
//! tracing::info_span!("Request", client_id)
//! }
//!
//! fn on_request_end<B: MessageBody>(_span: Span, _outcome: &Result<ServiceResponse<B>, Error>) {}
//! }
//!
//! let custom_middleware = TracingLogger::<DomainRootSpanBuilder>::new();
//! ```
//!
//! There is an issue, though: `client_id` is the _only_ property we are capturing.
//! With `DomainRootSpanBuilder`, as it is, we do not get any of that useful HTTP-related information provided by
//! [`DefaultRootSpanBuilder`].
//!
//! We can do better!
//!
//! ```rust
//! use actix_web::body::MessageBody;
//! use actix_web::dev::{ServiceResponse, ServiceRequest};
//! use actix_web::Error;
//! use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder, RootSpanBuilder};
//! use tracing::Span;
//!
//! pub struct DomainRootSpanBuilder;
//!
//! impl RootSpanBuilder for DomainRootSpanBuilder {
//! fn on_request_start(request: &ServiceRequest) -> Span {
//! let client_id: &str = todo!("Somehow extract it from the authorization header");
//! tracing_actix_web::root_span!(request, client_id)
//! }
//!
//! fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
//! DefaultRootSpanBuilder::on_request_end(span, outcome);
//! }
//! }
//!
//! let custom_middleware = TracingLogger::<DomainRootSpanBuilder>::new();
//! ```
//!
//! [`root_span!`] is a macro provided by `tracing-actix-web`: it creates a new span by combining all the HTTP properties tracked
//! by [`DefaultRootSpanBuilder`] with the custom ones you specify when calling it (e.g. `client_id` in our example).
//!
//! We need to use a macro because `tracing` requires all the properties attached to a span to be declared upfront, when the span is created.
//! You cannot add new ones afterwards. This makes it extremely fast, but it pushes us to reach for macros when we need some level of
//! composition.
//!
//! [`root_span!`] exposes more or less the same knob you can find on `tracing`'s `span!` macro. You can, for example, customise
//! the span level:
//!
//! ```rust
//! use actix_web::body::MessageBody;
//! use actix_web::dev::{ServiceResponse, ServiceRequest};
//! use actix_web::Error;
//! use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder, RootSpanBuilder, Level};
//! use tracing::Span;
//!
//! pub struct CustomLevelRootSpanBuilder;
//!
//! impl RootSpanBuilder for CustomLevelRootSpanBuilder {
//! fn on_request_start(request: &ServiceRequest) -> Span {
//! let level = if request.path() == "/health_check" {
//! Level::DEBUG
//! } else {
//! Level::INFO
//! };
//! tracing_actix_web::root_span!(level = level, request)
//! }
//!
//! fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
//! DefaultRootSpanBuilder::on_request_end(span, outcome);
//! }
//! }
//!
//! let custom_middleware = TracingLogger::<CustomLevelRootSpanBuilder>::new();
//! ```
//!
//! ## The [`RootSpan`] extractor
//!
//! It often happens that not all information about a task is known upfront, encoded in the incoming request.
//! You can use the [`RootSpan`] extractor to grab the root span in your handlers and attach more information
//! to your root span as it becomes available:
//!
//! ```rust
//! use actix_web::body::MessageBody;
//! use actix_web::dev::{ServiceResponse, ServiceRequest};
//! use actix_web::{Error, HttpResponse};
//! use tracing_actix_web::{RootSpan, DefaultRootSpanBuilder, RootSpanBuilder};
//! use tracing::Span;
//! use actix_web::get;
//! use tracing_actix_web::RequestId;
//! use uuid::Uuid;
//!
//! #[get("/")]
//! async fn handler(root_span: RootSpan) -> HttpResponse {
//! let application_id: &str = todo!("Some domain logic");
//! // Record the property value against the root span
//! root_span.record("application_id", &application_id);
//!
//! // [...]
//! # todo!()
//! }
//!
//! pub struct DomainRootSpanBuilder;
//!
//! impl RootSpanBuilder for DomainRootSpanBuilder {
//! fn on_request_start(request: &ServiceRequest) -> Span {
//! let client_id: &str = todo!("Somehow extract it from the authorization header");
//! // All fields you want to capture must be declared upfront.
//! // If you don't know the value (yet), use tracing's `Empty`
//! tracing_actix_web::root_span!(
//! request,
//! client_id, application_id = tracing::field::Empty
//! )
//! }
//!
//! fn on_request_end<B: MessageBody>(span: Span, response: &Result<ServiceResponse<B>, Error>) {
//! DefaultRootSpanBuilder::on_request_end(span, response);
//! }
//! }
//! ```
//!
//! # Unique identifiers
//!
//! ## Request Id
//!
//! `tracing-actix-web` generates a unique identifier for each incoming request, the **request id**.
//!
//! You can extract the request id using the [`RequestId`] extractor:
//!
//! ```rust
//! use actix_web::get;
//! use tracing_actix_web::RequestId;
//! use uuid::Uuid;
//!
//! #[get("/")]
//! async fn index(request_id: RequestId) -> String {
//! format!("{}", request_id)
//! }
//! ```
//!
//! The request id is meant to identify all operations related to a particular request **within the boundary of your API**.
//! If you need to **trace** a request across multiple services (e.g. in a microservice architecture), you want to look at the `trace_id` field - see the next section on OpenTelemetry for more details.
//!
//! Optionally, using the `uuid_v7` feature flag will allow [`RequestId`] to use UUID v7 instead of the currently used UUID v4.
//!
//! ## Trace Id
//!
//! To fulfill a request you often have to perform additional I/O operations - e.g. calls to other REST or gRPC APIs, database queries, etc.
//! **Distributed tracing** is the standard approach to **trace** a single request across the entirety of your stack.
//!
//! `tracing-actix-web` provides support for distributed tracing by supporting the [OpenTelemetry standard](https://opentelemetry.io/).
//! `tracing-actix-web` follows [OpenTelemetry's semantic convention](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext)
//! for field names.
//! Furthermore, it provides an `opentelemetry_0_17` feature flag to automatically performs trace propagation: it tries to extract the OpenTelemetry context out of the headers of incoming requests and, when it finds one, it sets it as the remote context for the current root span. The context is then propagated to your downstream dependencies if your HTTP or gRPC clients are OpenTelemetry-aware - e.g. using [`reqwest-middleware` and `reqwest-tracing`](https://github.com/TrueLayer/reqwest-middleware) if you are using `reqwest` as your HTTP client.
//! You can then find all logs for the same request across all the services it touched by looking for the `trace_id`, automatically logged by `tracing-actix-web`.
//!
//! If you add [`tracing-opentelemetry::OpenTelemetryLayer`](https://docs.rs/tracing-opentelemetry/0.17.0/tracing_opentelemetry/struct.OpenTelemetryLayer.html)
//! in your `tracing::Subscriber` you will be able to export the root span (and all its children) as OpenTelemetry spans.
//!
//! Check out the [relevant example in the GitHub repository](https://github.com/LukeMathWalker/tracing-actix-web/tree/main/examples/opentelemetry) for reference.
//!
//! [root span]: crate::RootSpan
//! [`actix-web`]: https://docs.rs/actix-web/4.0.0-beta.13/actix_web/index.html
mod middleware;
mod request_id;
mod root_span;
mod root_span_builder;
pub use middleware::{StreamSpan, TracingLogger};
pub use request_id::RequestId;
pub use root_span::RootSpan;
pub use root_span_builder::{DefaultRootSpanBuilder, RootSpanBuilder};
// Re-exporting the `Level` enum since it's used in our `root_span!` macro
pub use tracing::Level;
#[doc(hidden)]
pub mod root_span_macro;
mutually_exclusive_features::none_or_one_of!(
"opentelemetry_0_13",
"opentelemetry_0_14",
"opentelemetry_0_15",
"opentelemetry_0_16",
"opentelemetry_0_17",
"opentelemetry_0_18",
"opentelemetry_0_19",
"opentelemetry_0_20",
"opentelemetry_0_21",
"opentelemetry_0_22",
"opentelemetry_0_23",
"opentelemetry_0_24",
"opentelemetry_0_25",
);
#[cfg(any(
feature = "opentelemetry_0_13",
feature = "opentelemetry_0_14",
feature = "opentelemetry_0_15",
feature = "opentelemetry_0_16",
feature = "opentelemetry_0_17",
feature = "opentelemetry_0_18",
feature = "opentelemetry_0_19",
feature = "opentelemetry_0_20",
feature = "opentelemetry_0_21",
feature = "opentelemetry_0_22",
feature = "opentelemetry_0_23",
feature = "opentelemetry_0_24",
feature = "opentelemetry_0_25",
))]
mod otel;

View File

@ -0,0 +1,266 @@
use crate::{DefaultRootSpanBuilder, RequestId, RootSpan, RootSpanBuilder};
use actix_web::body::{BodySize, MessageBody};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::web::Bytes;
use actix_web::{Error, HttpMessage};
use std::future::{ready, Future, Ready};
use std::pin::Pin;
use std::task::{Context, Poll};
use tracing::Span;
/// `TracingLogger` is a middleware to capture structured diagnostic when processing an HTTP request.
/// Check the crate-level documentation for an in-depth introduction.
///
/// `TracingLogger` is designed as a drop-in replacement of [`actix-web`]'s [`Logger`].
///
/// # Usage
///
/// Register `TracingLogger` as a middleware for your application using `.wrap` on `App`.
/// In this example we add a [`tracing::Subscriber`] to output structured logs to the console.
///
/// ```rust
/// use actix_web::App;
/// use tracing::{Subscriber, subscriber::set_global_default};
/// use tracing_actix_web::TracingLogger;
/// use tracing_log::LogTracer;
/// use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
/// use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
///
/// /// Compose multiple layers into a `tracing`'s subscriber.
/// pub fn get_subscriber(
/// name: String,
/// env_filter: String
/// ) -> impl Subscriber + Send + Sync {
/// let env_filter = EnvFilter::try_from_default_env()
/// .unwrap_or(EnvFilter::new(env_filter));
/// let formatting_layer = BunyanFormattingLayer::new(
/// name.into(),
/// std::io::stdout
/// );
/// Registry::default()
/// .with(env_filter)
/// .with(JsonStorageLayer)
/// .with(formatting_layer)
/// }
///
/// /// Register a subscriber as global default to process span data.
/// ///
/// /// It should only be called once!
/// pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) {
/// LogTracer::init().expect("Failed to set logger");
/// set_global_default(subscriber).expect("Failed to set subscriber");
/// }
///
/// fn main() {
/// let subscriber = get_subscriber("app".into(), "info".into());
/// init_subscriber(subscriber);
///
/// let app = App::new().wrap(TracingLogger::default());
/// }
/// ```
///
/// Like [`actix-web`]'s [`Logger`], in order to use `TracingLogger` inside a Scope, Resource, or
/// Condition, the [`Compat`] middleware must be used.
///
/// ```rust
/// use actix_web::middleware::Compat;
/// use actix_web::{web, App};
/// use tracing_actix_web::TracingLogger;
///
/// let app = App::new()
/// .service(
/// web::scope("/some/route")
/// .wrap(Compat::new(TracingLogger::default())),
/// );
/// ```
///
/// [`actix-web`]: https://docs.rs/actix-web
/// [`Logger`]: https://docs.rs/actix-web/4.0.0-beta.13/actix_web/middleware/struct.Logger.html
/// [`Compat`]: https://docs.rs/actix-web/4.0.0-beta.13/actix_web/middleware/struct.Compat.html
/// [`tracing`]: https://docs.rs/tracing
pub struct TracingLogger<RootSpan: RootSpanBuilder> {
root_span_builder: std::marker::PhantomData<RootSpan>,
}
impl<RootSpan: RootSpanBuilder> Clone for TracingLogger<RootSpan> {
fn clone(&self) -> Self {
Self::new()
}
}
impl Default for TracingLogger<DefaultRootSpanBuilder> {
fn default() -> Self {
TracingLogger::new()
}
}
impl<RootSpan: RootSpanBuilder> TracingLogger<RootSpan> {
pub fn new() -> TracingLogger<RootSpan> {
TracingLogger {
root_span_builder: Default::default(),
}
}
}
impl<S, B, RootSpan> Transform<S, ServiceRequest> for TracingLogger<RootSpan>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
RootSpan: RootSpanBuilder,
{
type Response = ServiceResponse<StreamSpan<B>>;
type Error = Error;
type Transform = TracingLoggerMiddleware<S, RootSpan>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(TracingLoggerMiddleware {
service,
root_span_builder: std::marker::PhantomData,
}))
}
}
#[doc(hidden)]
pub struct TracingLoggerMiddleware<S, RootSpanBuilder> {
service: S,
root_span_builder: std::marker::PhantomData<RootSpanBuilder>,
}
#[allow(clippy::type_complexity)]
impl<S, B, RootSpanType> Service<ServiceRequest> for TracingLoggerMiddleware<S, RootSpanType>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
RootSpanType: RootSpanBuilder,
{
type Response = ServiceResponse<StreamSpan<B>>;
type Error = Error;
type Future = TracingResponse<S::Future, RootSpanType>;
actix_web::dev::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
req.extensions_mut().insert(RequestId::generate());
let root_span = RootSpanType::on_request_start(&req);
let root_span_wrapper = RootSpan::new(root_span.clone());
req.extensions_mut().insert(root_span_wrapper);
let fut = root_span.in_scope(|| self.service.call(req));
TracingResponse {
fut,
span: root_span,
_root_span_type: std::marker::PhantomData,
}
}
}
#[doc(hidden)]
#[pin_project::pin_project]
pub struct TracingResponse<F, RootSpanType> {
#[pin]
fut: F,
span: Span,
_root_span_type: std::marker::PhantomData<RootSpanType>,
}
#[doc(hidden)]
#[pin_project::pin_project]
pub struct StreamSpan<B> {
#[pin]
body: B,
span: Span,
}
impl<F, B, RootSpanType> Future for TracingResponse<F, RootSpanType>
where
F: Future<Output = Result<ServiceResponse<B>, Error>>,
B: MessageBody + 'static,
RootSpanType: RootSpanBuilder,
{
type Output = Result<ServiceResponse<StreamSpan<B>>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let fut = this.fut;
let span = this.span;
span.in_scope(|| match fut.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(outcome) => {
RootSpanType::on_request_end(Span::current(), &outcome);
#[cfg(feature = "emit_event_on_error")]
{
emit_event_on_error(&outcome);
}
Poll::Ready(outcome.map(|service_response| {
service_response.map_body(|_, body| StreamSpan {
body,
span: span.clone(),
})
}))
}
})
}
}
impl<B> MessageBody for StreamSpan<B>
where
B: MessageBody,
{
type Error = B::Error;
fn size(&self) -> BodySize {
self.body.size()
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
let this = self.project();
let body = this.body;
let span = this.span;
span.in_scope(|| body.poll_next(cx))
}
}
#[cfg(feature = "emit_event_on_error")]
fn emit_event_on_error<B: 'static>(
outcome: &Result<actix_web::dev::ServiceResponse<B>, actix_web::Error>,
) {
match outcome {
Ok(response) => {
if let Some(err) = response.response().error() {
// use the status code already constructed for the outgoing HTTP response
emit_error_event(err.as_response_error(), response.status())
}
}
Err(error) => {
let response_error = error.as_response_error();
emit_error_event(response_error, response_error.status_code())
}
}
}
#[cfg(feature = "emit_event_on_error")]
fn emit_error_event(
response_error: &dyn actix_web::ResponseError,
status_code: actix_web::http::StatusCode,
) {
let error_msg_prefix = "Error encountered while processing the incoming HTTP request";
if status_code.is_client_error() {
tracing::warn!("{}: {:?}", error_msg_prefix, response_error);
} else {
tracing::error!("{}: {:?}", error_msg_prefix, response_error);
}
}

View File

@ -0,0 +1,119 @@
use actix_web::dev::ServiceRequest;
#[cfg(feature = "opentelemetry_0_13")]
use opentelemetry_0_13_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_14")]
use opentelemetry_0_14_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_15")]
use opentelemetry_0_15_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_16")]
use opentelemetry_0_16_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_17")]
use opentelemetry_0_17_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_18")]
use opentelemetry_0_18_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_19")]
use opentelemetry_0_19_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_20")]
use opentelemetry_0_20_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_21")]
use opentelemetry_0_21_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_22")]
use opentelemetry_0_22_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_23")]
use opentelemetry_0_23_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_24")]
use opentelemetry_0_24_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_25")]
use opentelemetry_0_25_pkg as opentelemetry;
#[cfg(feature = "opentelemetry_0_13")]
use tracing_opentelemetry_0_12_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_14")]
use tracing_opentelemetry_0_13_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_15")]
use tracing_opentelemetry_0_14_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_16")]
use tracing_opentelemetry_0_16_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_17")]
use tracing_opentelemetry_0_17_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_18")]
use tracing_opentelemetry_0_18_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_19")]
use tracing_opentelemetry_0_19_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_20")]
use tracing_opentelemetry_0_21_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_21")]
use tracing_opentelemetry_0_22_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_22")]
use tracing_opentelemetry_0_23_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_23")]
use tracing_opentelemetry_0_24_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_24")]
use tracing_opentelemetry_0_25_pkg as tracing_opentelemetry;
#[cfg(feature = "opentelemetry_0_25")]
use tracing_opentelemetry_0_26_pkg as tracing_opentelemetry;
use opentelemetry::propagation::Extractor;
pub(crate) struct RequestHeaderCarrier<'a> {
headers: &'a actix_web::http::header::HeaderMap,
}
impl<'a> RequestHeaderCarrier<'a> {
pub(crate) fn new(headers: &'a actix_web::http::header::HeaderMap) -> Self {
RequestHeaderCarrier { headers }
}
}
impl<'a> Extractor for RequestHeaderCarrier<'a> {
fn get(&self, key: &str) -> Option<&str> {
self.headers.get(key).and_then(|v| v.to_str().ok())
}
fn keys(&self) -> Vec<&str> {
self.headers.keys().map(|header| header.as_str()).collect()
}
}
pub(crate) fn set_otel_parent(req: &ServiceRequest, span: &tracing::Span) {
use opentelemetry::trace::TraceContextExt as _;
use tracing_opentelemetry::OpenTelemetrySpanExt as _;
let parent_context = opentelemetry::global::get_text_map_propagator(|propagator| {
propagator.extract(&RequestHeaderCarrier::new(req.headers()))
});
span.set_parent(parent_context);
// If we have a remote parent span, this will be the parent's trace identifier.
// If not, it will be the newly generated trace identifier with this request as root span.
#[cfg(not(any(
feature = "opentelemetry_0_17",
feature = "opentelemetry_0_18",
feature = "opentelemetry_0_19",
feature = "opentelemetry_0_20",
feature = "opentelemetry_0_21",
feature = "opentelemetry_0_22",
feature = "opentelemetry_0_23",
feature = "opentelemetry_0_24",
feature = "opentelemetry_0_25",
)))]
let trace_id = span.context().span().span_context().trace_id().to_hex();
#[cfg(any(
feature = "opentelemetry_0_17",
feature = "opentelemetry_0_18",
feature = "opentelemetry_0_19",
feature = "opentelemetry_0_20",
feature = "opentelemetry_0_21",
feature = "opentelemetry_0_22",
feature = "opentelemetry_0_23",
feature = "opentelemetry_0_24",
feature = "opentelemetry_0_25",
))]
let trace_id = {
let id = span.context().span().span_context().trace_id();
format!("{:032x}", id)
};
span.record("trace_id", &tracing::field::display(trace_id));
}

View File

@ -0,0 +1,109 @@
use actix_web::{dev::Payload, HttpMessage};
use actix_web::{FromRequest, HttpRequest, ResponseError};
use std::future::{ready, Ready};
use uuid::Uuid;
/// A unique identifier generated for each incoming request.
///
/// Extracting a `RequestId` when the `TracingLogger` middleware is not registered will result in
/// an internal server error.
///
/// # Usage
/// ```rust
/// use actix_web::get;
/// use tracing_actix_web::RequestId;
/// use uuid::Uuid;
///
/// #[get("/")]
/// async fn index(request_id: RequestId) -> String {
/// format!("{}", request_id)
/// }
///
/// #[get("/2")]
/// async fn index2(request_id: RequestId) -> String {
/// let uuid: Uuid = request_id.into();
/// format!("{}", uuid)
/// }
/// ```
///
/// Optionally, using the `uuid_v7` feature flag will allow [`RequestId`] to use UUID v7 instead of the currently used UUID v4.
#[derive(Clone, Copy, Debug)]
pub struct RequestId(Uuid);
impl RequestId {
pub(crate) fn generate() -> Self {
#[cfg(not(feature = "uuid_v7"))]
{
Self(Uuid::new_v4())
}
#[cfg(feature = "uuid_v7")]
{
Self(Uuid::now_v7())
}
}
}
impl std::ops::Deref for RequestId {
type Target = Uuid;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<RequestId> for Uuid {
fn from(r: RequestId) -> Self {
r.0
}
}
impl std::fmt::Display for RequestId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromRequest for RequestId {
type Error = RequestIdExtractionError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(
req.extensions()
.get::<RequestId>()
.copied()
.ok_or(RequestIdExtractionError { _priv: () }),
)
}
}
#[derive(Debug)]
/// Error returned by the [`RequestId`] extractor when it fails to retrieve
/// the current request id from request-local storage.
///
/// It only happens if you try to extract the current request id without having
/// registered [`TracingLogger`] as a middleware for your application.
///
/// [`TracingLogger`]: crate::TracingLogger
pub struct RequestIdExtractionError {
// It turns out that a unit struct has a public constructor!
// Therefore adding fields to it (either public or private) later on
// is an API breaking change.
// Therefore we are adding a dummy private field that the compiler is going
// to optimise away to make sure users cannot construct this error
// manually in their own code.
_priv: (),
}
impl ResponseError for RequestIdExtractionError {}
impl std::fmt::Display for RequestIdExtractionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Failed to retrieve request id from request-local storage."
)
}
}
impl std::error::Error for RequestIdExtractionError {}

View File

@ -0,0 +1,92 @@
use actix_web::{dev::Payload, HttpMessage};
use actix_web::{FromRequest, HttpRequest, ResponseError};
use std::future::{ready, Ready};
use tracing::Span;
#[derive(Clone)]
/// The root span associated to the in-flight current request.
///
/// It can be used to populate additional properties using values computed or retrieved in the request
/// handler - see the crate-level documentation for more details.
///
/// Extracting a `RootSpan` when the `TracingLogger` middleware is not registered will result in
/// an internal server error.
///
/// # Usage
/// ```rust
/// use actix_web::get;
/// use tracing_actix_web::RootSpan;
/// use uuid::Uuid;
///
/// #[get("/")]
/// async fn index(root_span: RootSpan) -> String {
/// root_span.record("route", &"/");
/// # "Hello".to_string()
/// }
/// ```
pub struct RootSpan(Span);
impl RootSpan {
pub(crate) fn new(span: Span) -> Self {
Self(span)
}
}
impl std::ops::Deref for RootSpan {
type Target = Span;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<RootSpan> for Span {
fn from(r: RootSpan) -> Self {
r.0
}
}
impl FromRequest for RootSpan {
type Error = RootSpanExtractionError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(
req.extensions()
.get::<RootSpan>()
.cloned()
.ok_or(RootSpanExtractionError { _priv: () }),
)
}
}
#[derive(Debug)]
/// Error returned by the [`RootSpan`] extractor when it fails to retrieve
/// the root span from request-local storage.
///
/// It only happens if you try to extract the root span without having
/// registered [`TracingLogger`] as a middleware for your application.
///
/// [`TracingLogger`]: crate::TracingLogger
pub struct RootSpanExtractionError {
// It turns out that a unit struct has a public constructor!
// Therefore adding fields to it (either public or private) later on
// is an API breaking change.
// Therefore we are adding a dummy private field that the compiler is going
// to optimise away to make sure users cannot construct this error
// manually in their own code.
_priv: (),
}
impl ResponseError for RootSpanExtractionError {}
impl std::fmt::Display for RootSpanExtractionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Failed to retrieve the root span from request-local storage."
)
}
}
impl std::error::Error for RootSpanExtractionError {}

View File

@ -0,0 +1,79 @@
use crate::root_span;
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::http::StatusCode;
use actix_web::{Error, ResponseError};
use tracing::Span;
/// `RootSpanBuilder` allows you to customise the root span attached by
/// [`TracingLogger`] to incoming requests.
///
/// [`TracingLogger`]: crate::TracingLogger
pub trait RootSpanBuilder {
fn on_request_start(request: &ServiceRequest) -> Span;
fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>);
}
/// The default [`RootSpanBuilder`] for [`TracingLogger`].
///
/// It captures:
/// - HTTP method (`http.method`);
/// - HTTP route (`http.route`), with templated parameters;
/// - HTTP version (`http.flavor`);
/// - HTTP host (`http.host`);
/// - Client IP (`http.client_ip`);
/// - User agent (`http.user_agent`);
/// - Request path (`http.target`);
/// - Status code (`http.status_code`);
/// - [Request id](crate::RequestId) (`request_id`);
/// - `Display` (`exception.message`) and `Debug` (`exception.details`) representations of the error, if there was an error;
/// - [Request id](crate::RequestId) (`request_id`);
/// - [OpenTelemetry trace identifier](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext) (`trace_id`). Empty if the feature is not enabled;
/// - OpenTelemetry span kind, set to `server` (`otel.kind`).
///
/// All field names follow [OpenTelemetry's semantic convention](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions).
///
/// [`TracingLogger`]: crate::TracingLogger
pub struct DefaultRootSpanBuilder;
impl RootSpanBuilder for DefaultRootSpanBuilder {
fn on_request_start(request: &ServiceRequest) -> Span {
root_span!(level = crate::Level::INFO, request)
}
fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
match &outcome {
Ok(response) => {
if let Some(error) = response.response().error() {
// use the status code already constructed for the outgoing HTTP response
handle_error(span, response.status(), error.as_response_error());
} else {
let code: i32 = response.response().status().as_u16().into();
span.record("http.status_code", code);
span.record("otel.status_code", "OK");
}
}
Err(error) => {
let response_error = error.as_response_error();
handle_error(span, response_error.status_code(), response_error);
}
};
}
}
fn handle_error(span: Span, status_code: StatusCode, response_error: &dyn ResponseError) {
// pre-formatting errors is a workaround for https://github.com/tokio-rs/tracing/issues/1565
let display = format!("{response_error}");
let debug = format!("{response_error:?}");
span.record("exception.message", tracing::field::display(display));
span.record("exception.details", tracing::field::display(debug));
let code: i32 = status_code.as_u16().into();
span.record("http.status_code", code);
if status_code.is_client_error() {
span.record("otel.status_code", "OK");
} else {
span.record("otel.status_code", "ERROR");
}
}

View File

@ -0,0 +1,227 @@
#[macro_export]
/// `root_span!` creates a new [`tracing::Span`].
/// It empowers you to add custom properties to the root span on top of the HTTP properties tracked
/// by [`DefaultRootSpanBuilder`].
///
/// # Why a macro?
///
/// `tracing` requires all the properties attached to a span to be declared upfront, when the span is created.
/// You cannot add new ones afterwards.
/// This makes it extremely fast, but it pushes us to reach for macros when we need some level of composition.
///
/// # Macro syntax
///
/// The first argument passed to `root_span!` must be a reference to an [`actix_web::dev::ServiceRequest`].
///
/// ```rust
/// use actix_web::body::MessageBody;
/// use actix_web::dev::{ServiceResponse, ServiceRequest};
/// use actix_web::Error;
/// use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder, RootSpanBuilder, root_span};
/// use tracing::Span;
///
/// pub struct CustomRootSpanBuilder;
///
/// impl RootSpanBuilder for CustomRootSpanBuilder {
/// fn on_request_start(request: &ServiceRequest) -> Span {
/// root_span!(request)
/// }
///
/// fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
/// DefaultRootSpanBuilder::on_request_end(span, outcome);
/// }
/// }
/// ```
///
/// If nothing else is specified, the span generated by `root_span!` is identical to the one you'd
/// get by using `DefaultRootSpanBuilder`.
///
/// You can define new fields following the same syntax of `tracing::info_span!` for fields:
///
/// ```rust,should_panic
/// # let request: &actix_web::dev::ServiceRequest = todo!();
/// use tracing_actix_web::Level;
///
/// // Define a `client_id` field as empty. It might be populated later.
/// tracing_actix_web::root_span!(request, client_id = tracing::field::Empty);
///
/// // Define a `name` field with a known value, `AppName`.
/// tracing_actix_web::root_span!(request, name = "AppName");
///
/// // Define an `app_id` field using the variable with the same name as value.
/// let app_id = "XYZ";
/// tracing_actix_web::root_span!(request, app_id);
///
/// // Use a custom level, `DEBUG`, instead of the default (`INFO`).
/// tracing_actix_web::root_span!(level = Level::DEBUG, request);
///
/// // All together
/// tracing_actix_web::root_span!(request, client_id = tracing::field::Empty, name = "AppName", app_id);
/// ```
///
/// [`DefaultRootSpanBuilder`]: crate::DefaultRootSpanBuilder
macro_rules! root_span {
// Vanilla root span, with no additional fields
($request:ident) => {
$crate::root_span!($request,)
};
// Vanilla root span, with a level but no additional fields
(level = $lvl:expr, $request:ident) => {
$crate::root_span!(level = $lvl, $request,)
};
// One or more additional fields, comma separated, without a level
($request:ident, $($field:tt)*) => {
$crate::root_span!(level = $crate::Level::INFO, $request, $($field)*)
};
// One or more additional fields, comma separated
(level = $lvl:expr, $request:ident, $($field:tt)*) => {
{
let user_agent = $request
.headers()
.get("User-Agent")
.map(|h| h.to_str().unwrap_or(""))
.unwrap_or("");
let http_route: std::borrow::Cow<'static, str> = $request
.match_pattern()
.map(Into::into)
.unwrap_or_else(|| "default".into());
let http_method = $crate::root_span_macro::private::http_method_str($request.method());
let connection_info = $request.connection_info();
let request_id = $crate::root_span_macro::private::get_request_id($request);
macro_rules! inner_span {
($level:expr) => {
$crate::root_span_macro::private::tracing::span!(
$level,
"HTTP request",
http.method = %http_method,
http.route = %http_route,
http.flavor = %$crate::root_span_macro::private::http_flavor($request.version()),
http.scheme = %$crate::root_span_macro::private::http_scheme(connection_info.scheme()),
http.host = %connection_info.host(),
http.client_ip = %$request.connection_info().realip_remote_addr().unwrap_or(""),
http.user_agent = %user_agent,
http.target = %$request.uri().path_and_query().map(|p| p.as_str()).unwrap_or(""),
http.status_code = $crate::root_span_macro::private::tracing::field::Empty,
otel.name = %format!("{} {}", http_method, http_route),
otel.kind = "server",
otel.status_code = $crate::root_span_macro::private::tracing::field::Empty,
trace_id = $crate::root_span_macro::private::tracing::field::Empty,
request_id = %request_id,
exception.message = $crate::root_span_macro::private::tracing::field::Empty,
// Not proper OpenTelemetry, but their terminology is fairly exception-centric
exception.details = $crate::root_span_macro::private::tracing::field::Empty,
$($field)*
)
};
}
let span = match $lvl {
$crate::Level::TRACE => inner_span!($crate::Level::TRACE),
$crate::Level::DEBUG => inner_span!($crate::Level::DEBUG),
$crate::Level::INFO => inner_span!($crate::Level::INFO),
$crate::Level::WARN => inner_span!($crate::Level::WARN),
$crate::Level::ERROR => inner_span!($crate::Level::ERROR),
};
std::mem::drop(connection_info);
// Previously, this line was instrumented with an opentelemetry-specific feature
// flag check. However, this resulted in the feature flags being resolved in the crate
// which called `root_span!` as opposed to being resolved by this crate as expected.
// Therefore, this function simply wraps an internal function with the feature flags
// to ensure that the flags are resolved against this crate.
$crate::root_span_macro::private::set_otel_parent(&$request, &span);
span
}
};
}
#[doc(hidden)]
pub mod private {
//! This module exposes and re-exports various functions and traits as public in order to leverage them
//! in the code generated by the `root_span` macro.
//! Items in this module are not part of the public interface of `tracing-actix-web` - they are considered
//! implementation details and will change without notice in patch, minor and major releases.
use crate::RequestId;
use actix_web::dev::ServiceRequest;
use actix_web::http::{Method, Version};
use std::borrow::Cow;
pub use tracing;
#[doc(hidden)]
// We need to allow unused variables because the function
// body is empty if the user of the library chose not to activate
// any OTEL feature.
#[allow(unused_variables)]
pub fn set_otel_parent(req: &ServiceRequest, span: &tracing::Span) {
#[cfg(any(
feature = "opentelemetry_0_13",
feature = "opentelemetry_0_14",
feature = "opentelemetry_0_15",
feature = "opentelemetry_0_16",
feature = "opentelemetry_0_17",
feature = "opentelemetry_0_18",
feature = "opentelemetry_0_19",
feature = "opentelemetry_0_20",
feature = "opentelemetry_0_21",
feature = "opentelemetry_0_22",
feature = "opentelemetry_0_23",
feature = "opentelemetry_0_24",
feature = "opentelemetry_0_25",
))]
crate::otel::set_otel_parent(req, span);
}
#[doc(hidden)]
#[inline]
pub fn http_method_str(method: &Method) -> Cow<'static, str> {
match method {
&Method::OPTIONS => "OPTIONS".into(),
&Method::GET => "GET".into(),
&Method::POST => "POST".into(),
&Method::PUT => "PUT".into(),
&Method::DELETE => "DELETE".into(),
&Method::HEAD => "HEAD".into(),
&Method::TRACE => "TRACE".into(),
&Method::CONNECT => "CONNECT".into(),
&Method::PATCH => "PATCH".into(),
other => other.to_string().into(),
}
}
#[doc(hidden)]
#[inline]
pub fn http_flavor(version: Version) -> Cow<'static, str> {
match version {
Version::HTTP_09 => "0.9".into(),
Version::HTTP_10 => "1.0".into(),
Version::HTTP_11 => "1.1".into(),
Version::HTTP_2 => "2.0".into(),
Version::HTTP_3 => "3.0".into(),
other => format!("{other:?}").into(),
}
}
#[doc(hidden)]
#[inline]
pub fn http_scheme(scheme: &str) -> Cow<'static, str> {
match scheme {
"http" => "http".into(),
"https" => "https".into(),
other => other.to_string().into(),
}
}
#[doc(hidden)]
pub fn generate_request_id() -> RequestId {
RequestId::generate()
}
#[doc(hidden)]
pub fn get_request_id(request: &ServiceRequest) -> RequestId {
use actix_web::HttpMessage;
request.extensions().get::<RequestId>().cloned().unwrap()
}
}