heartbit_core/agent/
observability.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum ObservabilityMode {
12 #[default]
14 Production,
15 Analysis,
17 Debug,
19}
20
21impl ObservabilityMode {
22 pub fn from_str_loose(s: &str) -> Option<Self> {
24 match s.to_ascii_lowercase().as_str() {
25 "production" | "prod" => Some(Self::Production),
26 "analysis" | "analyze" => Some(Self::Analysis),
27 "debug" | "dbg" => Some(Self::Debug),
28 _ => None,
29 }
30 }
31
32 pub fn resolve(
36 env_key: &str,
37 config_str: Option<&str>,
38 builder_value: Option<ObservabilityMode>,
39 ) -> Self {
40 if let Ok(val) = std::env::var(env_key) {
42 if let Some(mode) = Self::from_str_loose(&val) {
43 return mode;
44 }
45 tracing::warn!(
46 env = env_key,
47 value = %val,
48 "unknown observability mode, falling back"
49 );
50 }
51 if let Some(s) = config_str {
53 if let Some(mode) = Self::from_str_loose(s) {
54 return mode;
55 }
56 tracing::warn!(
57 value = %s,
58 "unknown observability mode in config, falling back"
59 );
60 }
61 if let Some(mode) = builder_value {
63 return mode;
64 }
65 Self::default()
67 }
68
69 pub fn includes_metrics(self) -> bool {
71 matches!(self, Self::Analysis | Self::Debug)
72 }
73
74 pub fn includes_payloads(self) -> bool {
76 matches!(self, Self::Debug)
77 }
78}
79
80impl std::fmt::Display for ObservabilityMode {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 Self::Production => write!(f, "production"),
84 Self::Analysis => write!(f, "analysis"),
85 Self::Debug => write!(f, "debug"),
86 }
87 }
88}
89
90pub const OBSERVABILITY_ENV_KEY: &str = "HEARTBIT_OBSERVABILITY";
92
93pub const GEN_AI_SYSTEM: &str = "gen_ai.system";
99pub const GEN_AI_REQUEST_MODEL: &str = "gen_ai.request.model";
101pub const GEN_AI_RESPONSE_MODEL: &str = "gen_ai.response.model";
103pub const GEN_AI_RESPONSE_FINISH_REASON: &str = "gen_ai.response.finish_reasons";
105pub const GEN_AI_USAGE_INPUT_TOKENS: &str = "gen_ai.usage.input_tokens";
107pub const GEN_AI_USAGE_OUTPUT_TOKENS: &str = "gen_ai.usage.output_tokens";
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn mode_default_is_production() {
116 assert_eq!(ObservabilityMode::default(), ObservabilityMode::Production);
117 }
118
119 #[test]
120 fn from_str_loose_all_variants() {
121 assert_eq!(
122 ObservabilityMode::from_str_loose("production"),
123 Some(ObservabilityMode::Production)
124 );
125 assert_eq!(
126 ObservabilityMode::from_str_loose("prod"),
127 Some(ObservabilityMode::Production)
128 );
129 assert_eq!(
130 ObservabilityMode::from_str_loose("PRODUCTION"),
131 Some(ObservabilityMode::Production)
132 );
133 assert_eq!(
134 ObservabilityMode::from_str_loose("analysis"),
135 Some(ObservabilityMode::Analysis)
136 );
137 assert_eq!(
138 ObservabilityMode::from_str_loose("analyze"),
139 Some(ObservabilityMode::Analysis)
140 );
141 assert_eq!(
142 ObservabilityMode::from_str_loose("ANALYSIS"),
143 Some(ObservabilityMode::Analysis)
144 );
145 assert_eq!(
146 ObservabilityMode::from_str_loose("debug"),
147 Some(ObservabilityMode::Debug)
148 );
149 assert_eq!(
150 ObservabilityMode::from_str_loose("dbg"),
151 Some(ObservabilityMode::Debug)
152 );
153 assert_eq!(
154 ObservabilityMode::from_str_loose("DEBUG"),
155 Some(ObservabilityMode::Debug)
156 );
157 }
158
159 #[test]
160 fn from_str_loose_unknown_returns_none() {
161 assert_eq!(ObservabilityMode::from_str_loose("banana"), None);
162 assert_eq!(ObservabilityMode::from_str_loose(""), None);
163 }
164
165 #[test]
166 fn resolve_env_overrides_config() {
167 let key = "HEARTBIT_OBSERVABILITY_TEST_1";
169 unsafe {
171 std::env::set_var(key, "debug");
172 }
173 let mode =
174 ObservabilityMode::resolve(key, Some("production"), Some(ObservabilityMode::Analysis));
175 assert_eq!(mode, ObservabilityMode::Debug);
176 unsafe {
178 std::env::remove_var(key);
179 }
180 }
181
182 #[test]
183 fn resolve_config_overrides_builder() {
184 let key = "HEARTBIT_OBSERVABILITY_TEST_2";
185 unsafe {
187 std::env::remove_var(key);
188 }
189 let mode =
190 ObservabilityMode::resolve(key, Some("analysis"), Some(ObservabilityMode::Production));
191 assert_eq!(mode, ObservabilityMode::Analysis);
192 }
193
194 #[test]
195 fn resolve_default_fallback() {
196 let key = "HEARTBIT_OBSERVABILITY_TEST_3";
197 unsafe {
199 std::env::remove_var(key);
200 }
201 let mode = ObservabilityMode::resolve(key, None, None);
202 assert_eq!(mode, ObservabilityMode::Production);
203 }
204
205 #[test]
206 fn includes_metrics_analysis_and_debug() {
207 assert!(!ObservabilityMode::Production.includes_metrics());
208 assert!(ObservabilityMode::Analysis.includes_metrics());
209 assert!(ObservabilityMode::Debug.includes_metrics());
210 }
211
212 #[test]
213 fn includes_payloads_debug_only() {
214 assert!(!ObservabilityMode::Production.includes_payloads());
215 assert!(!ObservabilityMode::Analysis.includes_payloads());
216 assert!(ObservabilityMode::Debug.includes_payloads());
217 }
218
219 #[test]
220 fn display_impl() {
221 assert_eq!(ObservabilityMode::Production.to_string(), "production");
222 assert_eq!(ObservabilityMode::Analysis.to_string(), "analysis");
223 assert_eq!(ObservabilityMode::Debug.to_string(), "debug");
224 }
225
226 #[test]
229 fn gen_ai_constants_match_spec() {
230 assert_eq!(GEN_AI_SYSTEM, "gen_ai.system");
232 assert_eq!(GEN_AI_REQUEST_MODEL, "gen_ai.request.model");
233 assert_eq!(GEN_AI_RESPONSE_MODEL, "gen_ai.response.model");
234 assert_eq!(
235 GEN_AI_RESPONSE_FINISH_REASON,
236 "gen_ai.response.finish_reasons"
237 );
238 assert_eq!(GEN_AI_USAGE_INPUT_TOKENS, "gen_ai.usage.input_tokens");
239 assert_eq!(GEN_AI_USAGE_OUTPUT_TOKENS, "gen_ai.usage.output_tokens");
240 }
241}