1use leviath_core::telemetry::{LaneHealth, LogKind, ProviderHealth, TelemetryEvent, TelemetrySink};
11
12pub(crate) fn format_event(event: &TelemetryEvent) -> String {
14 match event {
15 TelemetryEvent::RunStarted {
16 run_id,
17 agent_name,
18 recovered,
19 ..
20 } => {
21 let suffix = if *recovered { " (recovered)" } else { "" };
22 format!("run {run_id} started: agent {agent_name}{suffix}")
23 }
24 TelemetryEvent::StageEntered {
25 run_id,
26 stage_index,
27 stage_name,
28 ..
29 } => format!("run {run_id} entered stage {stage_index} ({stage_name})"),
30 TelemetryEvent::StageExited {
31 run_id,
32 stage_index,
33 stage_name,
34 prompt_tokens,
35 completion_tokens,
36 ..
37 } => format!(
38 "run {run_id} exited stage {stage_index} ({stage_name}): \
39 {prompt_tokens} in, {completion_tokens} out"
40 ),
41 TelemetryEvent::InferenceCompleted {
42 run_id,
43 provider,
44 model,
45 latency_ms,
46 prompt_tokens,
47 completion_tokens,
48 success,
49 ..
50 } => format!(
51 "run {run_id} inference {}: {provider}/{model} {latency_ms}ms, \
52 {prompt_tokens} in, {completion_tokens} out",
53 if *success { "ok" } else { "failed" }
54 ),
55 TelemetryEvent::ToolCallCompleted {
56 run_id,
57 tool_name,
58 batch_latency_ms,
59 success,
60 ..
61 } => format!(
62 "run {run_id} tool {tool_name} {}: batch {batch_latency_ms}ms",
63 if *success { "ok" } else { "failed" }
64 ),
65 TelemetryEvent::CompactionCompleted {
66 run_id, success, ..
67 } => format!(
68 "run {run_id} compaction {}",
69 if *success { "ok" } else { "failed" }
70 ),
71 TelemetryEvent::RunCompleted {
72 run_id,
73 status,
74 prompt_tokens,
75 completion_tokens,
76 tool_calls,
77 empty_output,
78 ..
79 } => format!(
80 "run {run_id} {status}: {prompt_tokens} in, {completion_tokens} out, \
81 {tool_calls} tool calls{}",
82 if *empty_output { " (no output)" } else { "" }
83 ),
84 TelemetryEvent::Log {
85 run_id,
86 stage_index,
87 kind,
88 line,
89 } => {
90 let kind = match kind {
91 LogKind::Output => "output",
92 LogKind::Runtime => "log",
93 };
94 format!("run {run_id} stage {stage_index} {kind}: {line}")
95 }
96 }
97}
98
99pub(crate) fn format_lane_health(health: &LaneHealth) -> String {
101 format!(
102 "lanes: agents active={} waiting={}, tools {}/{} busy, {} parked, {} queued, \
103 dead cycles {}, relief {}",
104 health.agents_active,
105 health.agents_waiting,
106 health.tools_busy,
107 health.tools_workers,
108 health.tools_parked,
109 health.tools_queued,
110 health.dead_cycles,
111 health.relief_granted,
112 )
113}
114
115pub(crate) fn format_providers_down(down: &[ProviderHealth]) -> Option<String> {
120 if down.is_empty() {
121 return None;
122 }
123 let each = down
124 .iter()
125 .map(|p| {
126 format!(
127 "{} ({}, {} failures, retry in {}s)",
128 p.provider, p.reason, p.consecutive_failures, p.retry_in_secs
129 )
130 })
131 .collect::<Vec<_>>()
132 .join(", ");
133 Some(format!("providers out of service: {each}"))
134}
135
136pub struct LogSink;
138
139impl TelemetrySink for LogSink {
140 fn emit(&self, event: TelemetryEvent) {
141 tracing::info!(target: "leviath::telemetry", "{}", format_event(&event));
142 }
143
144 fn observe_lanes(&self, health: LaneHealth) {
145 tracing::info!(target: "leviath::telemetry", "{}", format_lane_health(&health));
146 }
147
148 fn observe_providers(&self, down: &[ProviderHealth]) {
149 if let Some(line) = format_providers_down(down) {
150 tracing::warn!(target: "leviath::telemetry", "{line}");
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn every_event_formats_to_one_line() {
161 let events = [
162 (
163 TelemetryEvent::RunStarted {
164 run_id: "r1".to_string(),
165 agent_name: "coder".to_string(),
166 model: None,
167 parent_run_id: None,
168 recovered: false,
169 at_ms: 0,
170 },
171 "run r1 started: agent coder",
172 ),
173 (
174 TelemetryEvent::RunStarted {
175 run_id: "r1".to_string(),
176 agent_name: "coder".to_string(),
177 model: None,
178 parent_run_id: None,
179 recovered: true,
180 at_ms: 0,
181 },
182 "run r1 started: agent coder (recovered)",
183 ),
184 (
185 TelemetryEvent::StageEntered {
186 run_id: "r1".to_string(),
187 stage_index: 1,
188 stage_name: "build".to_string(),
189 at_ms: 0,
190 },
191 "run r1 entered stage 1 (build)",
192 ),
193 (
194 TelemetryEvent::StageExited {
195 run_id: "r1".to_string(),
196 stage_index: 1,
197 stage_name: "build".to_string(),
198 prompt_tokens: 10,
199 completion_tokens: 4,
200 at_ms: 0,
201 },
202 "run r1 exited stage 1 (build): 10 in, 4 out",
203 ),
204 (
205 TelemetryEvent::InferenceCompleted {
206 run_id: "r1".to_string(),
207 stage_name: "build".to_string(),
208 provider: "anthropic".to_string(),
209 model: "m".to_string(),
210 latency_ms: 120,
211 prompt_tokens: 10,
212 completion_tokens: 4,
213 cached_tokens: 0,
214 success: true,
215 },
216 "run r1 inference ok: anthropic/m 120ms, 10 in, 4 out",
217 ),
218 (
219 TelemetryEvent::InferenceCompleted {
220 run_id: "r1".to_string(),
221 stage_name: "build".to_string(),
222 provider: "anthropic".to_string(),
223 model: "m".to_string(),
224 latency_ms: 120,
225 prompt_tokens: 0,
226 completion_tokens: 0,
227 cached_tokens: 0,
228 success: false,
229 },
230 "run r1 inference failed: anthropic/m 120ms, 0 in, 0 out",
231 ),
232 (
233 TelemetryEvent::ToolCallCompleted {
234 run_id: "r1".to_string(),
235 stage_name: "build".to_string(),
236 tool_name: "read_file".to_string(),
237 batch_latency_ms: 30,
238 success: true,
239 },
240 "run r1 tool read_file ok: batch 30ms",
241 ),
242 (
243 TelemetryEvent::ToolCallCompleted {
244 run_id: "r1".to_string(),
245 stage_name: "build".to_string(),
246 tool_name: "shell".to_string(),
247 batch_latency_ms: 30,
248 success: false,
249 },
250 "run r1 tool shell failed: batch 30ms",
251 ),
252 (
253 TelemetryEvent::CompactionCompleted {
254 run_id: "r1".to_string(),
255 stage_name: "build".to_string(),
256 success: true,
257 },
258 "run r1 compaction ok",
259 ),
260 (
261 TelemetryEvent::CompactionCompleted {
262 run_id: "r1".to_string(),
263 stage_name: "build".to_string(),
264 success: false,
265 },
266 "run r1 compaction failed",
267 ),
268 (
269 TelemetryEvent::RunCompleted {
270 run_id: "r1".to_string(),
271 status: "complete".to_string(),
272 prompt_tokens: 10,
273 completion_tokens: 4,
274 tool_calls: 2,
275 empty_output: false,
276 at_ms: 0,
277 },
278 "run r1 complete: 10 in, 4 out, 2 tool calls",
279 ),
280 (
281 TelemetryEvent::RunCompleted {
284 run_id: "r1".to_string(),
285 status: "complete".to_string(),
286 prompt_tokens: 10,
287 completion_tokens: 4,
288 tool_calls: 2,
289 empty_output: true,
290 at_ms: 0,
291 },
292 "run r1 complete: 10 in, 4 out, 2 tool calls (no output)",
293 ),
294 (
295 TelemetryEvent::Log {
296 run_id: "r1".to_string(),
297 stage_index: 0,
298 kind: LogKind::Output,
299 line: "hello".to_string(),
300 },
301 "run r1 stage 0 output: hello",
302 ),
303 (
304 TelemetryEvent::Log {
305 run_id: "r1".to_string(),
306 stage_index: 0,
307 kind: LogKind::Runtime,
308 line: "[Tokens: 1 in, 1 out]".to_string(),
309 },
310 "run r1 stage 0 log: [Tokens: 1 in, 1 out]",
311 ),
312 ];
313 for (event, expected) in events {
314 assert_eq!(format_event(&event), expected);
315 }
316 }
317
318 #[test]
319 fn emit_routes_through_tracing_without_panicking() {
320 let _guard = leviath_testkit::tracing_guard();
321 LogSink.emit(TelemetryEvent::RunCompleted {
322 run_id: "r1".to_string(),
323 status: "complete".to_string(),
324 prompt_tokens: 0,
325 completion_tokens: 0,
326 tool_calls: 0,
327 empty_output: false,
328 at_ms: 0,
329 });
330 }
331
332 #[test]
335 fn lane_health_formats_to_one_line() {
336 let line = format_lane_health(&LaneHealth {
337 agents_active: 6,
338 agents_waiting: 2,
339 tools_busy: 8,
340 tools_queued: 12,
341 tools_parked: 3,
342 tools_workers: 8,
343 dead_cycles: 4,
344 relief_granted: 2,
345 });
346 assert_eq!(
347 line,
348 "lanes: agents active=6 waiting=2, tools 8/8 busy, 3 parked, 12 queued, \
349 dead cycles 4, relief 2"
350 );
351 assert!(!line.contains('\n'), "one line: {line}");
352 }
353
354 #[test]
355 fn observe_lanes_routes_through_tracing_without_panicking() {
356 let _guard = leviath_testkit::tracing_guard();
357 LogSink.observe_lanes(LaneHealth::default());
358 }
359
360 #[test]
361 fn providers_down_formats_to_one_line() {
362 let line = format_providers_down(&[
363 ProviderHealth {
364 provider: "openrouter".to_string(),
365 reason: "credits-exhausted".to_string(),
366 consecutive_failures: 3,
367 retry_in_secs: 240,
368 },
369 ProviderHealth {
370 provider: "anthropic".to_string(),
371 reason: "auth-failed".to_string(),
372 consecutive_failures: 5,
373 retry_in_secs: 30,
374 },
375 ])
376 .expect("something is down");
377 assert_eq!(
378 line,
379 "providers out of service: openrouter (credits-exhausted, 3 failures, retry in 240s), \
380 anthropic (auth-failed, 5 failures, retry in 30s)"
381 );
382 assert!(!line.contains('\n'), "one line: {line}");
383 }
384
385 #[test]
387 fn nothing_down_is_no_line_at_all() {
388 assert_eq!(format_providers_down(&[]), None);
389 let _guard = leviath_testkit::tracing_guard();
390 LogSink.observe_providers(&[]);
391 }
392
393 #[test]
394 fn observe_providers_routes_through_tracing_without_panicking() {
395 let _guard = leviath_testkit::tracing_guard();
396 LogSink.observe_providers(&[ProviderHealth {
397 provider: "openrouter".to_string(),
398 reason: "credits-exhausted".to_string(),
399 consecutive_failures: 3,
400 retry_in_secs: 240,
401 }]);
402 }
403}