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}