objectstore_server/extractors/
downstream_service.rs1use axum::extract::FromRequestParts;
4use axum::http::request::Parts;
5
6const HEADER_SERVICE: &str = "x-downstream-service";
9
10#[derive(Debug, Clone)]
16pub struct DownstreamService(pub Option<String>);
17
18impl DownstreamService {
19 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
31fn 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 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 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 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 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 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}