Skip to main content

claw10_telemetry/
lib.rs

1use std::collections::HashMap;
2
3use chrono::{DateTime, Utc};
4use serde::Serialize;
5use tracing::info;
6
7#[derive(Debug, thiserror::Error)]
8pub enum TelemetryError {
9    #[error("Telemetry emission failed: {0}")]
10    EmissionFailed(String),
11}
12
13/// A structured telemetry event for observability pipeline consumption.
14///
15/// Follows the Claw10 Telemetry Specification (PRD ยง36).
16/// Required fields: timestamp, event_type, status.
17/// Optional fields for correlation: tenant_id, mission_id, task_id, agent_id, etc.
18#[derive(Debug, Clone, Serialize)]
19pub struct TelemetryEvent {
20    pub timestamp: DateTime<Utc>,
21    pub tenant_id: Option<String>,
22    pub mission_id: Option<String>,
23    pub task_id: Option<String>,
24    pub agent_id: Option<String>,
25    pub parent_agent_id: Option<String>,
26    pub lineage_id: Option<String>,
27    pub worker_id: Option<String>,
28    pub trace_id: Option<String>,
29    pub event_type: String,
30    pub lifecycle_mode: Option<String>,
31    pub risk_level: Option<String>,
32    pub status: String,
33    pub cost_usd: f64,
34    pub additional: HashMap<String, String>,
35}
36
37impl TelemetryEvent {
38    #[must_use]
39    pub fn new(event_type: impl Into<String>, status: impl Into<String>) -> Self {
40        Self {
41            timestamp: Utc::now(),
42            tenant_id: None,
43            mission_id: None,
44            task_id: None,
45            agent_id: None,
46            parent_agent_id: None,
47            lineage_id: None,
48            worker_id: None,
49            trace_id: None,
50            event_type: event_type.into(),
51            lifecycle_mode: None,
52            risk_level: None,
53            status: status.into(),
54            cost_usd: 0.0,
55            additional: HashMap::new(),
56        }
57    }
58
59    #[must_use]
60    pub fn with_tenant_id(mut self, tenant_id: String) -> Self {
61        self.tenant_id = Some(tenant_id);
62        self
63    }
64
65    #[must_use]
66    pub fn with_mission_id(mut self, mission_id: String) -> Self {
67        self.mission_id = Some(mission_id);
68        self
69    }
70
71    #[must_use]
72    pub fn with_task_id(mut self, task_id: String) -> Self {
73        self.task_id = Some(task_id);
74        self
75    }
76
77    #[must_use]
78    pub fn with_agent_id(mut self, agent_id: String) -> Self {
79        self.agent_id = Some(agent_id);
80        self
81    }
82
83    #[must_use]
84    pub fn with_parent_agent_id(mut self, parent_agent_id: String) -> Self {
85        self.parent_agent_id = Some(parent_agent_id);
86        self
87    }
88
89    #[must_use]
90    pub fn with_lineage_id(mut self, lineage_id: String) -> Self {
91        self.lineage_id = Some(lineage_id);
92        self
93    }
94
95    #[must_use]
96    pub fn with_worker_id(mut self, worker_id: String) -> Self {
97        self.worker_id = Some(worker_id);
98        self
99    }
100
101    #[must_use]
102    pub fn with_trace_id(mut self, trace_id: String) -> Self {
103        self.trace_id = Some(trace_id);
104        self
105    }
106
107    #[must_use]
108    pub fn with_lifecycle_mode(mut self, lifecycle_mode: String) -> Self {
109        self.lifecycle_mode = Some(lifecycle_mode);
110        self
111    }
112
113    #[must_use]
114    pub fn with_risk_level(mut self, risk_level: String) -> Self {
115        self.risk_level = Some(risk_level);
116        self
117    }
118
119    #[must_use]
120    pub fn with_cost(mut self, cost_usd: f64) -> Self {
121        self.cost_usd = cost_usd;
122        self
123    }
124
125    #[must_use]
126    pub fn with_additional(mut self, key: String, value: String) -> Self {
127        self.additional.insert(key, value);
128        self
129    }
130}
131
132/// Telemetry service for emitting structured events.
133///
134/// Per FR-093: Telemetry failure must not affect task/agent state.
135/// All emission methods are fire-and-forget.
136#[derive(Clone)]
137pub struct TelemetryService {
138    enabled: bool,
139}
140
141impl TelemetryService {
142    #[must_use]
143    pub fn new(enabled: bool) -> Self {
144        Self { enabled }
145    }
146
147    /// Emit a telemetry event as a structured JSON tracing log.
148    ///
149    /// # Errors
150    /// Returns `TelemetryError` only if serialization fails.
151    /// Per FR-093, callers should not propagate this error to task state.
152    pub fn emit(&self, event: &TelemetryEvent) -> Result<(), TelemetryError> {
153        if !self.enabled {
154            return Ok(());
155        }
156
157        let json = serde_json::to_string(event)
158            .map_err(|e| TelemetryError::EmissionFailed(e.to_string()))?;
159
160        info!(target: "claw10_telemetry", "{}", json);
161        Ok(())
162    }
163
164    /// Convenience method: build an event from parts and emit it.
165    ///
166    /// # Errors
167    /// Same as `emit`.
168    pub fn record(
169        &self,
170        event_type: &str,
171        status: &str,
172        builder: impl FnOnce(TelemetryEvent) -> TelemetryEvent,
173    ) -> Result<(), TelemetryError> {
174        let event = builder(TelemetryEvent::new(event_type, status));
175        self.emit(&event)
176    }
177}
178
179impl Default for TelemetryService {
180    fn default() -> Self {
181        Self { enabled: true }
182    }
183}
184
185#[cfg(test)]
186#[path = "lib_test.rs"]
187mod tests;
188