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:
commit
b3a26979a1
@ -5,3 +5,4 @@ ci-check-min-examples = "hack check --workspace --no-default-features --examples
|
|||||||
ci-check = "check --workspace --tests --examples --bins"
|
ci-check = "check --workspace --tests --examples --bins"
|
||||||
ci-test = "test --workspace --lib --tests --all-features --examples --bins --no-fail-fast"
|
ci-test = "test --workspace --lib --tests --all-features --examples --bins --no-fail-fast"
|
||||||
ci-doctest = "test --workspace --doc --all-features --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"
|
||||||
|
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -69,7 +69,10 @@ jobs:
|
|||||||
|
|
||||||
- name: tests
|
- name: tests
|
||||||
timeout-minutes: 40
|
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
|
- name: CI cache clean
|
||||||
run: cargo-ci-cache-clean
|
run: cargo-ci-cache-clean
|
||||||
@ -127,7 +130,10 @@ jobs:
|
|||||||
|
|
||||||
- name: tests
|
- name: tests
|
||||||
timeout-minutes: 40
|
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
|
- name: CI cache clean
|
||||||
run: cargo-ci-cache-clean
|
run: cargo-ci-cache-clean
|
||||||
|
@ -9,6 +9,10 @@ members = [
|
|||||||
"actix-settings",
|
"actix-settings",
|
||||||
"actix-web-httpauth",
|
"actix-web-httpauth",
|
||||||
"actix-ws",
|
"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]
|
[workspace.package]
|
||||||
|
111
tracing-actix-web/Cargo.toml
Normal file
111
tracing-actix-web/Cargo.toml
Normal 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"
|
1
tracing-actix-web/LICENSE-APACHE
Symbolic link
1
tracing-actix-web/LICENSE-APACHE
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../LICENSE-APACHE
|
1
tracing-actix-web/LICENSE-MIT
Symbolic link
1
tracing-actix-web/LICENSE-MIT
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../LICENSE-MIT
|
331
tracing-actix-web/README.md
Normal file
331
tracing-actix-web/README.md
Normal 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
|
17
tracing-actix-web/examples/custom-root-span/Cargo.toml
Normal file
17
tracing-actix-web/examples/custom-root-span/Cargo.toml
Normal 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"] }
|
38
tracing-actix-web/examples/custom-root-span/README.md
Normal file
38
tracing-actix-web/examples/custom-root-span/README.md
Normal 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
|
||||||
|
```
|
116
tracing-actix-web/examples/custom-root-span/src/main.rs
Normal file
116
tracing-actix-web/examples/custom-root-span/src/main.rs
Normal 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.")
|
||||||
|
}
|
17
tracing-actix-web/examples/opentelemetry/Cargo.toml
Normal file
17
tracing-actix-web/examples/opentelemetry/Cargo.toml
Normal 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"
|
33
tracing-actix-web/examples/opentelemetry/README.md
Normal file
33
tracing-actix-web/examples/opentelemetry/README.md
Normal 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.
|
75
tracing-actix-web/examples/opentelemetry/src/main.rs
Normal file
75
tracing-actix-web/examples/opentelemetry/src/main.rs
Normal 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(())
|
||||||
|
}
|
@ -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 = "../.." }
|
@ -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
|
||||||
|
...
|
||||||
|
```
|
@ -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(())
|
||||||
|
}
|
329
tracing-actix-web/src/lib.rs
Normal file
329
tracing-actix-web/src/lib.rs
Normal 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;
|
266
tracing-actix-web/src/middleware.rs
Normal file
266
tracing-actix-web/src/middleware.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
119
tracing-actix-web/src/otel.rs
Normal file
119
tracing-actix-web/src/otel.rs
Normal 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));
|
||||||
|
}
|
109
tracing-actix-web/src/request_id.rs
Normal file
109
tracing-actix-web/src/request_id.rs
Normal 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 {}
|
92
tracing-actix-web/src/root_span.rs
Normal file
92
tracing-actix-web/src/root_span.rs
Normal 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 {}
|
79
tracing-actix-web/src/root_span_builder.rs
Normal file
79
tracing-actix-web/src/root_span_builder.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
227
tracing-actix-web/src/root_span_macro.rs
Normal file
227
tracing-actix-web/src/root_span_macro.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user