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#[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#[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 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 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