use std::sync::Arc;
use rainier_config::Config;
use rainier_metrics::Metrics;
use rainier_openapi::{OpenApi, Rendered};
use rainier_routing::CompiledRouter;
use rainier_support::Result;
use rainier_telemetry::{LogFormat, Trace};
use crate::keys;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetricsSettings {
pub enabled: bool,
pub path: String,
}
impl MetricsSettings {
pub fn from_config(config: &Config) -> Self {
Self {
enabled: config.get(keys::METRICS_ENABLED).unwrap_or(false),
path: config.get(keys::METRICS_PATH).unwrap_or_else(|| "/metrics".to_string()),
}
}
pub fn registry(&self) -> Option<Arc<Metrics>> {
self.enabled.then(|| Arc::new(Metrics::new()))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TelemetrySettings {
pub enabled: bool,
pub endpoint: Option<String>,
pub service_name: String,
pub sample_ratio: f64,
pub log_format: LogFormat,
}
impl TelemetrySettings {
pub fn from_config(config: &Config) -> Self {
Self {
enabled: config.get(keys::TELEMETRY_ENABLED).unwrap_or(false),
endpoint: config.get(keys::TELEMETRY_ENDPOINT),
service_name: config
.get(keys::TELEMETRY_SERVICE_NAME)
.or_else(|| config.get(keys::APP_NAME))
.unwrap_or_else(|| "rainier".to_string()),
sample_ratio: config.get(keys::TELEMETRY_SAMPLE_RATIO).unwrap_or(1.0),
log_format: config.setting(keys::LOG_FORMAT).unwrap_or_default(),
}
}
pub fn middleware(&self) -> Option<Trace> {
self.enabled.then(|| Trace::new().sampling(self.sample_ratio > 0.0))
}
pub fn exports(&self) -> bool {
self.enabled && self.endpoint.is_some()
}
}
impl TelemetrySettings {
pub fn install_logging(&self, environment: &str, filter: &str) -> bool {
use tracing_subscriber::{fmt, EnvFilter};
let filter =
EnvFilter::try_new(filter).unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_FILTER));
let builder = fmt().with_env_filter(filter);
match self.log_format.resolve(environment) {
LogFormat::Json => {
builder.json().flatten_event(true).with_current_span(true).try_init().is_ok()
}
LogFormat::Compact => builder.compact().try_init().is_ok(),
LogFormat::Pretty | LogFormat::Auto => builder.try_init().is_ok(),
}
}
}
const DEFAULT_LOG_FILTER: &str = "info";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenApiSettings {
pub enabled: bool,
pub path: String,
pub title: String,
pub version: String,
pub server: Option<String>,
}
impl OpenApiSettings {
pub fn from_config(config: &Config) -> Self {
Self {
enabled: config.get(keys::OPENAPI_ENABLED).unwrap_or(false),
path: config.get(keys::OPENAPI_PATH).unwrap_or_else(|| "/openapi.json".to_string()),
title: config
.get(keys::OPENAPI_TITLE)
.or_else(|| config.get(keys::APP_NAME))
.unwrap_or_else(|| "API".to_string()),
version: config.get(keys::OPENAPI_VERSION).unwrap_or_else(|| "1.0.0".to_string()),
server: config.get(keys::OPENAPI_SERVER),
}
}
pub fn apply(&self, document: OpenApi) -> OpenApi {
let document = document.titled(&self.title, &self.version);
match &self.server {
Some(url) => document.server(url),
None => document,
}
}
pub fn render(&self, document: OpenApi, router: &CompiledRouter) -> Option<Arc<Rendered>> {
self.enabled.then(|| Arc::new(Rendered::new(&self.apply(document), router)))
}
}
pub async fn metrics_endpoint(_request: rainier_routing::Req) -> rainier_http::Response {
let Ok(metrics) = rainier_container::facade_application().resolve::<Metrics>() else {
return rainier_http::Response::new(rainier_http::StatusCode::NOT_FOUND);
};
rainier_http::Response::ok(metrics.render())
.with_header("content-type", "text/plain; version=0.0.4; charset=utf-8")
}
pub async fn openapi_endpoint(_request: rainier_routing::Req) -> rainier_http::Response {
let Ok(document) = rainier_container::facade_application().resolve::<Rendered>() else {
return rainier_http::Response::new(rainier_http::StatusCode::NOT_FOUND);
};
rainier_http::Response::ok(document.json().to_string())
.with_header("content-type", "application/json; charset=utf-8")
}
pub fn settings(config: &Config) -> Result<(MetricsSettings, TelemetrySettings, OpenApiSettings)> {
Ok((
MetricsSettings::from_config(config),
TelemetrySettings::from_config(config),
OpenApiSettings::from_config(config),
))
}
#[cfg(test)]
mod tests {
use super::*;
fn config() -> Config {
Config::new()
}
#[test]
fn everything_is_off_when_nothing_is_configured() {
let config = config();
assert!(!MetricsSettings::from_config(&config).enabled);
assert!(!TelemetrySettings::from_config(&config).enabled);
assert!(!OpenApiSettings::from_config(&config).enabled);
}
#[test]
fn metrics_off_means_no_registry_and_so_no_timer() {
let settings = MetricsSettings::from_config(&config());
assert!(settings.registry().is_none());
let config = config();
config.set(keys::METRICS_ENABLED, true).unwrap();
assert!(MetricsSettings::from_config(&config).registry().is_some());
}
#[test]
fn the_paths_have_defaults_worth_having() {
assert_eq!(MetricsSettings::from_config(&config()).path, "/metrics");
assert_eq!(OpenApiSettings::from_config(&config()).path, "/openapi.json");
}
#[test]
fn a_configured_path_wins() {
let config = config();
config.set(keys::METRICS_PATH, "/internal/metrics".to_string()).unwrap();
assert_eq!(MetricsSettings::from_config(&config).path, "/internal/metrics");
}
#[test]
fn tracing_can_propagate_without_exporting() {
let config = config();
config.set(keys::TELEMETRY_ENABLED, true).unwrap();
let settings = TelemetrySettings::from_config(&config);
assert!(settings.enabled);
assert!(!settings.exports(), "no endpoint, so nothing is sent");
assert!(settings.middleware().is_some());
}
#[test]
fn an_endpoint_turns_exporting_on() {
let config = config();
config.set(keys::TELEMETRY_ENABLED, true).unwrap();
config.set(keys::TELEMETRY_ENDPOINT, "http://localhost:4317".to_string()).unwrap();
assert!(TelemetrySettings::from_config(&config).exports());
}
#[test]
fn an_endpoint_alone_does_nothing_while_telemetry_is_off() {
let config = config();
config.set(keys::TELEMETRY_ENDPOINT, "http://localhost:4317".to_string()).unwrap();
let settings = TelemetrySettings::from_config(&config);
assert!(!settings.exports());
assert!(settings.middleware().is_none());
}
#[test]
fn the_service_name_falls_back_to_the_application_name() {
let config = config();
config.set(keys::APP_NAME, "Rainier Sample".to_string()).unwrap();
assert_eq!(TelemetrySettings::from_config(&config).service_name, "Rainier Sample");
config.set(keys::TELEMETRY_SERVICE_NAME, "api".to_string()).unwrap();
assert_eq!(TelemetrySettings::from_config(&config).service_name, "api");
}
#[test]
fn the_document_title_falls_back_to_the_application_name() {
let config = config();
config.set(keys::APP_NAME, "Rainier Sample".to_string()).unwrap();
assert_eq!(OpenApiSettings::from_config(&config).title, "Rainier Sample");
}
#[test]
fn the_log_format_defaults_to_auto_and_follows_the_environment() {
let config = Config::new();
assert_eq!(TelemetrySettings::from_config(&config).log_format, LogFormat::Auto);
assert_eq!(LogFormat::Auto.resolve("production"), LogFormat::Json);
}
#[test]
fn the_log_format_is_read_from_configuration() {
let config = Config::new();
config.set(keys::LOG_FORMAT.path(), "compact").unwrap();
assert_eq!(TelemetrySettings::from_config(&config).log_format, LogFormat::Compact);
}
#[test]
fn a_log_format_nobody_can_spell_falls_back_rather_than_silencing_the_logs() {
let config = Config::new();
config.set(keys::LOG_FORMAT.path(), "jsonn").unwrap();
assert_eq!(TelemetrySettings::from_config(&config).log_format, LogFormat::Auto);
}
}