use std::net::SocketAddr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ObservabilityConfig {
#[serde(default)]
pub promql: PromqlConfig,
#[serde(default)]
pub otlp: OtlpConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromqlConfig {
#[serde(default = "default_true")]
pub enabled: bool,
}
impl Default for PromqlConfig {
fn default() -> Self {
Self { enabled: true }
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OtlpConfig {
#[serde(default)]
pub receiver: OtlpReceiverConfig,
#[serde(default)]
pub export: OtlpExportConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OtlpReceiverConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_otlp_http_listen")]
pub http_listen: SocketAddr,
#[serde(default = "default_otlp_grpc_listen")]
pub grpc_listen: SocketAddr,
}
impl Default for OtlpReceiverConfig {
fn default() -> Self {
Self {
enabled: false,
http_listen: default_otlp_http_listen(),
grpc_listen: default_otlp_grpc_listen(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OtlpExportConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub endpoint: String,
#[serde(default = "default_metrics_interval")]
pub metrics_interval_secs: u64,
}
impl Default for OtlpExportConfig {
fn default() -> Self {
Self {
enabled: false,
endpoint: String::new(),
metrics_interval_secs: 15,
}
}
}
fn default_true() -> bool {
true
}
fn default_otlp_http_listen() -> SocketAddr {
SocketAddr::from(([0, 0, 0, 0], 4318))
}
pub fn validate_feature_availability(config: &ObservabilityConfig) -> Result<(), String> {
if config.promql.enabled {
#[cfg(not(feature = "promql"))]
return Err(
"observability.promql.enabled = true, but this binary was built without \
the `promql` feature. Rebuild with `--features promql` or \
`--features monitoring`, or set enabled = false."
.into(),
);
}
if config.otlp.receiver.enabled {
#[cfg(not(feature = "otel"))]
return Err(
"observability.otlp.receiver.enabled = true, but this binary was built without \
the `otel` feature. Rebuild with `--features otel` or \
`--features monitoring`, or set enabled = false."
.into(),
);
}
if config.otlp.export.enabled {
#[cfg(not(feature = "otel"))]
return Err(
"observability.otlp.export.enabled = true, but this binary was built without \
the `otel` feature. Rebuild with `--features otel` or \
`--features monitoring`, or set enabled = false."
.into(),
);
}
Ok(())
}
fn default_otlp_grpc_listen() -> SocketAddr {
SocketAddr::from(([0, 0, 0, 0], 4317))
}
fn default_metrics_interval() -> u64 {
15
}
pub fn apply_observability_env(config: &mut ObservabilityConfig) {
if let Ok(val) = std::env::var("NODEDB_PROMQL_ENABLED")
&& let Ok(b) = val.parse::<bool>()
{
tracing::info!(
env_var = "NODEDB_PROMQL_ENABLED",
value = b,
"override applied"
);
config.promql.enabled = b;
}
if let Ok(val) = std::env::var("NODEDB_OTLP_RECEIVER_ENABLED")
&& let Ok(b) = val.parse::<bool>()
{
tracing::info!(
env_var = "NODEDB_OTLP_RECEIVER_ENABLED",
value = b,
"override applied"
);
config.otlp.receiver.enabled = b;
}
if let Ok(val) = std::env::var("NODEDB_OTLP_HTTP_LISTEN")
&& let Ok(addr) = val.parse::<SocketAddr>()
{
tracing::info!(env_var = "NODEDB_OTLP_HTTP_LISTEN", value = %val, "override applied");
config.otlp.receiver.http_listen = addr;
}
if let Ok(val) = std::env::var("NODEDB_OTLP_GRPC_LISTEN")
&& let Ok(addr) = val.parse::<SocketAddr>()
{
tracing::info!(env_var = "NODEDB_OTLP_GRPC_LISTEN", value = %val, "override applied");
config.otlp.receiver.grpc_listen = addr;
}
if let Ok(val) = std::env::var("NODEDB_OTLP_EXPORT_ENABLED")
&& let Ok(b) = val.parse::<bool>()
{
tracing::info!(
env_var = "NODEDB_OTLP_EXPORT_ENABLED",
value = b,
"override applied"
);
config.otlp.export.enabled = b;
}
if let Ok(val) = std::env::var("NODEDB_OTLP_EXPORT_ENDPOINT") {
tracing::info!(env_var = "NODEDB_OTLP_EXPORT_ENDPOINT", value = %val, "override applied");
config.otlp.export.endpoint = val;
}
if let Ok(val) = std::env::var("NODEDB_OTLP_EXPORT_INTERVAL")
&& let Ok(secs) = val.parse::<u64>()
{
tracing::info!(
env_var = "NODEDB_OTLP_EXPORT_INTERVAL",
value = secs,
"override applied"
);
config.otlp.export.metrics_interval_secs = secs;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let cfg = ObservabilityConfig::default();
assert!(cfg.promql.enabled);
assert!(!cfg.otlp.receiver.enabled);
assert!(!cfg.otlp.export.enabled);
assert_eq!(cfg.otlp.receiver.http_listen.port(), 4318);
assert_eq!(cfg.otlp.receiver.grpc_listen.port(), 4317);
assert_eq!(cfg.otlp.export.metrics_interval_secs, 15);
}
#[test]
fn validate_default_config() {
let cfg = ObservabilityConfig::default();
let result = validate_feature_availability(&cfg);
#[cfg(feature = "promql")]
assert!(result.is_ok());
#[cfg(not(feature = "promql"))]
assert!(result.is_err());
}
#[test]
fn validate_disabled_always_passes() {
let mut cfg = ObservabilityConfig::default();
cfg.promql.enabled = false;
cfg.otlp.receiver.enabled = false;
cfg.otlp.export.enabled = false;
assert!(validate_feature_availability(&cfg).is_ok());
}
#[test]
fn toml_roundtrip() {
let cfg = ObservabilityConfig::default();
let toml_str = toml::to_string_pretty(&cfg).unwrap();
let _parsed: ObservabilityConfig = toml::from_str(&toml_str).unwrap();
}
}