Skip to main content

soaprs_http/
telemetry.rs

1//! Provider-neutral telemetry declarations with safe defaults.
2
3use http::HeaderName;
4use http::header::{AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, SET_COOKIE};
5use soaprs_core::{SoapError, SoapResult};
6
7/// Endpoint-level telemetry instructions consumed by tracing and metrics adapters.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct TelemetryPolicy {
10    /// Whether request tracing and metrics are enabled.
11    pub enabled: bool,
12    /// Optional stable span name; endpoint identity is used when absent.
13    pub span_name: Option<String>,
14    request_headers: Vec<HeaderName>,
15    response_headers: Vec<HeaderName>,
16}
17
18impl TelemetryPolicy {
19    /// Creates enabled telemetry without recording request or response data.
20    pub const fn enabled() -> Self {
21        Self {
22            enabled: true,
23            span_name: None,
24            request_headers: Vec::new(),
25            response_headers: Vec::new(),
26        }
27    }
28
29    /// Disables endpoint tracing and metrics explicitly.
30    pub const fn disabled() -> Self {
31        Self {
32            enabled: false,
33            span_name: None,
34            request_headers: Vec::new(),
35            response_headers: Vec::new(),
36        }
37    }
38
39    /// Sets a stable non-empty span name.
40    pub fn span_name(mut self, span_name: impl Into<String>) -> SoapResult<Self> {
41        let span_name = span_name.into();
42        if span_name.trim().is_empty() || span_name.chars().any(char::is_control) {
43            return Err(SoapError::validation(
44                "telemetry span name is empty or contains control characters",
45            ));
46        }
47        self.span_name = Some(span_name);
48        Ok(self)
49    }
50
51    /// Allows one non-sensitive request header to be recorded.
52    pub fn record_request_header(mut self, header: HeaderName) -> SoapResult<Self> {
53        if sensitive_request_header(&header) {
54            return Err(SoapError::validation(format!(
55                "sensitive request header `{header}` cannot be recorded"
56            )));
57        }
58        push_once(&mut self.request_headers, header);
59        Ok(self)
60    }
61
62    /// Allows one non-sensitive response header to be recorded.
63    pub fn record_response_header(mut self, header: HeaderName) -> SoapResult<Self> {
64        if header == SET_COOKIE {
65            return Err(SoapError::validation(
66                "sensitive response header `set-cookie` cannot be recorded",
67            ));
68        }
69        push_once(&mut self.response_headers, header);
70        Ok(self)
71    }
72
73    /// Returns the allowlist of request headers safe to record.
74    pub fn request_headers(&self) -> &[HeaderName] {
75        &self.request_headers
76    }
77
78    /// Returns the allowlist of response headers safe to record.
79    pub fn response_headers(&self) -> &[HeaderName] {
80        &self.response_headers
81    }
82
83    /// Validates public telemetry fields after direct mutation.
84    pub fn validate(&self) -> SoapResult<()> {
85        if self.span_name.as_ref().is_some_and(|span_name| {
86            span_name.trim().is_empty() || span_name.chars().any(char::is_control)
87        }) {
88            return Err(SoapError::validation(
89                "telemetry span name is empty or contains control characters",
90            ));
91        }
92        Ok(())
93    }
94}
95
96impl Default for TelemetryPolicy {
97    fn default() -> Self {
98        Self::enabled()
99    }
100}
101
102fn sensitive_request_header(header: &HeaderName) -> bool {
103    header == AUTHORIZATION
104        || header == PROXY_AUTHORIZATION
105        || header == COOKIE
106        || header == SET_COOKIE
107}
108
109fn push_once(headers: &mut Vec<HeaderName>, header: HeaderName) {
110    if !headers.contains(&header) {
111        headers.push(header);
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use http::header::{AUTHORIZATION, CONTENT_TYPE, SET_COOKIE};
118
119    use super::TelemetryPolicy;
120
121    #[test]
122    fn sensitive_headers_are_denied_from_telemetry_allowlists() {
123        assert!(
124            TelemetryPolicy::enabled()
125                .record_request_header(AUTHORIZATION)
126                .is_err()
127        );
128        assert!(
129            TelemetryPolicy::enabled()
130                .record_response_header(SET_COOKIE)
131                .is_err()
132        );
133        let policy = TelemetryPolicy::enabled().record_request_header(CONTENT_TYPE);
134        assert_eq!(
135            policy.ok().map(|value| value.request_headers().len()),
136            Some(1)
137        );
138    }
139}