hyperstack_interpreter/
canonical_log.rs1use serde::Serialize;
9use serde_json::{json, Value};
10use std::collections::HashMap;
11use std::time::Instant;
12
13#[cfg(feature = "otel")]
14use opentelemetry::trace::TraceContextExt;
15#[cfg(feature = "otel")]
16use tracing_opentelemetry::OpenTelemetrySpanExt;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum LogLevel {
20 Trace,
21 Debug,
22 #[default]
23 Info,
24 Warn,
25 Error,
26}
27
28pub struct CanonicalLog {
29 data: HashMap<String, Value>,
30 start: Instant,
31 level: LogLevel,
32 emitted: bool,
33}
34
35impl CanonicalLog {
36 pub fn new() -> Self {
37 Self {
38 data: HashMap::new(),
39 start: Instant::now(),
40 level: LogLevel::Info,
41 emitted: false,
42 }
43 }
44
45 pub fn set(&mut self, key: impl Into<String>, value: impl Serialize) -> &mut Self {
46 if let Ok(v) = serde_json::to_value(value) {
47 self.data.insert(key.into(), v);
48 }
49 self
50 }
51
52 pub fn set_level(&mut self, level: LogLevel) -> &mut Self {
53 self.level = level;
54 self
55 }
56
57 pub fn inc(&mut self, key: &str, amount: i64) -> &mut Self {
58 let current = self.data.get(key).and_then(|v| v.as_i64()).unwrap_or(0);
59 self.data.insert(key.to_string(), json!(current + amount));
60 self
61 }
62
63 pub fn duration_ms(&self) -> f64 {
64 self.start.elapsed().as_secs_f64() * 1000.0
65 }
66
67 pub fn suppress(&mut self) {
68 self.emitted = true;
69 }
70
71 pub fn emit(mut self) {
72 self.do_emit();
73 }
74
75 fn do_emit(&mut self) {
76 if self.emitted {
77 return;
78 }
79 self.emitted = true;
80
81 self.data
82 .insert("duration_ms".to_string(), json!(self.duration_ms()));
83
84 #[cfg(feature = "otel")]
85 {
86 let span = tracing::Span::current();
87 let context = span.context();
88 let span_ref = context.span();
89 let span_context = span_ref.span_context();
90 if span_context.is_valid() {
91 self.data.insert(
92 "trace_id".to_string(),
93 json!(format!("{:032x}", span_context.trace_id())),
94 );
95 self.data.insert(
96 "span_id".to_string(),
97 json!(format!("{:016x}", span_context.span_id())),
98 );
99 }
100 }
101
102 let canonical = serde_json::to_string(&self.data).unwrap_or_else(|_| "{}".to_string());
104
105 match self.level {
106 LogLevel::Trace => {
107 tracing::trace!(target: "hyperstack::canonical", canonical = %canonical, "canonical_event")
108 }
109 LogLevel::Debug => {
110 tracing::debug!(target: "hyperstack::canonical", canonical = %canonical, "canonical_event")
111 }
112 LogLevel::Info => {
113 tracing::info!(target: "hyperstack::canonical", canonical = %canonical, "canonical_event")
114 }
115 LogLevel::Warn => {
116 tracing::warn!(target: "hyperstack::canonical", canonical = %canonical, "canonical_event")
117 }
118 LogLevel::Error => {
119 tracing::error!(target: "hyperstack::canonical", canonical = %canonical, "canonical_event")
120 }
121 }
122 }
123}
124
125impl Default for CanonicalLog {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131impl Drop for CanonicalLog {
132 fn drop(&mut self) {
133 self.do_emit();
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_basic_usage() {
143 let mut log = CanonicalLog::new();
144 log.set("event_type", "BuyIxState")
145 .set("slot", 12345)
146 .set("mutations", 3);
147 log.suppress();
148 assert!(log.data.contains_key("event_type"));
149 }
150
151 #[test]
152 fn test_increment() {
153 let mut log = CanonicalLog::new();
154 log.inc("cache_hits", 1);
155 log.inc("cache_hits", 1);
156 log.inc("cache_hits", 1);
157 log.suppress();
158 assert_eq!(log.data.get("cache_hits"), Some(&json!(3)));
159 }
160}