1use std::fmt;
2use std::ops::ControlFlow;
3use std::time::{Duration, Instant};
4
5use http::header::{AUTHORIZATION, WWW_AUTHENTICATE};
6use http::request::Parts;
7use http::{HeaderValue, Method, StatusCode};
8use sofie_core::{Error, Result};
9use sofie_telemetry::metrics::{self, Instrument, Kind};
10
11use crate::answer::{self, Response, TEXT};
12use crate::trace::RouteLabel;
13
14pub(crate) const PATH: &str = "/metrics";
15const PROMETHEUS_TEXT: &str = "text/plain; version=0.0.4; charset=utf-8";
16const UNNAMED: &str = "unnamed";
18const UNMATCHED: &str = "unmatched";
19const OTHER_METHOD: &str = "_OTHER";
20const MIN_TOKEN_LENGTH: usize = 32;
21
22static REQUEST_DURATION: Instrument = Instrument {
23 name: "http.server.request.duration",
24 description: "How long a request took, by route name, method, status and area.",
25 kind: Kind::Duration,
26 labels: &[
27 "http.route",
28 "http.request.method",
29 "http.response.status_code",
30 "area",
31 ],
32};
33static HANDLER_DURATION: Instrument = Instrument {
34 name: "sofie.handler.duration",
35 description: "How long an action or a loader took, commit included, by handler and module.",
36 kind: Kind::Duration,
37 labels: &["code.function.name", "module"],
38};
39
40#[derive(Clone, Copy)]
41pub(crate) struct RequestMeasure {
42 route: &'static str,
43 method: &'static str,
44 area: &'static str,
45}
46
47impl RequestMeasure {
48 pub(crate) fn of(method: &Method, route: Option<RouteLabel<'_>>) -> Self {
49 Self {
50 route: route.map_or(UNMATCHED, |route| route.name.unwrap_or(UNNAMED)),
51 method: method_label(method),
52 area: route.and_then(|route| route.area).unwrap_or_default(),
53 }
54 }
55
56 pub(crate) fn record(self, status: u16, elapsed: Duration) {
57 let labels = [
58 self.route.into(),
59 self.method.into(),
60 status.into(),
61 self.area.into(),
62 ];
63 metrics::record_duration(&REQUEST_DURATION, &labels, elapsed);
64 }
65}
66
67fn method_label(method: &Method) -> &'static str {
69 match *method {
70 Method::GET => "GET",
71 Method::HEAD => "HEAD",
72 Method::POST => "POST",
73 Method::PUT => "PUT",
74 Method::PATCH => "PATCH",
75 Method::DELETE => "DELETE",
76 Method::OPTIONS => "OPTIONS",
77 _ => OTHER_METHOD,
78 }
79}
80
81pub(crate) async fn timed<T>(
82 (name, module): crate::trace::HandlerName,
83 work: impl Future<Output = T>,
84) -> T {
85 let started = Instant::now();
86 let output = work.await;
87 metrics::record_duration(
88 &HANDLER_DURATION,
89 &[name.into(), module.into()],
90 started.elapsed(),
91 );
92 output
93}
94
95impl crate::Serving {
96 #[must_use]
98 pub fn metrics_token(mut self, token: MetricsToken) -> Self {
99 self.metrics_token = Some(token);
100 self
101 }
102}
103
104pub struct MetricsToken(String);
106
107impl MetricsToken {
108 pub fn parse(text: &str) -> Result<Self> {
109 let text = text.trim();
110 if text.len() < MIN_TOKEN_LENGTH || !text.bytes().all(|byte| byte.is_ascii_graphic()) {
111 return Err(Error::internal_message(format!(
112 "a metrics token needs at least {MIN_TOKEN_LENGTH} visible ASCII characters"
113 )));
114 }
115 Ok(Self(text.to_owned()))
116 }
117
118 fn admits(&self, authorization: Option<&HeaderValue>) -> bool {
120 let Some(sent) = authorization.and_then(crate::caller::bearer_credentials) else {
121 return false;
122 };
123 let (sent, own) = (sent.as_bytes(), self.0.as_bytes());
124 sent.len() == own.len()
125 && sent
126 .iter()
127 .zip(own)
128 .fold(0, |difference, (sent, own)| difference | (sent ^ own))
129 == 0
130 }
131}
132
133impl fmt::Debug for MetricsToken {
134 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135 formatter.write_str("MetricsToken(..)")
136 }
137}
138
139pub(crate) async fn scrape(request: &Parts, token: Option<&MetricsToken>) -> ControlFlow<Response> {
140 if request.uri.path() != PATH
141 || (request.method != Method::GET && request.method != Method::HEAD)
142 {
143 return ControlFlow::Continue(());
144 }
145 let Some(token) = token else {
146 return ControlFlow::Break(answer::response(
147 StatusCode::NOT_FOUND,
148 Some(TEXT),
149 "Not found.",
150 ));
151 };
152 if !token.admits(request.headers.get(AUTHORIZATION)) {
153 tracing::warn!(
154 http.route = PATH,
155 "refused a scrape without the metrics token"
156 );
157 let mut refused = answer::response(StatusCode::UNAUTHORIZED, Some(TEXT), "Unauthorized.");
158 refused
159 .headers_mut()
160 .insert(WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"));
161 return ControlFlow::Break(refused);
162 }
163 ControlFlow::Break(match metrics::prometheus().await {
164 Some(text) => answer::response(StatusCode::OK, Some(PROMETHEUS_TEXT), text),
165 None => answer::response(
166 StatusCode::SERVICE_UNAVAILABLE,
167 Some(TEXT),
168 "unavailable: metrics",
169 ),
170 })
171}