actix_web_opentelemetry/
util.rs

1use actix_http::header::{self, CONTENT_LENGTH};
2use actix_web::{
3    dev::ServiceRequest,
4    http::{Method, Version},
5};
6use opentelemetry::{KeyValue, Value};
7use opentelemetry_semantic_conventions::{
8    attribute::MESSAGING_MESSAGE_BODY_SIZE,
9    trace::{
10        CLIENT_ADDRESS, HTTP_REQUEST_METHOD, HTTP_ROUTE, NETWORK_PEER_ADDRESS,
11        NETWORK_PROTOCOL_VERSION, SERVER_ADDRESS, SERVER_PORT, URL_PATH, URL_QUERY, URL_SCHEME,
12        USER_AGENT_ORIGINAL,
13    },
14};
15
16#[cfg(feature = "awc")]
17#[inline]
18pub(super) fn http_url(uri: &actix_web::http::Uri) -> String {
19    let scheme = uri.scheme().map(|s| s.as_str()).unwrap_or_default();
20    let host = uri.host().unwrap_or_default();
21    let path = uri.path();
22    let port = uri.port_u16().filter(|&p| p != 80 && p != 443);
23    let (query, query_delimiter) = if let Some(query) = uri.query() {
24        (query, "?")
25    } else {
26        ("", "")
27    };
28
29    if let Some(port) = port {
30        format!("{scheme}://{host}:{port}{path}{query_delimiter}{query}")
31    } else {
32        format!("{scheme}://{host}{path}{query_delimiter}{query}")
33    }
34}
35
36#[inline]
37pub(super) fn http_method_str(method: &Method) -> Value {
38    match method {
39        &Method::OPTIONS => "OPTIONS".into(),
40        &Method::GET => "GET".into(),
41        &Method::POST => "POST".into(),
42        &Method::PUT => "PUT".into(),
43        &Method::DELETE => "DELETE".into(),
44        &Method::HEAD => "HEAD".into(),
45        &Method::TRACE => "TRACE".into(),
46        &Method::CONNECT => "CONNECT".into(),
47        &Method::PATCH => "PATCH".into(),
48        other => other.to_string().into(),
49    }
50}
51
52#[inline]
53pub(super) fn protocol_version(version: Version) -> Value {
54    match version {
55        Version::HTTP_09 => "0.9".into(),
56        Version::HTTP_10 => "1.0".into(),
57        Version::HTTP_11 => "1.1".into(),
58        Version::HTTP_2 => "2".into(),
59        Version::HTTP_3 => "3".into(),
60        other => format!("{:?}", other).into(),
61    }
62}
63
64#[inline]
65pub(super) fn url_scheme(scheme: &str) -> Value {
66    match scheme {
67        "http" => "http".into(),
68        "https" => "https".into(),
69        other => other.to_string().into(),
70    }
71}
72
73pub(super) fn trace_attributes_from_request(
74    req: &ServiceRequest,
75    http_route: &str,
76) -> Vec<KeyValue> {
77    let conn_info = req.connection_info();
78    let remote_addr = conn_info.realip_remote_addr();
79
80    let mut attributes = Vec::with_capacity(14);
81
82    // Server attrs
83    // <https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md#http-server>
84    attributes.push(KeyValue::new(HTTP_ROUTE, http_route.to_owned()));
85    if let Some(remote) = remote_addr {
86        attributes.push(KeyValue::new(CLIENT_ADDRESS, remote.to_string()));
87    }
88    if let Some(peer_addr) = req.peer_addr().map(|socket| socket.ip().to_string()) {
89        if Some(peer_addr.as_str()) != remote_addr {
90            // Client is going through a proxy
91            attributes.push(KeyValue::new(NETWORK_PEER_ADDRESS, peer_addr));
92        }
93    }
94    let mut host_parts = conn_info.host().split_terminator(':');
95    if let Some(host) = host_parts.next() {
96        attributes.push(KeyValue::new(SERVER_ADDRESS, host.to_string()));
97    }
98    if let Some(port) = host_parts.next().and_then(|port| port.parse::<i64>().ok()) {
99        if port != 80 && port != 443 {
100            attributes.push(KeyValue::new(SERVER_PORT, port));
101        }
102    }
103    if let Some(path_query) = req.uri().path_and_query() {
104        if path_query.path() != "/" {
105            attributes.push(KeyValue::new(URL_PATH, path_query.path().to_string()));
106        }
107        if let Some(query) = path_query.query() {
108            attributes.push(KeyValue::new(URL_QUERY, query.to_string()));
109        }
110    }
111    attributes.push(KeyValue::new(URL_SCHEME, url_scheme(conn_info.scheme())));
112
113    // Common attrs
114    // <https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md#common-attributes>
115    attributes.push(KeyValue::new(
116        HTTP_REQUEST_METHOD,
117        http_method_str(req.method()),
118    ));
119    attributes.push(KeyValue::new(
120        NETWORK_PROTOCOL_VERSION,
121        protocol_version(req.version()),
122    ));
123
124    if let Some(content_length) = req
125        .headers()
126        .get(CONTENT_LENGTH)
127        .and_then(|len| len.to_str().ok().and_then(|s| s.parse::<i64>().ok()))
128        .filter(|&len| len > 0)
129    {
130        attributes.push(KeyValue::new(MESSAGING_MESSAGE_BODY_SIZE, content_length));
131    }
132
133    if let Some(user_agent) = req
134        .headers()
135        .get(header::USER_AGENT)
136        .and_then(|s| s.to_str().ok())
137    {
138        attributes.push(KeyValue::new(USER_AGENT_ORIGINAL, user_agent.to_string()));
139    }
140
141    attributes
142}
143
144/// Create metric attributes for the given request
145#[cfg(feature = "metrics")]
146pub fn metrics_attributes_from_request(
147    req: &ServiceRequest,
148    http_route: std::borrow::Cow<'static, str>,
149) -> Vec<KeyValue> {
150    let conn_info = req.connection_info();
151
152    let mut attributes = Vec::with_capacity(7);
153    attributes.push(KeyValue::new(HTTP_ROUTE, http_route));
154    attributes.push(KeyValue::new(
155        HTTP_REQUEST_METHOD,
156        http_method_str(req.method()),
157    ));
158    attributes.push(KeyValue::new(
159        NETWORK_PROTOCOL_VERSION,
160        protocol_version(req.version()),
161    ));
162
163    let mut host_parts = conn_info.host().split_terminator(':');
164    if let Some(host) = host_parts.next() {
165        attributes.push(KeyValue::new(SERVER_ADDRESS, host.to_string()));
166    }
167    if let Some(port) = host_parts.next().and_then(|port| port.parse::<i64>().ok()) {
168        attributes.push(KeyValue::new(SERVER_PORT, port))
169    }
170    attributes.push(KeyValue::new(URL_SCHEME, url_scheme(conn_info.scheme())));
171
172    attributes
173}