#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ObservabilityMode {
#[default]
Production,
Analysis,
Debug,
}
impl ObservabilityMode {
pub fn from_str_loose(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"production" | "prod" => Some(Self::Production),
"analysis" | "analyze" => Some(Self::Analysis),
"debug" | "dbg" => Some(Self::Debug),
_ => None,
}
}
pub fn resolve(
env_key: &str,
config_str: Option<&str>,
builder_value: Option<ObservabilityMode>,
) -> Self {
if let Ok(val) = std::env::var(env_key) {
if let Some(mode) = Self::from_str_loose(&val) {
return mode;
}
tracing::warn!(
env = env_key,
value = %val,
"unknown observability mode, falling back"
);
}
if let Some(s) = config_str {
if let Some(mode) = Self::from_str_loose(s) {
return mode;
}
tracing::warn!(
value = %s,
"unknown observability mode in config, falling back"
);
}
if let Some(mode) = builder_value {
return mode;
}
Self::default()
}
pub fn includes_metrics(self) -> bool {
matches!(self, Self::Analysis | Self::Debug)
}
pub fn includes_payloads(self) -> bool {
matches!(self, Self::Debug)
}
}
impl std::fmt::Display for ObservabilityMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Production => write!(f, "production"),
Self::Analysis => write!(f, "analysis"),
Self::Debug => write!(f, "debug"),
}
}
}
pub const OBSERVABILITY_ENV_KEY: &str = "HEARTBIT_OBSERVABILITY";
pub const GEN_AI_SYSTEM: &str = "gen_ai.system";
pub const GEN_AI_REQUEST_MODEL: &str = "gen_ai.request.model";
pub const GEN_AI_RESPONSE_MODEL: &str = "gen_ai.response.model";
pub const GEN_AI_RESPONSE_FINISH_REASON: &str = "gen_ai.response.finish_reasons";
pub const GEN_AI_USAGE_INPUT_TOKENS: &str = "gen_ai.usage.input_tokens";
pub const GEN_AI_USAGE_OUTPUT_TOKENS: &str = "gen_ai.usage.output_tokens";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mode_default_is_production() {
assert_eq!(ObservabilityMode::default(), ObservabilityMode::Production);
}
#[test]
fn from_str_loose_all_variants() {
assert_eq!(
ObservabilityMode::from_str_loose("production"),
Some(ObservabilityMode::Production)
);
assert_eq!(
ObservabilityMode::from_str_loose("prod"),
Some(ObservabilityMode::Production)
);
assert_eq!(
ObservabilityMode::from_str_loose("PRODUCTION"),
Some(ObservabilityMode::Production)
);
assert_eq!(
ObservabilityMode::from_str_loose("analysis"),
Some(ObservabilityMode::Analysis)
);
assert_eq!(
ObservabilityMode::from_str_loose("analyze"),
Some(ObservabilityMode::Analysis)
);
assert_eq!(
ObservabilityMode::from_str_loose("ANALYSIS"),
Some(ObservabilityMode::Analysis)
);
assert_eq!(
ObservabilityMode::from_str_loose("debug"),
Some(ObservabilityMode::Debug)
);
assert_eq!(
ObservabilityMode::from_str_loose("dbg"),
Some(ObservabilityMode::Debug)
);
assert_eq!(
ObservabilityMode::from_str_loose("DEBUG"),
Some(ObservabilityMode::Debug)
);
}
#[test]
fn from_str_loose_unknown_returns_none() {
assert_eq!(ObservabilityMode::from_str_loose("banana"), None);
assert_eq!(ObservabilityMode::from_str_loose(""), None);
}
#[test]
fn resolve_env_overrides_config() {
let key = "HEARTBIT_OBSERVABILITY_TEST_1";
unsafe {
std::env::set_var(key, "debug");
}
let mode =
ObservabilityMode::resolve(key, Some("production"), Some(ObservabilityMode::Analysis));
assert_eq!(mode, ObservabilityMode::Debug);
unsafe {
std::env::remove_var(key);
}
}
#[test]
fn resolve_config_overrides_builder() {
let key = "HEARTBIT_OBSERVABILITY_TEST_2";
unsafe {
std::env::remove_var(key);
}
let mode =
ObservabilityMode::resolve(key, Some("analysis"), Some(ObservabilityMode::Production));
assert_eq!(mode, ObservabilityMode::Analysis);
}
#[test]
fn resolve_default_fallback() {
let key = "HEARTBIT_OBSERVABILITY_TEST_3";
unsafe {
std::env::remove_var(key);
}
let mode = ObservabilityMode::resolve(key, None, None);
assert_eq!(mode, ObservabilityMode::Production);
}
#[test]
fn includes_metrics_analysis_and_debug() {
assert!(!ObservabilityMode::Production.includes_metrics());
assert!(ObservabilityMode::Analysis.includes_metrics());
assert!(ObservabilityMode::Debug.includes_metrics());
}
#[test]
fn includes_payloads_debug_only() {
assert!(!ObservabilityMode::Production.includes_payloads());
assert!(!ObservabilityMode::Analysis.includes_payloads());
assert!(ObservabilityMode::Debug.includes_payloads());
}
#[test]
fn display_impl() {
assert_eq!(ObservabilityMode::Production.to_string(), "production");
assert_eq!(ObservabilityMode::Analysis.to_string(), "analysis");
assert_eq!(ObservabilityMode::Debug.to_string(), "debug");
}
#[test]
fn gen_ai_constants_match_spec() {
assert_eq!(GEN_AI_SYSTEM, "gen_ai.system");
assert_eq!(GEN_AI_REQUEST_MODEL, "gen_ai.request.model");
assert_eq!(GEN_AI_RESPONSE_MODEL, "gen_ai.response.model");
assert_eq!(
GEN_AI_RESPONSE_FINISH_REASON,
"gen_ai.response.finish_reasons"
);
assert_eq!(GEN_AI_USAGE_INPUT_TOKENS, "gen_ai.usage.input_tokens");
assert_eq!(GEN_AI_USAGE_OUTPUT_TOKENS, "gen_ai.usage.output_tokens");
}
}