mod setup;
pub use setup::{init_tracing, init_tracing_from_env, TelemetryBuilder, TelemetryGuard};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug)]
pub struct TracingConfig {
pub format: TracingFormat,
pub service_name: String,
pub level: String,
pub include_file_line: bool,
pub include_target: bool,
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
format: TracingFormat::Pretty,
service_name: "attuned".to_string(),
level: "info".to_string(),
include_file_line: false,
include_target: true,
}
}
}
#[derive(Clone, Debug, Default)]
pub enum TracingFormat {
#[default]
Pretty,
Json,
Compact,
}
#[derive(Clone, Debug)]
pub struct OtelConfig {
pub endpoint: String,
pub service_name: String,
pub service_version: String,
pub sample_rate: f64,
}
impl Default for OtelConfig {
fn default() -> Self {
Self {
endpoint: "http://localhost:4317".to_string(),
service_name: "attuned".to_string(),
service_version: env!("CARGO_PKG_VERSION").to_string(),
sample_rate: 1.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HealthState {
Healthy,
Degraded,
Unhealthy,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ComponentHealth {
pub name: String,
pub status: HealthState,
pub latency_ms: Option<u64>,
pub message: Option<String>,
}
impl ComponentHealth {
pub fn healthy(name: impl Into<String>) -> Self {
Self {
name: name.into(),
status: HealthState::Healthy,
latency_ms: None,
message: None,
}
}
pub fn healthy_with_latency(name: impl Into<String>, latency_ms: u64) -> Self {
Self {
name: name.into(),
status: HealthState::Healthy,
latency_ms: Some(latency_ms),
message: None,
}
}
pub fn unhealthy(name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
name: name.into(),
status: HealthState::Unhealthy,
latency_ms: None,
message: Some(message.into()),
}
}
pub fn degraded(name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
name: name.into(),
status: HealthState::Degraded,
latency_ms: None,
message: Some(message.into()),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HealthStatus {
pub status: HealthState,
pub version: String,
pub uptime_seconds: u64,
pub checks: Vec<ComponentHealth>,
}
impl HealthStatus {
pub fn from_checks(checks: Vec<ComponentHealth>, uptime_seconds: u64) -> Self {
let status = checks
.iter()
.map(|c| &c.status)
.fold(HealthState::Healthy, |acc, s| match (&acc, s) {
(HealthState::Unhealthy, _) | (_, HealthState::Unhealthy) => HealthState::Unhealthy,
(HealthState::Degraded, _) | (_, HealthState::Degraded) => HealthState::Degraded,
_ => HealthState::Healthy,
});
Self {
status,
version: env!("CARGO_PKG_VERSION").to_string(),
uptime_seconds,
checks,
}
}
}
#[async_trait::async_trait]
pub trait HealthCheck: Send + Sync {
async fn check(&self) -> ComponentHealth;
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditEventType {
StateCreated,
StateUpdated,
StateDeleted,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuditEvent {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub event_type: AuditEventType,
pub user_id: String,
pub source: crate::Source,
pub axes_changed: Vec<String>,
pub confidence: f32,
pub trace_id: Option<String>,
}
impl AuditEvent {
pub fn new(
event_type: AuditEventType,
user_id: impl Into<String>,
source: crate::Source,
axes_changed: Vec<String>,
confidence: f32,
) -> Self {
Self {
timestamp: chrono::Utc::now(),
event_type,
user_id: user_id.into(),
source,
axes_changed,
confidence,
trace_id: None,
}
}
pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
self.trace_id = Some(trace_id.into());
self
}
pub fn emit(&self) {
tracing::info!(
event_type = ?self.event_type,
user_id = %self.user_id,
source = %self.source,
axes_changed = ?self.axes_changed,
confidence = %self.confidence,
trace_id = ?self.trace_id,
"audit_event"
);
}
}
pub mod metric_names {
pub const STATE_UPDATES_TOTAL: &str = "attuned_state_updates_total";
pub const STATE_READS_TOTAL: &str = "attuned_state_reads_total";
pub const TRANSLATIONS_TOTAL: &str = "attuned_translations_total";
pub const ERRORS_TOTAL: &str = "attuned_errors_total";
pub const STATE_UPDATE_DURATION: &str = "attuned_state_update_duration_seconds";
pub const STATE_READ_DURATION: &str = "attuned_state_read_duration_seconds";
pub const TRANSLATION_DURATION: &str = "attuned_translation_duration_seconds";
pub const ACTIVE_USERS: &str = "attuned_active_users";
pub const HTTP_REQUEST_DURATION: &str = "attuned_http_request_duration_seconds";
}
pub mod span_names {
pub const STORE_UPSERT: &str = "attuned.store.upsert";
pub const STORE_GET: &str = "attuned.store.get";
pub const STORE_DELETE: &str = "attuned.store.delete";
pub const TRANSLATE: &str = "attuned.translate";
pub const HEALTH_CHECK: &str = "attuned.health_check";
pub const HTTP_REQUEST: &str = "attuned.http.request";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_health_status_aggregation() {
let checks = vec![
ComponentHealth::healthy("store"),
ComponentHealth::degraded("qdrant", "high latency"),
];
let status = HealthStatus::from_checks(checks, 100);
assert_eq!(status.status, HealthState::Degraded);
}
#[test]
fn test_health_status_unhealthy_dominates() {
let checks = vec![
ComponentHealth::healthy("store"),
ComponentHealth::unhealthy("qdrant", "connection failed"),
ComponentHealth::degraded("cache", "high miss rate"),
];
let status = HealthStatus::from_checks(checks, 100);
assert_eq!(status.status, HealthState::Unhealthy);
}
#[test]
fn test_audit_event_creation() {
let event = AuditEvent::new(
AuditEventType::StateUpdated,
"user_123",
crate::Source::SelfReport,
vec!["warmth".to_string(), "formality".to_string()],
1.0,
);
assert_eq!(event.user_id, "user_123");
assert_eq!(event.axes_changed.len(), 2);
}
}