tracing_actix_web/
root_span_builder.rs

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