rivetkit_core/
metrics_endpoint.rs1use std::{collections::HashMap, sync::LazyLock};
2
3use anyhow::{Context, Result};
4use parking_lot::Mutex;
5use rivet_metrics::prometheus::{
6 Encoder, IntGaugeVec, TextEncoder, register_int_gauge_vec_with_registry,
7};
8use subtle::ConstantTimeEq;
9
10const METRICS_ENABLED_ENV: &str = "RIVETKIT_METRICS_ENABLED";
11const METRICS_TOKEN_ENV: &str = "RIVETKIT_METRICS_TOKEN";
12
13static RIVETKIT_INFO: LazyLock<IntGaugeVec> = LazyLock::new(|| {
14 register_int_gauge_vec_with_registry!(
15 "rivetkit_info",
16 "Static RivetKit build information.",
17 &[
18 "runtime",
19 "version",
20 "type",
21 "envoy_version",
22 "envoy_kind",
23 "pool_name",
24 ],
25 *rivet_metrics::REGISTRY
26 )
27 .unwrap()
28});
29
30static CURRENT_RIVETKIT_INFO: LazyLock<Mutex<Option<RivetKitInfo>>> =
31 LazyLock::new(|| Mutex::new(None));
32
33#[derive(Clone, Debug, Eq, PartialEq)]
34struct RivetKitInfo {
35 runtime: String,
36 version: String,
37 runtime_type: String,
38 envoy_version: String,
39 envoy_kind: String,
40 pool_name: String,
41}
42
43impl RivetKitInfo {
44 fn labels(&self) -> [&str; 6] {
45 [
46 &self.runtime,
47 &self.version,
48 &self.runtime_type,
49 &self.envoy_version,
50 &self.envoy_kind,
51 &self.pool_name,
52 ]
53 }
54}
55
56pub struct RenderedMetrics {
57 pub content_type: String,
58 pub body: Vec<u8>,
59}
60
61pub enum MetricsAccessError {
62 NotEnabled,
63 Unauthorized,
64}
65
66pub fn runtime_type() -> &'static str {
67 if std::env::var("NODE_ENV").as_deref() == Ok("production") {
68 "deployed"
69 } else {
70 "local"
71 }
72}
73
74pub fn record_rivetkit_info(
75 version: impl Into<String>,
76 envoy_version: u32,
77 envoy_kind: impl Into<String>,
78 pool_name: impl Into<String>,
79) {
80 record_rivetkit_info_inner(RivetKitInfo {
81 runtime: "rivetkit".to_owned(),
82 version: version.into(),
83 runtime_type: runtime_type().to_owned(),
84 envoy_version: envoy_version.to_string(),
85 envoy_kind: envoy_kind.into(),
86 pool_name: pool_name.into(),
87 });
88}
89
90pub fn authorize_metrics_request(
91 bearer_token: Option<&str>,
92) -> std::result::Result<(), MetricsAccessError> {
93 let Some(configured_token) = configured_metrics_token() else {
94 return Err(MetricsAccessError::NotEnabled);
95 };
96
97 let Some(bearer_token) = bearer_token.filter(|token| !token.is_empty()) else {
98 return Err(MetricsAccessError::Unauthorized);
99 };
100
101 if bearer_token
102 .as_bytes()
103 .ct_eq(configured_token.as_bytes())
104 .into()
105 {
106 Ok(())
107 } else {
108 Err(MetricsAccessError::Unauthorized)
109 }
110}
111
112pub fn render_prometheus_metrics() -> Result<RenderedMetrics> {
113 ensure_rivetkit_info_recorded();
114
115 let encoder = TextEncoder::new();
116 let metric_families = rivet_metrics::REGISTRY.gather();
117 let mut body = Vec::new();
118 encoder
119 .encode(&metric_families, &mut body)
120 .context("encode prometheus metrics")?;
121
122 Ok(RenderedMetrics {
123 content_type: encoder.format_type().to_owned(),
124 body,
125 })
126}
127
128pub fn authorization_bearer_token(headers: &http::HeaderMap) -> Option<&str> {
129 headers
130 .get(http::header::AUTHORIZATION)
131 .and_then(|value| value.to_str().ok())
132 .and_then(bearer_token_from_authorization)
133}
134
135pub fn authorization_bearer_token_map(headers: &HashMap<String, String>) -> Option<&str> {
136 headers
137 .iter()
138 .find(|(name, _)| name.eq_ignore_ascii_case(http::header::AUTHORIZATION.as_str()))
139 .and_then(|(_, value)| bearer_token_from_authorization(value))
140}
141
142fn configured_metrics_token() -> Option<String> {
143 let enabled = std::env::var(METRICS_ENABLED_ENV).ok()?;
144 if enabled != "1" {
145 return None;
146 }
147
148 std::env::var(METRICS_TOKEN_ENV)
149 .ok()
150 .filter(|token| !token.is_empty())
151}
152
153fn ensure_rivetkit_info_recorded() {
154 if CURRENT_RIVETKIT_INFO.lock().is_some() {
155 return;
156 }
157
158 let envoy_version = std::env::var("RIVET_ENVOY_VERSION")
159 .ok()
160 .and_then(|value| value.parse().ok())
161 .unwrap_or(1);
162 let pool_name = std::env::var("RIVET_POOL_NAME").unwrap_or_else(|_| "rivetkit-rust".to_owned());
163 record_rivetkit_info(
164 env!("CARGO_PKG_VERSION"),
165 envoy_version,
166 "unknown",
167 pool_name,
168 );
169}
170
171fn record_rivetkit_info_inner(info: RivetKitInfo) {
172 let mut current = CURRENT_RIVETKIT_INFO.lock();
173 if let Some(previous) = current.as_ref()
174 && previous != &info
175 {
176 RIVETKIT_INFO.with_label_values(&previous.labels()).set(0);
177 }
178
179 RIVETKIT_INFO.with_label_values(&info.labels()).set(1);
180 *current = Some(info);
181}
182
183fn bearer_token_from_authorization(value: &str) -> Option<&str> {
184 let value = value.trim_start();
185 let scheme = value.get(..6)?;
186 if !scheme.eq_ignore_ascii_case("bearer") {
187 return None;
188 }
189
190 let rest = value.get(6..)?;
191 if !rest.chars().next().is_some_and(char::is_whitespace) {
192 return None;
193 }
194
195 let token = rest.trim_start();
196 if token.is_empty() { None } else { Some(token) }
197}