use opentelemetry::{
global,
trace::{Tracer, TracerProvider},
KeyValue,
};
use std::collections::HashMap;
use std::time::Duration;
use crate::error::{Error, Result};
use fusabi_host::Value;
#[derive(Debug, Clone)]
pub struct ObservabilityConfig {
pub service_name: String,
pub service_version: String,
pub resource_attributes: HashMap<String, String>,
pub tracing_enabled: bool,
pub metrics_enabled: bool,
}
impl Default for ObservabilityConfig {
fn default() -> Self {
Self {
service_name: "fusabi-app".to_string(),
service_version: "0.1.0".to_string(),
resource_attributes: HashMap::new(),
tracing_enabled: true,
metrics_enabled: true,
}
}
}
impl ObservabilityConfig {
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
..Default::default()
}
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.service_version = version.into();
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.resource_attributes.insert(key.into(), value.into());
self
}
pub fn with_tracing(mut self, enabled: bool) -> Self {
self.tracing_enabled = enabled;
self
}
pub fn with_metrics(mut self, enabled: bool) -> Self {
self.metrics_enabled = enabled;
self
}
}
#[derive(Debug, Clone)]
pub struct SpanContext {
pub trace_id: String,
pub span_id: String,
pub name: String,
pub start_time_ns: u64,
pub attributes: HashMap<String, Value>,
}
impl SpanContext {
pub fn new(name: impl Into<String>) -> Self {
use std::time::SystemTime;
let start_time_ns = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
Self {
trace_id: generate_id(16),
span_id: generate_id(8),
name: name.into(),
start_time_ns,
attributes: HashMap::new(),
}
}
pub fn with_attribute(mut self, key: impl Into<String>, value: Value) -> Self {
self.attributes.insert(key.into(), value);
self
}
pub fn elapsed(&self) -> Duration {
use std::time::SystemTime;
let now_ns = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
Duration::from_nanos(now_ns.saturating_sub(self.start_time_ns))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub level: LogLevel,
pub message: String,
pub fields: HashMap<String, Value>,
pub timestamp_ns: u64,
}
impl LogEntry {
pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
use std::time::SystemTime;
let timestamp_ns = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
Self {
level,
message: message.into(),
fields: HashMap::new(),
timestamp_ns,
}
}
pub fn with_field(mut self, key: impl Into<String>, value: Value) -> Self {
self.fields.insert(key.into(), value);
self
}
}
fn generate_id(bytes: usize) -> String {
use std::time::SystemTime;
let seed = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
let mut result = String::with_capacity(bytes * 2);
let mut state = seed;
for _ in 0..bytes {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
result.push_str(&format!("{:02x}", (state >> 56) as u8));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = ObservabilityConfig::new("test-service")
.with_version("1.0.0")
.with_attribute("env", "test")
.with_tracing(true)
.with_metrics(false);
assert_eq!(config.service_name, "test-service");
assert_eq!(config.service_version, "1.0.0");
assert!(config.tracing_enabled);
assert!(!config.metrics_enabled);
}
#[test]
fn test_span_context() {
let span = SpanContext::new("test-span")
.with_attribute("key", Value::String("value".into()));
assert_eq!(span.name, "test-span");
assert!(!span.trace_id.is_empty());
assert!(!span.span_id.is_empty());
}
#[test]
fn test_log_entry() {
let entry = LogEntry::new(LogLevel::Info, "test message")
.with_field("count", Value::Int(42));
assert_eq!(entry.level, LogLevel::Info);
assert_eq!(entry.message, "test message");
}
#[test]
fn test_generate_id() {
let id = generate_id(8);
assert_eq!(id.len(), 16); }
}