Skip to main content

objectstore_server/extractors/
downstream_service.rs

1//! Downstream service extractor from the `x-downstream-service` request header.
2
3use axum::extract::FromRequestParts;
4use axum::http::request::Parts;
5
6/// Header used to identify the downstream service making the request, for use in killswitches and
7/// logging.
8const HEADER_SERVICE: &str = "x-downstream-service";
9
10/// Extractor for the downstream service identifier from the request header.
11///
12/// This extracts the `x-downstream-service` header value, which is used to identify which
13/// Kubernetes service or downstream system is making the request. This can be used for
14/// killswitches, rate limiting, and logging.
15#[derive(Debug, Clone)]
16pub struct DownstreamService(pub Option<String>);
17
18impl DownstreamService {
19    /// Returns the downstream service identifier, if present.
20    pub fn as_str(&self) -> Option<&str> {
21        self.0.as_deref()
22    }
23}
24
25impl std::fmt::Display for DownstreamService {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.write_str(self.0.as_deref().unwrap_or("unknown"))
28    }
29}
30
31/// Strips a Kubernetes pod suffix from a service name, leaving the base workload name.
32///
33/// Deployment pods are named `<deployment>-<replicaset-hash>-<pod-suffix>` and StatefulSet
34/// pods `<name>-<ordinal>`. The hash/suffix and ordinal change on every rollout, creating
35/// useless metric cardinality. Segment lengths follow the Kubernetes naming rules to limit
36/// false positives; anything else is returned unchanged.
37fn strip_pod_suffix(service: &str) -> &str {
38    let is_hash = |segment: &str| {
39        segment
40            .chars()
41            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
42    };
43
44    let Some((rest, last)) = service.rsplit_once('-') else {
45        return service;
46    };
47    if rest.is_empty() {
48        return service;
49    }
50
51    // Deployment: the pod suffix is 5 chars and the ReplicaSet hash is 7-10 chars.
52    if last.len() == 5
53        && is_hash(last)
54        && let Some((deployment, hash)) = rest.rsplit_once('-')
55        && !deployment.is_empty()
56        && (7..=10).contains(&hash.len())
57        && is_hash(hash)
58    {
59        return deployment;
60    }
61
62    // StatefulSet: the pod suffix is a numeric ordinal.
63    if !last.is_empty() && last.bytes().all(|b| b.is_ascii_digit()) {
64        return rest;
65    }
66
67    service
68}
69
70impl<S: Send + Sync> FromRequestParts<S> for DownstreamService {
71    type Rejection = std::convert::Infallible;
72
73    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
74        let service = parts
75            .headers
76            .get(HEADER_SERVICE)
77            .and_then(|v| v.to_str().ok())
78            .map(str::to_owned);
79
80        if let Some(ref service) = service {
81            // Tag the raw pod name intentionally: it pinpoints the exact instance when
82            // debugging, and tag cardinality is not a concern here.
83            sentry::configure_scope(|s| {
84                s.set_tag("downstream_service", service);
85            });
86        }
87
88        let normalized = service.as_deref().map(strip_pod_suffix).map(str::to_owned);
89        Ok(DownstreamService(normalized))
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn strips_typical_pod_name() {
99        assert_eq!(
100            strip_pod_suffix("getsentry-incinerator-7d8f9c5b6d-abc12"),
101            "getsentry-incinerator"
102        );
103    }
104
105    #[test]
106    fn strips_single_segment_deployment() {
107        assert_eq!(strip_pod_suffix("service-7d8f9c5b6d-abcde"), "service");
108    }
109
110    #[test]
111    fn keeps_plain_name() {
112        assert_eq!(strip_pod_suffix("relay"), "relay");
113    }
114
115    #[test]
116    fn keeps_name_without_pod_suffix() {
117        assert_eq!(
118            strip_pod_suffix("getsentry-incinerator"),
119            "getsentry-incinerator"
120        );
121    }
122
123    #[test]
124    fn keeps_short_trailing_segment() {
125        assert_eq!(strip_pod_suffix("my-service"), "my-service");
126    }
127
128    #[test]
129    fn keeps_uppercase_suffix() {
130        assert_eq!(
131            strip_pod_suffix("my-svc-7d8f9c5b6d-ABC12"),
132            "my-svc-7d8f9c5b6d-ABC12"
133        );
134    }
135
136    #[test]
137    fn keeps_empty_deployment() {
138        assert_eq!(strip_pod_suffix("-abcde-fghij"), "-abcde-fghij");
139    }
140
141    #[test]
142    fn keeps_wrong_length_hash() {
143        // Hash segment is just outside the 7-10 char range, so it is not a ReplicaSet hash.
144        assert_eq!(strip_pod_suffix("foo-sixchr-abcde"), "foo-sixchr-abcde");
145        assert_eq!(
146            strip_pod_suffix("foo-abcdefghijk-abcde"),
147            "foo-abcdefghijk-abcde"
148        );
149    }
150
151    #[test]
152    fn strips_min_length_hash() {
153        // A 7-char hash is the shortest that is still treated as a ReplicaSet hash.
154        assert_eq!(strip_pod_suffix("svc-abcdefg-hijkl"), "svc");
155    }
156
157    #[test]
158    fn strips_statefulset_ordinal() {
159        assert_eq!(
160            strip_pod_suffix("getsentry-incinerator-0"),
161            "getsentry-incinerator"
162        );
163        assert_eq!(strip_pod_suffix("web-12"), "web");
164    }
165}