1use std::collections::HashMap;
17use std::sync::atomic::{AtomicUsize, Ordering};
18use std::sync::{Mutex, OnceLock};
19use std::time::Instant;
20
21pub(crate) const BOUNDS: [f64; 14] = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0];
23
24pub(crate) const SCHEMA: i32 = 3;
27
28pub(crate) const ZERO_THRESHOLD: f64 = 2.938735877055719e-39; const MIN_INDEX: i32 = -200;
33const MAX_INDEX: i32 = 160;
34
35const METHODS: [&str; 9] = ["CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"];
36const OTHER: &str = "_OTHER";
37
38#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
40pub(crate) struct Series {
41 method: u8,
43 pub(crate) status: i16,
44 pub(crate) https: bool,
45 version: u8,
47}
48
49impl Series {
50 pub(crate) fn new(method: &str, status: i16, https: bool, http_version: &str) -> Series {
51 let method = METHODS.iter().position(|m| *m == method).unwrap_or(METHODS.len()) as u8;
52 let version = match http_version {
53 "HTTP/1.0" => 0,
54 "HTTP/2" | "HTTP/2.0" => 2,
55 "HTTP/3" | "HTTP/3.0" => 3,
56 _ => 1,
57 };
58 Series { method, status, https, version }
59 }
60
61 fn key(&self) -> u32 {
63 (self.method as u32) << 24 | (self.status as u16 as u32) << 8 | (self.https as u32) << 4 | self.version as u32
64 }
65
66 pub(crate) fn method(&self) -> &'static str {
67 METHODS.get(self.method as usize).copied().unwrap_or(OTHER)
68 }
69
70 pub(crate) fn scheme(&self) -> &'static str {
71 if self.https { "https" } else { "http" }
72 }
73
74 pub(crate) fn protocol_version(&self) -> &'static str {
75 ["1.0", "1.1", "2", "3"][self.version as usize]
76 }
77}
78
79#[derive(Clone, Debug, Default, PartialEq)]
81pub(crate) struct Histogram {
82 pub(crate) count: u64,
83 pub(crate) sum: f64,
84 pub(crate) counts: [u64; BOUNDS.len() + 1],
87 pub(crate) zero_count: u64,
88 pub(crate) native_offset: i32,
90 pub(crate) native: Vec<u64>,
91}
92
93impl Histogram {
94 pub(crate) fn observe(&mut self, secs: f64) {
95 self.count += 1;
96 self.sum += secs;
97 let slot = BOUNDS.iter().position(|b| secs <= *b).unwrap_or(BOUNDS.len());
98 self.counts[slot] += 1;
99 if secs <= ZERO_THRESHOLD {
100 self.zero_count += 1;
101 return;
102 }
103 self.add_native(native_index(secs), 1);
104 }
105
106 fn add_native(&mut self, index: i32, n: u64) {
107 if self.native.is_empty() {
108 self.native_offset = index;
109 self.native.push(0);
110 } else if index < self.native_offset {
111 let grow = (self.native_offset - index) as usize;
112 self.native.splice(0..0, std::iter::repeat_n(0, grow));
113 self.native_offset = index;
114 } else if index >= self.native_offset + self.native.len() as i32 {
115 self.native.resize((index - self.native_offset + 1) as usize, 0);
116 }
117 self.native[(index - self.native_offset) as usize] += n;
118 }
119
120 fn merge(&mut self, other: &Histogram) {
121 self.count += other.count;
122 self.sum += other.sum;
123 for (mine, theirs) in self.counts.iter_mut().zip(other.counts.iter()) {
124 *mine += theirs;
125 }
126 self.zero_count += other.zero_count;
127 for (i, n) in other.native.iter().enumerate() {
128 if *n == 0 {
129 continue;
130 }
131 self.add_native(other.native_offset + i as i32, *n);
132 }
133 }
134
135 pub(crate) fn native_spans(&self) -> (Vec<(i32, u32)>, Vec<i64>) {
138 let mut spans: Vec<(i32, u32)> = Vec::new();
139 let mut deltas = Vec::new();
140 let mut previous_count = 0i64;
141 let mut last_index: Option<i32> = None;
142 for (i, n) in self.native.iter().enumerate() {
143 if *n == 0 {
144 continue;
145 }
146 let index = self.native_offset + i as i32;
147 match last_index {
148 Some(last) if index - last <= 3 => {
149 for _ in last + 1..index {
151 deltas.push(-previous_count);
152 previous_count = 0;
153 }
154 if let Some(span) = spans.last_mut() {
155 span.1 += (index - last) as u32;
156 }
157 }
158 Some(last) => spans.push((index - last - 1, 1)),
159 None => spans.push((index, 1)),
160 }
161 deltas.push(*n as i64 - previous_count);
162 previous_count = *n as i64;
163 last_index = Some(index);
164 }
165 (spans, deltas)
166 }
167}
168
169const MANTISSA_BOUNDS: [f64; 9] = [
172 1.0,
173 1.0905077326652577,
174 1.189207115002721,
175 1.2968395546510096,
176 1.4142135623730951,
177 1.5422108254079407,
178 1.681792830507429,
179 1.8340080864093424,
180 2.0,
181];
182
183pub(crate) fn native_index(secs: f64) -> i32 {
186 let bits = secs.to_bits();
187 let raw_exp = ((bits >> 52) & 0x7ff) as i32;
188 if raw_exp == 0 {
189 return MIN_INDEX; }
191 let exp = raw_exp - 1023;
193 let mantissa = f64::from_bits((bits & !(0x7ff << 52)) | (1023u64 << 52));
194 let within = MANTISSA_BOUNDS.iter().position(|bound| mantissa <= *bound).unwrap_or(8) as i32;
195 ((exp << SCHEMA) + within).clamp(MIN_INDEX, MAX_INDEX)
196}
197
198const SHARDS: usize = 16;
201
202impl std::hash::Hash for Series {
203 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
204 state.write_u32(self.key());
205 }
206}
207
208#[derive(Default)]
211struct SeriesHasher(u64);
212
213impl std::hash::Hasher for SeriesHasher {
214 fn write(&mut self, bytes: &[u8]) {
215 for b in bytes {
216 self.0 = (self.0 << 8 | *b as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
217 }
218 }
219 fn write_u32(&mut self, n: u32) {
220 self.0 = (n as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
221 }
222 fn finish(&self) -> u64 {
223 self.0
224 }
225}
226
227type Store = HashMap<Series, Histogram, std::hash::BuildHasherDefault<SeriesHasher>>;
228
229fn shards() -> &'static [Mutex<Store>] {
230 static STORE: OnceLock<Vec<Mutex<Store>>> = OnceLock::new();
231 STORE.get_or_init(|| (0..SHARDS).map(|_| Mutex::new(Store::default())).collect())
232}
233
234thread_local! {
235 static SHARD: usize = {
236 static NEXT: AtomicUsize = AtomicUsize::new(0);
237 NEXT.fetch_add(1, Ordering::Relaxed) % SHARDS
238 };
239}
240
241pub fn observe(method: &str, status: i16, https: bool, http_version: &str, started: Instant) {
243 let secs = started.elapsed().as_secs_f64();
244 record(Series::new(method, status, https, http_version), secs);
245}
246
247pub(crate) fn record(series: Series, secs: f64) {
248 let shard = SHARD.with(|s| *s);
249 let mut map = shards()[shard].lock().unwrap_or_else(|e| e.into_inner());
250 map.entry(series).or_default().observe(secs);
251}
252
253pub(crate) fn snapshot() -> Vec<(Series, Histogram)> {
255 let mut merged: HashMap<Series, Histogram> = HashMap::new();
256 for shard in shards() {
257 let map = shard.lock().unwrap_or_else(|e| e.into_inner());
258 for (series, histogram) in map.iter() {
259 merged.entry(*series).or_default().merge(histogram);
260 }
261 }
262 let mut all: Vec<_> = merged.into_iter().collect();
263 all.sort_by_key(|(s, _)| *s);
264 all
265}
266
267pub(crate) const NAME: &str = "http_server_request_duration_seconds";
268pub(crate) const HELP: &str = "Duration of HTTP server requests (OpenTelemetry http.server.request.duration)";
269
270pub(crate) fn labels(series: &Series) -> Vec<(String, String)> {
272 vec![
273 ("http_request_method".to_string(), series.method().to_string()),
274 ("http_response_status_code".to_string(), series.status.to_string()),
275 ("network_protocol_version".to_string(), series.protocol_version().to_string()),
276 ("url_scheme".to_string(), series.scheme().to_string()),
277 ]
278}
279
280pub(crate) fn prometheus_text() -> String {
282 let all = snapshot();
283 if all.is_empty() {
284 return String::new();
285 }
286 let mut out = format!("# HELP {NAME} {HELP}\n# TYPE {NAME} histogram\n");
287 for (series, histogram) in &all {
288 let labels: String = labels(series).iter().map(|(k, v)| format!("{k}=\"{v}\",")).collect();
289 let mut cumulative = 0;
290 for (i, bound) in BOUNDS.iter().enumerate() {
291 cumulative += histogram.counts[i];
292 out.push_str(&format!("{NAME}_bucket{{{labels}le=\"{bound}\"}} {cumulative}\n"));
293 }
294 out.push_str(&format!("{NAME}_bucket{{{labels}le=\"+Inf\"}} {}\n", histogram.count));
295 let labels = labels.trim_end_matches(',');
296 out.push_str(&format!("{NAME}_sum{{{labels}}} {}\n", histogram.sum));
297 out.push_str(&format!("{NAME}_count{{{labels}}} {}\n", histogram.count));
298 }
299 out
300}
301
302#[cfg(test)]
303mod tests;