rainier_framework/
observability.rs1use std::sync::Arc;
23
24use rainier_config::Config;
25use rainier_metrics::Metrics;
26use rainier_openapi::{OpenApi, Rendered};
27use rainier_routing::CompiledRouter;
28use rainier_support::Result;
29use rainier_telemetry::{LogFormat, Trace};
30
31use crate::keys;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct MetricsSettings {
36 pub enabled: bool,
38 pub path: String,
40}
41
42impl MetricsSettings {
43 pub fn from_config(config: &Config) -> Self {
45 Self {
46 enabled: config.get(keys::METRICS_ENABLED).unwrap_or(false),
47 path: config.get(keys::METRICS_PATH).unwrap_or_else(|| "/metrics".to_string()),
48 }
49 }
50
51 pub fn registry(&self) -> Option<Arc<Metrics>> {
57 self.enabled.then(|| Arc::new(Metrics::new()))
58 }
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub struct TelemetrySettings {
64 pub enabled: bool,
66 pub endpoint: Option<String>,
68 pub service_name: String,
70 pub sample_ratio: f64,
72 pub log_format: LogFormat,
74}
75
76impl TelemetrySettings {
77 pub fn from_config(config: &Config) -> Self {
79 Self {
80 enabled: config.get(keys::TELEMETRY_ENABLED).unwrap_or(false),
81 endpoint: config.get(keys::TELEMETRY_ENDPOINT),
82 service_name: config
83 .get(keys::TELEMETRY_SERVICE_NAME)
84 .or_else(|| config.get(keys::APP_NAME))
85 .unwrap_or_else(|| "rainier".to_string()),
86 sample_ratio: config.get(keys::TELEMETRY_SAMPLE_RATIO).unwrap_or(1.0),
87 log_format: config.setting(keys::LOG_FORMAT).unwrap_or_default(),
88 }
89 }
90
91 pub fn middleware(&self) -> Option<Trace> {
93 self.enabled.then(|| Trace::new().sampling(self.sample_ratio > 0.0))
94 }
95
96 pub fn exports(&self) -> bool {
102 self.enabled && self.endpoint.is_some()
103 }
104}
105
106impl TelemetrySettings {
107 pub fn install_logging(&self, environment: &str, filter: &str) -> bool {
117 use tracing_subscriber::{fmt, EnvFilter};
118
119 let filter =
120 EnvFilter::try_new(filter).unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_FILTER));
121 let builder = fmt().with_env_filter(filter);
122
123 match self.log_format.resolve(environment) {
124 LogFormat::Json => {
128 builder.json().flatten_event(true).with_current_span(true).try_init().is_ok()
129 }
130 LogFormat::Compact => builder.compact().try_init().is_ok(),
131 LogFormat::Pretty | LogFormat::Auto => builder.try_init().is_ok(),
133 }
134 }
135}
136
137const DEFAULT_LOG_FILTER: &str = "info";
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct OpenApiSettings {
143 pub enabled: bool,
145 pub path: String,
147 pub title: String,
149 pub version: String,
151 pub server: Option<String>,
153}
154
155impl OpenApiSettings {
156 pub fn from_config(config: &Config) -> Self {
158 Self {
159 enabled: config.get(keys::OPENAPI_ENABLED).unwrap_or(false),
160 path: config.get(keys::OPENAPI_PATH).unwrap_or_else(|| "/openapi.json".to_string()),
161 title: config
162 .get(keys::OPENAPI_TITLE)
163 .or_else(|| config.get(keys::APP_NAME))
164 .unwrap_or_else(|| "API".to_string()),
165 version: config.get(keys::OPENAPI_VERSION).unwrap_or_else(|| "1.0.0".to_string()),
166 server: config.get(keys::OPENAPI_SERVER),
167 }
168 }
169
170 pub fn apply(&self, document: OpenApi) -> OpenApi {
176 let document = document.titled(&self.title, &self.version);
177
178 match &self.server {
179 Some(url) => document.server(url),
180 None => document,
181 }
182 }
183
184 pub fn render(&self, document: OpenApi, router: &CompiledRouter) -> Option<Arc<Rendered>> {
186 self.enabled.then(|| Arc::new(Rendered::new(&self.apply(document), router)))
187 }
188}
189
190pub async fn metrics_endpoint(_request: rainier_routing::Req) -> rainier_http::Response {
196 let Ok(metrics) = rainier_container::facade_application().resolve::<Metrics>() else {
197 return rainier_http::Response::new(rainier_http::StatusCode::NOT_FOUND);
198 };
199
200 rainier_http::Response::ok(metrics.render())
201 .with_header("content-type", "text/plain; version=0.0.4; charset=utf-8")
202}
203
204pub async fn openapi_endpoint(_request: rainier_routing::Req) -> rainier_http::Response {
210 let Ok(document) = rainier_container::facade_application().resolve::<Rendered>() else {
211 return rainier_http::Response::new(rainier_http::StatusCode::NOT_FOUND);
212 };
213
214 rainier_http::Response::ok(document.json().to_string())
215 .with_header("content-type", "application/json; charset=utf-8")
216}
217
218pub fn settings(config: &Config) -> Result<(MetricsSettings, TelemetrySettings, OpenApiSettings)> {
220 Ok((
221 MetricsSettings::from_config(config),
222 TelemetrySettings::from_config(config),
223 OpenApiSettings::from_config(config),
224 ))
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 fn config() -> Config {
232 Config::new()
233 }
234
235 #[test]
236 fn everything_is_off_when_nothing_is_configured() {
237 let config = config();
239
240 assert!(!MetricsSettings::from_config(&config).enabled);
241 assert!(!TelemetrySettings::from_config(&config).enabled);
242 assert!(!OpenApiSettings::from_config(&config).enabled);
243 }
244
245 #[test]
246 fn metrics_off_means_no_registry_and_so_no_timer() {
247 let settings = MetricsSettings::from_config(&config());
248 assert!(settings.registry().is_none());
249
250 let config = config();
251 config.set(keys::METRICS_ENABLED, true).unwrap();
252 assert!(MetricsSettings::from_config(&config).registry().is_some());
253 }
254
255 #[test]
256 fn the_paths_have_defaults_worth_having() {
257 assert_eq!(MetricsSettings::from_config(&config()).path, "/metrics");
258 assert_eq!(OpenApiSettings::from_config(&config()).path, "/openapi.json");
259 }
260
261 #[test]
262 fn a_configured_path_wins() {
263 let config = config();
264 config.set(keys::METRICS_PATH, "/internal/metrics".to_string()).unwrap();
265
266 assert_eq!(MetricsSettings::from_config(&config).path, "/internal/metrics");
267 }
268
269 #[test]
270 fn tracing_can_propagate_without_exporting() {
271 let config = config();
274 config.set(keys::TELEMETRY_ENABLED, true).unwrap();
275
276 let settings = TelemetrySettings::from_config(&config);
277
278 assert!(settings.enabled);
279 assert!(!settings.exports(), "no endpoint, so nothing is sent");
280 assert!(settings.middleware().is_some());
281 }
282
283 #[test]
284 fn an_endpoint_turns_exporting_on() {
285 let config = config();
286 config.set(keys::TELEMETRY_ENABLED, true).unwrap();
287 config.set(keys::TELEMETRY_ENDPOINT, "http://localhost:4317".to_string()).unwrap();
288
289 assert!(TelemetrySettings::from_config(&config).exports());
290 }
291
292 #[test]
293 fn an_endpoint_alone_does_nothing_while_telemetry_is_off() {
294 let config = config();
297 config.set(keys::TELEMETRY_ENDPOINT, "http://localhost:4317".to_string()).unwrap();
298
299 let settings = TelemetrySettings::from_config(&config);
300 assert!(!settings.exports());
301 assert!(settings.middleware().is_none());
302 }
303
304 #[test]
305 fn the_service_name_falls_back_to_the_application_name() {
306 let config = config();
307 config.set(keys::APP_NAME, "Rainier Sample".to_string()).unwrap();
308
309 assert_eq!(TelemetrySettings::from_config(&config).service_name, "Rainier Sample");
310
311 config.set(keys::TELEMETRY_SERVICE_NAME, "api".to_string()).unwrap();
312 assert_eq!(TelemetrySettings::from_config(&config).service_name, "api");
313 }
314
315 #[test]
316 fn the_document_title_falls_back_to_the_application_name() {
317 let config = config();
318 config.set(keys::APP_NAME, "Rainier Sample".to_string()).unwrap();
319
320 assert_eq!(OpenApiSettings::from_config(&config).title, "Rainier Sample");
321 }
322
323 #[test]
324 fn the_log_format_defaults_to_auto_and_follows_the_environment() {
325 let config = Config::new();
326 assert_eq!(TelemetrySettings::from_config(&config).log_format, LogFormat::Auto);
327
328 assert_eq!(LogFormat::Auto.resolve("production"), LogFormat::Json);
331 }
332
333 #[test]
334 fn the_log_format_is_read_from_configuration() {
335 let config = Config::new();
336 config.set(keys::LOG_FORMAT.path(), "compact").unwrap();
337
338 assert_eq!(TelemetrySettings::from_config(&config).log_format, LogFormat::Compact);
339 }
340
341 #[test]
342 fn a_log_format_nobody_can_spell_falls_back_rather_than_silencing_the_logs() {
343 let config = Config::new();
348 config.set(keys::LOG_FORMAT.path(), "jsonn").unwrap();
349
350 assert_eq!(TelemetrySettings::from_config(&config).log_format, LogFormat::Auto);
351 }
352}