use core::fmt;
use std::collections::BTreeMap;
use std::net::SocketAddr;
use serde::Deserialize;
use shep_client::dogs::dog_config;
use shep_core::values::UpDuration;
#[dog_config]
#[derive(Clone, PartialEq, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields, default)]
pub struct MetricsConfig {
pub bind: SocketAddr,
pub otlp_endpoint: Option<String>,
pub otlp_interval: UpDuration,
pub otlp_timeout: UpDuration,
#[shep(secret)]
pub otlp_headers: BTreeMap<String, String>,
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
bind: SocketAddr::from(([127, 0, 0, 1], 9615)),
otlp_endpoint: None,
otlp_interval: UpDuration::from_millis(60_000),
otlp_timeout: UpDuration::from_millis(10_000),
otlp_headers: BTreeMap::new(),
}
}
}
impl fmt::Debug for MetricsConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"MetricsConfig {{ bind: {:?}, otlp_endpoint: {:?}, otlp_interval: {:?}, otlp_timeout: {:?}, otlp_headers: <redacted> }}",
self.bind, self.otlp_endpoint, self.otlp_interval, self.otlp_timeout,
)
}
}
#[cfg(test)]
impl MetricsConfig {
pub(super) fn default_on_port(port: u16) -> Self {
Self {
bind: SocketAddr::from(([127, 0, 0, 1], port)),
..Self::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_metrics_schema_offers_the_port_it_would_bind_and_refuses_unknown_keys() {
let schema = shep_client::dogs::config_schema::<MetricsConfig>();
let schema = schema.as_value();
assert_eq!(
schema.get("additionalProperties"),
Some(&serde_json::Value::Bool(false)),
"`deny_unknown_fields` is what catches a misspelled key"
);
assert_eq!(
schema.get("required"),
None,
"`default` on the type makes every key optional"
);
assert_eq!(
schema.pointer("/properties/bind/default"),
Some(&serde_json::Value::String("127.0.0.1:9615".to_owned())),
"the default is loopback, and a pane shows the port rather than a blank"
);
assert_eq!(
schema.pointer("/properties/otlp_endpoint/default"),
Some(&serde_json::Value::Null),
"no export is the default, and a pane shows an empty key rather than a guess"
);
assert_eq!(
schema.pointer("/properties/otlp_interval/default"),
Some(&serde_json::Value::String("1m".to_owned())),
"the interval a collector scraping /metrics already assumes"
);
assert_eq!(
schema.pointer("/properties/otlp_timeout/default"),
Some(&serde_json::Value::String("10s".to_owned())),
"the same per-delivery budget bark gives a sink"
);
assert_eq!(
schema.pointer("/properties/otlp_headers/default"),
Some(&serde_json::Value::Object(serde_json::Map::new())),
"an unconfigured exporter sends no extra header"
);
assert_eq!(
schema.pointer("/properties/otlp_headers/x-shep-secret"),
Some(&serde_json::Value::Bool(true)),
"an OTLP collector's credential rides in a header value"
);
}
#[test]
fn the_default_bind_is_loopback() {
assert_eq!(
MetricsConfig::default().bind,
"127.0.0.1:9615".parse::<SocketAddr>().unwrap()
);
let parsed: MetricsConfig = toml::from_str("").unwrap();
assert_eq!(parsed, MetricsConfig::default());
}
#[test]
fn metrics_config_debug_redacts_the_headers() {
let config = MetricsConfig {
otlp_endpoint: Some("https://otlp.internal:4318".to_owned()),
otlp_headers: BTreeMap::from([(
"Authorization".to_owned(),
"Bearer s3cr3t-token".to_owned(),
)]),
..MetricsConfig::default()
};
let rendered = format!("{config:?}");
assert_eq!(
rendered,
r#"MetricsConfig { bind: 127.0.0.1:9615, otlp_endpoint: Some("https://otlp.internal:4318"), otlp_interval: UpDuration(60s), otlp_timeout: UpDuration(10s), otlp_headers: <redacted> }"#
);
assert!(!rendered.contains("s3cr3t-token"), "{rendered}");
assert!(!rendered.contains("Authorization"), "{rendered}");
}
#[test]
fn the_documented_metrics_config_parses_from_toml() {
let toml_str = r#"
bind = "127.0.0.1:9615"
otlp_endpoint = "http://127.0.0.1:4318"
otlp_interval = "30s"
otlp_timeout = "5s"
[otlp_headers]
Authorization = "Bearer token-from-a-secret-manager"
"#;
let config: MetricsConfig =
toml::from_str(toml_str).expect("the documented [dog.metrics] example must parse");
assert_eq!(
config.otlp_endpoint.as_deref(),
Some("http://127.0.0.1:4318")
);
assert_eq!(config.otlp_interval.as_millis(), 30_000);
assert_eq!(config.otlp_timeout.as_millis(), 5_000);
assert_eq!(config.otlp_headers.len(), 1);
}
}