Skip to main content

spin_trigger_http/
instrument.rs

1use anyhow::Result;
2use http::Response;
3use opentelemetry_semantic_conventions::attribute as otel_attribute;
4use tracing::Level;
5
6use crate::Body;
7
8/// Create a span for an HTTP request.
9macro_rules! http_span {
10    ($request:tt, $addr:tt) => {
11        tracing::info_span!(
12            "spin_trigger_http.handle_http_request",
13            "otel.kind" = "server",
14            {opentelemetry_semantic_conventions::attribute::HTTP_REQUEST_METHOD} = %$request.method(),
15            {opentelemetry_semantic_conventions::attribute::NETWORK_PEER_ADDRESS} = %$addr.ip(),
16            {opentelemetry_semantic_conventions::attribute::NETWORK_PEER_PORT} = %$addr.port(),
17            {opentelemetry_semantic_conventions::attribute::NETWORK_PROTOCOL_NAME} = "http",
18            {opentelemetry_semantic_conventions::attribute::URL_PATH} = $request.uri().path(),
19            {opentelemetry_semantic_conventions::attribute::URL_QUERY} = $request.uri().query().unwrap_or(""),
20            {opentelemetry_semantic_conventions::attribute::URL_SCHEME} = $request.uri().scheme_str().unwrap_or(""),
21            {opentelemetry_semantic_conventions::attribute::CLIENT_ADDRESS} = $request.headers().get("x-forwarded-for").and_then(|val| val.to_str().ok()),
22            // Recorded later
23            {opentelemetry_semantic_conventions::attribute::ERROR_TYPE} = ::tracing::field::Empty,
24            {opentelemetry_semantic_conventions::attribute::HTTP_RESPONSE_STATUS_CODE} = ::tracing::field::Empty,
25            {opentelemetry_semantic_conventions::attribute::HTTP_ROUTE} = ::tracing::field::Empty,
26            "otel.name" = ::tracing::field::Empty,
27        )
28    };
29}
30
31pub(crate) use http_span;
32
33/// Finish setting attributes on the HTTP span.
34pub(crate) fn finalize_http_span(
35    response: Result<Response<Body>>,
36    method: String,
37) -> Result<Response<Body>> {
38    let span = tracing::Span::current();
39    match response {
40        Ok(response) => {
41            tracing::info!(
42                "Request finished, sending response with status code {}",
43                response.status()
44            );
45
46            let matched_route = response.extensions().get::<MatchedRoute>();
47            // Set otel.name and http.route
48            if let Some(MatchedRoute { route }) = matched_route {
49                span.record(otel_attribute::HTTP_ROUTE, route);
50                span.record("otel.name", format!("{method} {route}"));
51            } else {
52                span.record("otel.name", method);
53            }
54
55            // Set status code
56            span.record(
57                otel_attribute::HTTP_RESPONSE_STATUS_CODE,
58                response.status().as_u16(),
59            );
60
61            Ok(response)
62        }
63        Err(err) => {
64            instrument_error(&err);
65            span.record(otel_attribute::HTTP_RESPONSE_STATUS_CODE, 500);
66            span.record("otel.name", method);
67            Err(err)
68        }
69    }
70}
71
72/// Marks the current span as errored.
73pub(crate) fn instrument_error(err: &anyhow::Error) {
74    let span = tracing::Span::current();
75    tracing::event!(target:module_path!(), Level::INFO, error = %err);
76    span.record(otel_attribute::ERROR_TYPE, format!("{err:?}"));
77}
78
79/// MatchedRoute is used as a response extension to track the route that was matched for OTel
80/// tracing purposes.
81#[derive(Clone)]
82pub struct MatchedRoute {
83    pub route: String,
84}
85
86impl MatchedRoute {
87    pub fn set_response_extension(resp: &mut Response<Body>, route: impl Into<String>) {
88        resp.extensions_mut().insert(MatchedRoute {
89            route: route.into(),
90        });
91    }
92
93    pub fn with_response_extension(
94        mut resp: Response<Body>,
95        route: impl Into<String>,
96    ) -> Response<Body> {
97        Self::set_response_extension(&mut resp, route);
98        resp
99    }
100}