tracing_actix_web/
root_span_builder.rs

1use crate::root_span;
2use actix_web::body::MessageBody;
3use actix_web::dev::{ServiceRequest, ServiceResponse};
4use actix_web::http::StatusCode;
5use actix_web::{Error, ResponseError};
6use tracing::Span;
7
8/// `RootSpanBuilder` allows you to customise the root span attached by
9/// [`TracingLogger`] to incoming requests.
10///
11/// [`TracingLogger`]: crate::TracingLogger
12pub trait RootSpanBuilder {
13    fn on_request_start(request: &ServiceRequest) -> Span;
14    fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>);
15}
16
17/// The default [`RootSpanBuilder`] for [`TracingLogger`].
18///
19/// It captures:
20/// - HTTP method (`http.method`);
21/// - HTTP route (`http.route`), with templated parameters;
22/// - HTTP version (`http.flavor`);
23/// - HTTP host (`http.host`);
24/// - Client IP (`http.client_ip`);
25/// - User agent (`http.user_agent`);
26/// - Request path (`http.target`);
27/// - Status code (`http.status_code`);
28/// - [Request id](crate::RequestId) (`request_id`);
29/// - `Display` (`exception.message`) and `Debug` (`exception.details`) representations of the error, if there was an error;
30/// - [Request id](crate::RequestId) (`request_id`);
31/// - [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;
32/// - OpenTelemetry span kind, set to `server` (`otel.kind`).
33///
34/// All field names follow [OpenTelemetry's semantic convention](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions).
35///
36/// [`TracingLogger`]: crate::TracingLogger
37pub struct DefaultRootSpanBuilder;
38
39impl RootSpanBuilder for DefaultRootSpanBuilder {
40    fn on_request_start(request: &ServiceRequest) -> Span {
41        root_span!(level = crate::Level::INFO, request)
42    }
43
44    fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
45        match &outcome {
46            Ok(response) => {
47                if let Some(error) = response.response().error() {
48                    // use the status code already constructed for the outgoing HTTP response
49                    handle_error(span, response.status(), error.as_response_error());
50                } else {
51                    let code: i32 = response.response().status().as_u16().into();
52                    span.record("http.status_code", code);
53                    span.record("otel.status_code", "OK");
54                }
55            }
56            Err(error) => {
57                let response_error = error.as_response_error();
58                handle_error(span, response_error.status_code(), response_error);
59            }
60        };
61    }
62}
63
64fn handle_error(span: Span, status_code: StatusCode, response_error: &dyn ResponseError) {
65    // pre-formatting errors is a workaround for https://github.com/tokio-rs/tracing/issues/1565
66    let display = format!("{response_error}");
67    let debug = format!("{response_error:?}");
68    span.record("exception.message", tracing::field::display(display));
69    span.record("exception.details", tracing::field::display(debug));
70    let code: i32 = status_code.as_u16().into();
71
72    span.record("http.status_code", code);
73
74    if status_code.is_client_error() {
75        span.record("otel.status_code", "OK");
76    } else {
77        span.record("otel.status_code", "ERROR");
78    }
79}