1use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
14pub struct ErrorResponse {
15 pub error: String,
17 pub code: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub details: Option<String>,
22}
23
24impl ErrorResponse {
25 pub fn new(code: impl Into<String>, error: impl Into<String>) -> Self {
27 Self {
28 error: error.into(),
29 code: code.into(),
30 details: None,
31 }
32 }
33
34 pub fn with_details(
36 code: impl Into<String>,
37 error: impl Into<String>,
38 details: impl Into<String>,
39 ) -> Self {
40 Self {
41 error: error.into(),
42 code: code.into(),
43 details: Some(details.into()),
44 }
45 }
46
47 pub fn bad_request(message: impl Into<String>) -> Self {
49 Self::new("BAD_REQUEST", message)
50 }
51
52 pub fn not_found(resource: impl Into<String>) -> Self {
54 Self::new("NOT_FOUND", format!("{} not found", resource.into()))
55 }
56
57 pub fn internal_error(message: impl Into<String>) -> Self {
59 Self::new("INTERNAL_ERROR", message)
60 }
61
62 pub fn storage_error(operation: impl Into<String>) -> Self {
64 Self::with_details(
65 "STORAGE_ERROR",
66 format!("Storage operation failed: {}", operation.into()),
67 "Check storage configuration and disk space",
68 )
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
75pub struct LogsResponse {
76 pub logs: Vec<LogEntry>,
77 pub total: usize,
78 pub limit: usize,
79 pub offset: usize,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
85pub struct LogEntry {
86 pub timestamp: i64,
87 pub severity: String,
88 pub severity_text: Option<String>,
89 pub body: String,
90 #[serde(default)]
91 pub attributes: HashMap<String, String>,
92 pub resource: Option<Resource>,
93 pub trace_id: Option<String>,
94 pub span_id: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
100pub struct Resource {
101 pub attributes: HashMap<String, String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
107pub struct TracesResponse {
108 pub traces: Vec<TraceEntry>,
109 pub total: usize,
110 pub limit: usize,
111 pub offset: usize,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
117pub struct TraceEntry {
118 pub trace_id: String,
119 pub root_span_name: String,
120 pub start_time: i64,
121 pub duration: i64,
122 pub span_count: usize,
123 pub service_names: Vec<String>,
124 pub has_errors: bool,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
130pub struct TraceDetail {
131 pub trace_id: String,
132 pub spans: Vec<SpanEntry>,
133 pub start_time: i64,
134 pub end_time: i64,
135 pub duration: i64,
136 pub span_count: usize,
137 pub service_names: Vec<String>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
143pub struct SpanEntry {
144 pub span_id: String,
145 pub trace_id: String,
146 pub parent_span_id: Option<String>,
147 pub name: String,
148 pub kind: String,
149 pub start_time: i64,
150 pub end_time: i64,
151 pub duration: i64,
152 #[serde(default)]
153 pub attributes: HashMap<String, String>,
154 pub resource: Option<Resource>,
155 pub status: SpanStatus,
156 pub events: Vec<SpanEvent>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
162pub struct SpanStatus {
163 pub code: String,
164 pub message: Option<String>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
170pub struct SpanEvent {
171 pub name: String,
172 pub timestamp: i64,
173 #[serde(default)]
174 pub attributes: HashMap<String, String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
180pub struct MetricResponse {
181 pub name: String,
182 pub description: Option<String>,
183 pub unit: Option<String>,
184 pub metric_type: String,
185 pub value: MetricValue,
186 pub timestamp: i64,
187 #[serde(default)]
188 pub attributes: HashMap<String, String>,
189 pub resource: Option<Resource>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(untagged)]
195pub enum MetricValue {
196 Gauge(f64),
197 Counter(i64),
198 Histogram(HistogramValue),
199 Summary(SummaryValue),
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct HistogramValue {
205 pub sum: f64,
206 pub count: u64,
207 pub buckets: Vec<HistogramBucket>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct HistogramBucket {
213 pub upper_bound: f64,
214 pub count: u64,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SummaryValue {
220 pub sum: f64,
221 pub count: u64,
222 pub quantiles: Vec<Quantile>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct Quantile {
228 pub quantile: f64,
229 pub value: f64,
230}
231
232impl From<crate::telemetry::LogRecord> for LogEntry {
235 fn from(log: crate::telemetry::LogRecord) -> Self {
236 Self {
237 timestamp: log.timestamp,
238 severity: log.severity.as_str().to_string(),
239 severity_text: Some(log.severity.as_str().to_string()),
240 body: log.body,
241 attributes: log.attributes,
242 resource: log.resource.map(Resource::from),
243 trace_id: log.trace_id,
244 span_id: log.span_id,
245 }
246 }
247}
248
249impl From<crate::telemetry::Resource> for Resource {
250 fn from(resource: crate::telemetry::Resource) -> Self {
251 Self {
252 attributes: resource.attributes,
253 }
254 }
255}
256
257impl From<crate::telemetry::Span> for SpanEntry {
258 fn from(span: crate::telemetry::Span) -> Self {
259 use crate::telemetry::trace::{SpanKind, StatusCode};
260
261 let kind_str = match span.kind {
262 SpanKind::Internal => "Internal",
263 SpanKind::Server => "Server",
264 SpanKind::Client => "Client",
265 SpanKind::Producer => "Producer",
266 SpanKind::Consumer => "Consumer",
267 };
268
269 let status_code_str = match span.status.code {
270 StatusCode::Unset => "Unset",
271 StatusCode::Ok => "Ok",
272 StatusCode::Error => "Error",
273 };
274
275 Self {
276 span_id: span.span_id,
277 trace_id: span.trace_id,
278 parent_span_id: span.parent_span_id,
279 name: span.name,
280 kind: kind_str.to_string(),
281 start_time: span.start_time,
282 end_time: span.end_time,
283 duration: span.end_time - span.start_time,
284 attributes: span.attributes,
285 resource: span.resource.map(Resource::from),
286 status: SpanStatus {
287 code: status_code_str.to_string(),
288 message: span.status.message,
289 },
290 events: span
291 .events
292 .into_iter()
293 .map(|e| SpanEvent {
294 name: e.name,
295 timestamp: e.timestamp,
296 attributes: e.attributes,
297 })
298 .collect(),
299 }
300 }
301}
302
303impl From<crate::telemetry::Metric> for MetricResponse {
304 fn from(metric: crate::telemetry::Metric) -> Self {
305 use crate::telemetry::metric::MetricType;
306
307 let (metric_type_str, value) = match metric.metric_type {
308 MetricType::Gauge(v) => ("gauge", MetricValue::Gauge(v)),
309 MetricType::Counter(v) => ("counter", MetricValue::Counter(v as i64)),
310 MetricType::Histogram {
311 count,
312 sum,
313 buckets,
314 } => (
315 "histogram",
316 MetricValue::Histogram(HistogramValue {
317 sum,
318 count,
319 buckets: buckets
320 .into_iter()
321 .map(|b| HistogramBucket {
322 upper_bound: b.upper_bound,
323 count: b.count,
324 })
325 .collect(),
326 }),
327 ),
328 MetricType::Summary {
329 count,
330 sum,
331 quantiles,
332 } => (
333 "summary",
334 MetricValue::Summary(SummaryValue {
335 sum,
336 count,
337 quantiles: quantiles
338 .into_iter()
339 .map(|q| Quantile {
340 quantile: q.quantile,
341 value: q.value,
342 })
343 .collect(),
344 }),
345 ),
346 };
347
348 Self {
349 name: metric.name,
350 description: metric.description,
351 unit: metric.unit,
352 metric_type: metric_type_str.to_string(),
353 value,
354 timestamp: metric.timestamp,
355 attributes: metric.attributes,
356 resource: metric.resource.map(Resource::from),
357 }
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn test_error_response_new() {
367 let err = ErrorResponse::new("TEST_ERROR", "Test error message");
368 assert_eq!(err.code, "TEST_ERROR");
369 assert_eq!(err.error, "Test error message");
370 assert!(err.details.is_none());
371 }
372
373 #[test]
374 fn test_error_response_with_details() {
375 let err =
376 ErrorResponse::with_details("TEST_ERROR", "Test error message", "Additional details");
377 assert_eq!(err.code, "TEST_ERROR");
378 assert_eq!(err.error, "Test error message");
379 assert_eq!(err.details, Some("Additional details".to_string()));
380 }
381
382 #[test]
383 fn test_error_response_bad_request() {
384 let err = ErrorResponse::bad_request("Invalid parameter");
385 assert_eq!(err.code, "BAD_REQUEST");
386 assert_eq!(err.error, "Invalid parameter");
387 }
388
389 #[test]
390 fn test_error_response_not_found() {
391 let err = ErrorResponse::not_found("Log entry");
392 assert_eq!(err.code, "NOT_FOUND");
393 assert_eq!(err.error, "Log entry not found");
394 }
395
396 #[test]
397 fn test_error_response_internal_error() {
398 let err = ErrorResponse::internal_error("Database connection failed");
399 assert_eq!(err.code, "INTERNAL_ERROR");
400 assert_eq!(err.error, "Database connection failed");
401 }
402
403 #[test]
404 fn test_error_response_storage_error() {
405 let err = ErrorResponse::storage_error("write");
406 assert_eq!(err.code, "STORAGE_ERROR");
407 assert!(err.error.contains("write"));
408 assert!(err.details.is_some());
409 }
410
411 #[test]
412 fn test_error_response_serialization() {
413 let err = ErrorResponse::with_details("TEST", "message", "details");
414 let json = serde_json::to_string(&err).unwrap();
415 assert!(json.contains("\"code\":\"TEST\""));
416 assert!(json.contains("\"error\":\"message\""));
417 assert!(json.contains("\"details\":\"details\""));
418 }
419
420 #[test]
421 fn test_error_response_deserialization() {
422 let json = r#"{"error":"test message","code":"TEST_CODE","details":"test details"}"#;
423 let err: ErrorResponse = serde_json::from_str(json).unwrap();
424 assert_eq!(err.code, "TEST_CODE");
425 assert_eq!(err.error, "test message");
426 assert_eq!(err.details, Some("test details".to_string()));
427 }
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize)]
432#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
433pub struct TokenUsageResponse {
434 pub summary: TokenUsageSummary,
436 pub by_model: Vec<ModelUsage>,
438 pub by_system: Vec<SystemUsage>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
444#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
445pub struct TokenUsageSummary {
446 pub total_input_tokens: u64,
448 pub total_output_tokens: u64,
450 pub total_requests: usize,
452 #[serde(default)]
454 pub total_cache_creation_tokens: u64,
455 #[serde(default)]
457 pub total_cache_read_tokens: u64,
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
462#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
463pub struct ModelUsage {
464 pub model: String,
466 pub input_tokens: u64,
468 pub output_tokens: u64,
470 pub requests: usize,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
476#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
477pub struct SystemUsage {
478 pub system: String,
480 pub input_tokens: u64,
482 pub output_tokens: u64,
484 pub requests: usize,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
490#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
491pub struct CostSeriesPoint {
492 pub timestamp: i64,
494 pub model: Option<String>,
496 pub input_tokens: u64,
498 pub output_tokens: u64,
500 pub cache_creation_tokens: u64,
502 pub cache_read_tokens: u64,
504 pub requests: usize,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
510#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
511pub struct TopSpan {
512 pub trace_id: String,
513 pub span_id: String,
514 pub start_time: i64,
516 pub duration: i64,
518 pub model: Option<String>,
519 pub system: Option<String>,
520 pub session_id: Option<String>,
521 pub prompt_id: Option<String>,
522 pub input_tokens: u64,
523 pub output_tokens: u64,
524 pub cache_creation_tokens: u64,
525 pub cache_read_tokens: u64,
526 pub total_tokens: u64,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
531#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
532pub struct FinishReasonCount {
533 pub reason: String,
534 pub count: usize,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize)]
539#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
540pub struct LatencyStats {
541 pub model: Option<String>,
542 pub count: usize,
543 pub avg_ms: f64,
544 pub p50_ms: i64,
545 pub p95_ms: i64,
546 pub p99_ms: i64,
547 pub ttft_count: usize,
549 pub ttft_p50_ms: Option<i64>,
550 pub ttft_p95_ms: Option<i64>,
551 pub ttft_p99_ms: Option<i64>,
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize)]
556#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
557pub struct ErrorRateByModel {
558 pub model: Option<String>,
559 pub total: usize,
560 pub errors: usize,
561 pub error_rate: f64,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
567#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
568pub struct ToolUsage {
569 pub tool_name: String,
570 pub count: usize,
571 pub success_count: usize,
572 pub error_count: usize,
573 pub avg_duration_ms: f64,
574 pub total_duration_ms: i64,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
579#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
580pub struct RetryStats {
581 pub total_llm_calls: usize,
582 pub retried_calls: usize,
584 pub extra_attempts: usize,
586 pub retry_rate: f64,
588}