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 #[serde(default, skip_serializing_if = "Option::is_none")]
509 pub cost: Option<f64>,
510 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub cost_source: Option<String>,
513}
514
515#[derive(Debug, Clone, Default, Serialize, Deserialize)]
517#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
518#[serde(rename_all = "snake_case")]
519pub enum TopSpanSort {
520 #[default]
522 TotalTokens,
523 Duration,
525 OutputInputRatio,
527 CacheEfficiency,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
533#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
534pub struct TopSpan {
535 pub trace_id: String,
536 pub span_id: String,
537 pub start_time: i64,
539 pub duration: i64,
541 pub model: Option<String>,
542 pub system: Option<String>,
543 pub session_id: Option<String>,
544 pub prompt_id: Option<String>,
545 pub input_tokens: u64,
546 pub output_tokens: u64,
547 pub cache_creation_tokens: u64,
548 pub cache_read_tokens: u64,
549 pub total_tokens: u64,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub finish_reason: Option<String>,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub conversation_id: Option<String>,
556 #[serde(default, skip_serializing_if = "Option::is_none")]
559 pub cost: Option<f64>,
560 #[serde(default, skip_serializing_if = "Option::is_none")]
562 pub cost_source: Option<String>,
563 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub cost_reason: Option<String>,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub derived_output_tokens_per_sec: Option<f64>,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize)]
575#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
576pub struct SessionCostRow {
577 pub session_id: String,
578 pub request_count: u64,
579 pub input_tokens: u64,
580 pub output_tokens: u64,
581 pub total_tokens: u64,
582 #[serde(default, skip_serializing_if = "Option::is_none")]
583 pub cost: Option<f64>,
584 #[serde(default, skip_serializing_if = "Option::is_none")]
585 pub cost_source: Option<String>,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
590#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
591pub struct ConversationCostRow {
592 pub conversation_id: String,
593 pub request_count: u64,
594 pub input_tokens: u64,
595 pub output_tokens: u64,
596 pub total_tokens: u64,
597 #[serde(default, skip_serializing_if = "Option::is_none")]
598 pub cost: Option<f64>,
599 #[serde(default, skip_serializing_if = "Option::is_none")]
600 pub cost_source: Option<String>,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
605#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
606pub struct FinishReasonCount {
607 pub reason: String,
608 pub count: usize,
609}
610
611#[derive(Debug, Clone, Serialize, Deserialize)]
613#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
614pub struct LatencyStats {
615 pub model: Option<String>,
616 pub count: usize,
617 pub avg_ms: f64,
618 pub p50_ms: i64,
619 pub p95_ms: i64,
620 pub p99_ms: i64,
621 pub ttft_count: usize,
623 pub ttft_p50_ms: Option<i64>,
624 pub ttft_p95_ms: Option<i64>,
625 pub ttft_p99_ms: Option<i64>,
626 pub derived_tokens_per_sec_p50: Option<f64>,
630 pub derived_tokens_per_sec_p95: Option<f64>,
631 pub derived_tokens_per_sec_p99: Option<f64>,
632 pub input_tokens_p50: Option<i64>,
634 pub input_tokens_p95: Option<i64>,
635 pub input_tokens_p99: Option<i64>,
636 pub output_input_ratio_p50: Option<f64>,
638 pub output_input_ratio_p95: Option<f64>,
639 pub output_input_ratio_p99: Option<f64>,
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize)]
644#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
645pub struct ErrorRateByModel {
646 pub model: Option<String>,
647 pub total: usize,
648 pub errors: usize,
649 pub error_rate: f64,
651}
652
653#[derive(Debug, Clone, Serialize, Deserialize)]
655#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
656pub struct ToolUsage {
657 pub tool_name: String,
658 pub count: usize,
659 pub success_count: usize,
660 pub error_count: usize,
661 pub avg_duration_ms: f64,
662 pub total_duration_ms: i64,
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize)]
667#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
668pub struct RetryStats {
669 pub total_llm_calls: usize,
670 pub retried_calls: usize,
672 pub extra_attempts: usize,
674 pub retry_rate: f64,
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize)]
680#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
681pub struct RetrievalStats {
682 pub total_retrievals: usize,
683 pub avg_documents_per_query: f64,
684 pub avg_top_document_score: Option<f64>,
686 pub top_queries: Vec<TopRetrievalQuery>,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize)]
691#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
692pub struct TopRetrievalQuery {
693 pub query: String,
694 pub count: usize,
695 pub avg_documents: f64,
696 pub avg_top_score: Option<f64>,
697}
698
699#[derive(Debug, Clone, Serialize, Deserialize)]
701#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
702pub struct TruncationRateByModel {
703 pub model: Option<String>,
704 pub total: usize,
705 pub truncated: usize,
706 pub rate: f64,
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
714#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
715pub struct CacheHitRateByModel {
716 pub model: Option<String>,
717 pub total_input_tokens: u64,
718 pub total_cache_read_tokens: u64,
719 pub total_cache_creation_tokens: u64,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub hit_rate: Option<f64>,
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize)]
726#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
727pub struct TemperatureBucket {
728 pub temperature: Option<f64>,
730 pub count: usize,
731}
732
733#[derive(Debug, Clone, Serialize, Deserialize)]
735#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
736pub struct MaxTokensBucket {
737 pub max_tokens: Option<i64>,
739 pub count: usize,
740}
741
742#[derive(Debug, Clone, Serialize, Deserialize)]
744#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
745pub struct RequestParamProfile {
746 pub temperature_buckets: Vec<TemperatureBucket>,
747 pub max_tokens_buckets: Vec<MaxTokensBucket>,
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
752#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
753pub struct ConversationDepthStats {
754 pub total_conversations: usize,
755 pub avg_turns: f64,
756 pub p50_turns: i64,
757 pub p95_turns: i64,
758 pub p99_turns: i64,
759}
760
761#[derive(Debug, Clone, Serialize, Deserialize)]
763#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
764pub struct CallsSeriesPoint {
765 pub timestamp: i64,
767 pub model: Option<String>,
769 pub requests: usize,
771}
772
773#[derive(Debug, Clone, Serialize, Deserialize)]
775#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
776pub struct ErrorTypeBreakdown {
777 pub model: Option<String>,
778 pub error_type: String,
780 pub bucket: String,
783 pub count: usize,
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize)]
789#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
790pub struct ModelDriftPair {
791 pub request_model: Option<String>,
792 pub response_model: Option<String>,
793 pub count: usize,
794 pub differs: bool,
796}