use serde_json::json;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Quiet = 0,
Normal = 1,
Verbose = 2,
VeryVerbose = 3,
}
impl LogLevel {
pub fn to_min_severity(&self) -> Severity {
match self {
LogLevel::Quiet | LogLevel::Normal => Severity::Error,
LogLevel::Verbose => Severity::Info,
LogLevel::VeryVerbose => Severity::Debug,
}
}
pub fn to_llamacpp_verbosity(&self) -> i32 {
match self {
LogLevel::Quiet => 0,
LogLevel::Normal => 0, LogLevel::Verbose => 2, LogLevel::VeryVerbose => 4, }
}
pub fn from_u8(value: u8) -> Self {
match value {
0 => LogLevel::Quiet,
1 => LogLevel::Normal,
2 => LogLevel::Verbose,
_ => LogLevel::VeryVerbose,
}
}
}
static GLOBAL_LOG_LEVEL: AtomicU8 = AtomicU8::new(1);
pub fn set_global_log_level(level: LogLevel) {
GLOBAL_LOG_LEVEL.store(level as u8, Ordering::SeqCst);
#[cfg(feature = "llm-llamacpp")]
{
crate::runtime_adapter::llama_cpp::llama_log_set_verbosity(level.to_llamacpp_verbosity());
}
}
pub fn get_global_log_level() -> LogLevel {
LogLevel::from_u8(GLOBAL_LOG_LEVEL.load(Ordering::SeqCst))
}
pub fn should_log(severity: Severity) -> bool {
let level = get_global_log_level();
severity >= level.to_min_severity()
}
#[derive(Debug, Clone, PartialEq)]
pub enum TelemetryEvent {
StageStart,
StageComplete,
StageError,
PolicyEvaluation,
RoutingDecision,
ExecutionStart,
ExecutionComplete,
ExecutionError,
ControlSync,
}
impl TelemetryEvent {
pub fn as_str(&self) -> &'static str {
match self {
TelemetryEvent::StageStart => "stage_start",
TelemetryEvent::StageComplete => "stage_complete",
TelemetryEvent::StageError => "stage_error",
TelemetryEvent::PolicyEvaluation => "policy_evaluation",
TelemetryEvent::RoutingDecision => "routing_decision",
TelemetryEvent::ExecutionStart => "execution_start",
TelemetryEvent::ExecutionComplete => "execution_complete",
TelemetryEvent::ExecutionError => "execution_error",
TelemetryEvent::ControlSync => "control_sync",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
}
impl Severity {
pub fn as_str(&self) -> &'static str {
match self {
Severity::Debug => "DEBUG",
Severity::Info => "INFO",
Severity::Warn => "WARN",
Severity::Error => "ERROR",
}
}
pub fn from_level(level: u8) -> Self {
match level {
0 => Severity::Debug,
1 => Severity::Info,
2 => Severity::Warn,
_ => Severity::Error,
}
}
pub fn to_level(&self) -> u8 {
*self as u8
}
}
#[derive(Debug, Clone)]
pub struct TelemetryEntry {
pub timestamp: u64,
pub severity: Severity,
pub event: TelemetryEvent,
pub message: String,
pub attributes: serde_json::Value,
}
impl TelemetryEntry {
pub fn new(
severity: Severity,
event: TelemetryEvent,
message: String,
attributes: serde_json::Value,
) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
Self {
timestamp,
severity,
event,
message,
attributes,
}
}
pub fn to_json(&self) -> String {
let json_obj = json!({
"timestamp": self.timestamp,
"severity": self.severity.as_str(),
"event": self.event.as_str(),
"message": self.message,
"attributes": self.attributes
});
serde_json::to_string(&json_obj).unwrap_or_else(|_| "{}".to_string())
}
}
pub struct Telemetry {
enabled: bool,
min_severity: Severity,
}
impl Telemetry {
pub fn new() -> Self {
Self {
enabled: true,
min_severity: Severity::Info,
}
}
pub fn with_enabled(enabled: bool) -> Self {
Self {
enabled,
min_severity: Severity::Info,
}
}
pub fn with_min_severity(min_severity: Severity) -> Self {
Self {
enabled: true,
min_severity,
}
}
pub fn set_min_severity(&mut self, min_severity: Severity) {
self.min_severity = min_severity;
}
pub fn min_severity(&self) -> Severity {
self.min_severity
}
pub fn emit(&self, entry: TelemetryEntry) {
if !self.enabled {
return;
}
let global_min = get_global_log_level().to_min_severity();
let effective_min = if self.min_severity > global_min {
self.min_severity
} else {
global_min
};
if entry.severity >= effective_min {
println!("{}", entry.to_json());
}
}
pub fn log_stage_start(&self, stage_name: &str) {
let entry = TelemetryEntry::new(
Severity::Info,
TelemetryEvent::StageStart,
format!("Stage '{}' started", stage_name),
json!({
"stage": stage_name
}),
);
self.emit(entry);
}
pub fn log_stage_complete(
&self,
stage_name: &str,
target: &str,
latency_ms: u32,
additional_attrs: Option<serde_json::Value>,
) {
let mut attrs = json!({
"stage": stage_name,
"target": target,
"latency_ms": latency_ms
});
if let Some(extra) = additional_attrs {
if let Some(attrs_obj) = attrs.as_object_mut() {
if let Some(extra_obj) = extra.as_object() {
for (k, v) in extra_obj {
attrs_obj.insert(k.clone(), v.clone());
}
}
}
}
let entry = TelemetryEntry::new(
Severity::Info,
TelemetryEvent::StageComplete,
format!(
"Stage '{}' completed on {} in {}ms",
stage_name, target, latency_ms
),
attrs,
);
self.emit(entry);
}
pub fn log_stage_error(&self, stage_name: &str, error: &str) {
let entry = TelemetryEntry::new(
Severity::Error,
TelemetryEvent::StageError,
format!("Stage '{}' failed: {}", stage_name, error),
json!({
"stage": stage_name,
"error": error
}),
);
self.emit(entry);
}
pub fn log_policy_evaluation(&self, stage_name: &str, allowed: bool, reason: Option<&str>) {
let mut attrs = json!({
"stage": stage_name,
"allowed": allowed
});
if let Some(reason_str) = reason {
attrs
.as_object_mut()
.unwrap()
.insert("reason".to_string(), json!(reason_str));
}
let entry = TelemetryEntry::new(
Severity::Debug,
TelemetryEvent::PolicyEvaluation,
format!(
"Policy evaluation for '{}': {}",
stage_name,
if allowed { "allowed" } else { "denied" }
),
attrs,
);
self.emit(entry);
}
pub fn log_routing_decision(
&self,
stage_name: &str,
target: &str,
reason: &str,
recent_abort_rate: f32,
sample_size: u32,
) {
let safe_rate = if recent_abort_rate.is_finite() {
recent_abort_rate
} else {
0.0_f32
};
let entry = TelemetryEntry::new(
Severity::Info,
TelemetryEvent::RoutingDecision,
format!(
"Routing decision for '{}': {} ({})",
stage_name, target, reason
),
json!({
"stage": stage_name,
"target": target,
"reason": reason,
"local_reliability_hint": {
"recent_abort_rate": safe_rate,
"sample_size": sample_size,
}
}),
);
self.emit(entry);
}
pub fn log_execution_start(&self, stage_name: &str, target: &str) {
let entry = TelemetryEntry::new(
Severity::Debug,
TelemetryEvent::ExecutionStart,
format!("Execution started for '{}' on {}", stage_name, target),
json!({
"stage": stage_name,
"target": target
}),
);
self.emit(entry);
}
pub fn log_execution_complete(&self, stage_name: &str, target: &str, execution_time_ms: u32) {
let entry = TelemetryEntry::new(
Severity::Debug,
TelemetryEvent::ExecutionComplete,
format!(
"Execution completed for '{}' on {} in {}ms",
stage_name, target, execution_time_ms
),
json!({
"stage": stage_name,
"target": target,
"execution_time_ms": execution_time_ms
}),
);
self.emit(entry);
}
pub fn log_execution_error(&self, stage_name: &str, target: &str, error: &str) {
let entry = TelemetryEntry::new(
Severity::Error,
TelemetryEvent::ExecutionError,
format!(
"Execution failed for '{}' on {}: {}",
stage_name, target, error
),
json!({
"stage": stage_name,
"target": target,
"error": error
}),
);
self.emit(entry);
}
pub fn log_control_sync_event(
&self,
severity: Severity,
action: &str,
attributes: serde_json::Value,
) {
let mut attrs = json!({
"action": action
});
if let Some(attrs_obj) = attrs.as_object_mut() {
if let Some(extra) = attributes.as_object() {
for (key, value) in extra {
attrs_obj.insert(key.clone(), value.clone());
}
}
}
let entry = TelemetryEntry::new(
severity,
TelemetryEvent::ControlSync,
format!("control_sync {}", action),
attrs,
);
self.emit(entry);
}
pub fn log_bootstrap_start(&self) {
let entry = TelemetryEntry::new(
Severity::Info,
TelemetryEvent::StageStart,
"Bootstrap started".to_string(),
json!({
"bootstrap": true
}),
);
self.emit(entry);
}
pub fn log_bootstrap_complete(&self) {
let entry = TelemetryEntry::new(
Severity::Info,
TelemetryEvent::StageComplete,
"Bootstrap completed".to_string(),
json!({
"bootstrap": true
}),
);
self.emit(entry);
}
pub fn log_custom(
&self,
severity: Severity,
event: TelemetryEvent,
message: String,
attributes: serde_json::Value,
) {
let entry = TelemetryEntry::new(severity, event, message, attributes);
self.emit(entry);
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
impl Default for Telemetry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_telemetry_creation() {
let telemetry = Telemetry::new();
assert!(telemetry.is_enabled());
}
#[test]
fn test_telemetry_disabled() {
let telemetry = Telemetry::with_enabled(false);
assert!(!telemetry.is_enabled());
}
#[test]
fn test_log_stage_start() {
let telemetry = Telemetry::new();
telemetry.log_stage_start("test_stage");
}
#[test]
fn test_log_stage_complete() {
let telemetry = Telemetry::new();
telemetry.log_stage_complete("test_stage", "local", 100, None);
}
#[test]
fn test_log_policy_evaluation() {
let telemetry = Telemetry::new();
telemetry.log_policy_evaluation("test_stage", true, Some("policy passed"));
telemetry.log_policy_evaluation("test_stage", false, None);
}
#[test]
fn test_log_routing_decision() {
let telemetry = Telemetry::new();
telemetry.log_routing_decision("test_stage", "local", "high_latency", 0.0, 0);
}
#[test]
fn routing_decision_attributes_carry_local_reliability_hint() {
let attrs = json!({
"stage": "stage-1",
"target": "cloud",
"reason": "history_bias",
"local_reliability_hint": {
"recent_abort_rate": 0.75,
"sample_size": 4_u32,
}
});
let entry = TelemetryEntry::new(
Severity::Info,
TelemetryEvent::RoutingDecision,
String::from("Routing decision for 'stage-1': cloud (history_bias)"),
attrs,
);
let parsed: serde_json::Value =
serde_json::from_str(&entry.to_json()).expect("entry to_json must be valid JSON");
let attributes = parsed
.get("attributes")
.expect("entry must serialize attributes");
assert_eq!(
attributes["local_reliability_hint"]["recent_abort_rate"],
0.75
);
assert_eq!(attributes["local_reliability_hint"]["sample_size"], 4);
}
#[test]
fn test_telemetry_entry_json_format() {
let entry = TelemetryEntry::new(
Severity::Info,
TelemetryEvent::StageComplete,
"Test message".to_string(),
json!({"test": "value"}),
);
let json_str = entry.to_json();
assert!(json_str.contains("\"timestamp\""));
assert!(json_str.contains("\"severity\":\"INFO\""));
assert!(json_str.contains("\"event\":\"stage_complete\""));
assert!(json_str.contains("\"message\":\"Test message\""));
assert!(json_str.contains("\"attributes\""));
}
#[test]
fn test_telemetry_entry_timestamp() {
let entry1 = TelemetryEntry::new(
Severity::Info,
TelemetryEvent::StageStart,
"Test".to_string(),
json!({}),
);
std::thread::sleep(std::time::Duration::from_millis(1));
let entry2 = TelemetryEntry::new(
Severity::Info,
TelemetryEvent::StageStart,
"Test".to_string(),
json!({}),
);
assert!(entry2.timestamp >= entry1.timestamp);
}
#[test]
fn test_telemetry_disabled_no_output() {
let mut telemetry = Telemetry::new();
telemetry.set_enabled(false);
telemetry.log_stage_start("test");
}
}