Skip to main content

tael_server/storage/
models.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Span {
7    pub trace_id: String,
8    pub span_id: String,
9    pub parent_span_id: Option<String>,
10    pub service: String,
11    pub operation: String,
12    pub start_time: DateTime<Utc>,
13    pub end_time: DateTime<Utc>,
14    pub duration_ms: f64,
15    pub status: SpanStatus,
16    pub attributes: HashMap<String, String>,
17    pub events: Vec<SpanEvent>,
18    /// Span kind. `Llm` is a synthetic marker set when GenAI attributes are
19    /// detected during ingestion (see `ingest::otlp`). Defaults keep older
20    /// stored rows and existing call sites working.
21    #[serde(default)]
22    pub kind: SpanKind,
23    /// Typed LLM extension, present iff this span is an LLM call.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub llm: Option<LlmSpan>,
26}
27
28/// Span kind. Mirrors the OpenTelemetry `SpanKind`, plus a synthetic `Llm`
29/// variant that marks spans carrying a typed [`LlmSpan`] extension.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
31#[serde(rename_all = "lowercase")]
32pub enum SpanKind {
33    #[default]
34    Internal,
35    Server,
36    Client,
37    Producer,
38    Consumer,
39    Llm,
40}
41
42impl std::fmt::Display for SpanKind {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        let s = match self {
45            SpanKind::Internal => "internal",
46            SpanKind::Server => "server",
47            SpanKind::Client => "client",
48            SpanKind::Producer => "producer",
49            SpanKind::Consumer => "consumer",
50            SpanKind::Llm => "llm",
51        };
52        write!(f, "{s}")
53    }
54}
55
56impl SpanKind {
57    pub fn from_str(s: &str) -> Self {
58        match s.to_lowercase().as_str() {
59            "server" => SpanKind::Server,
60            "client" => SpanKind::Client,
61            "producer" => SpanKind::Producer,
62            "consumer" => SpanKind::Consumer,
63            "llm" => SpanKind::Llm,
64            _ => SpanKind::Internal,
65        }
66    }
67}
68
69/// High-level LLM operation, from `gen_ai.operation.name`.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
71#[serde(rename_all = "lowercase")]
72pub enum LlmOperation {
73    #[default]
74    Chat,
75    Completion,
76    Embedding,
77    Tool,
78    Other,
79}
80
81impl LlmOperation {
82    /// Map an OpenTelemetry GenAI `gen_ai.operation.name` value.
83    pub fn from_str(s: &str) -> Self {
84        match s.to_lowercase().as_str() {
85            "chat" => LlmOperation::Chat,
86            "text_completion" | "completion" => LlmOperation::Completion,
87            "embeddings" | "embedding" => LlmOperation::Embedding,
88            "execute_tool" | "tool" => LlmOperation::Tool,
89            _ => LlmOperation::Other,
90        }
91    }
92}
93
94/// Typed extension for LLM spans. Well-known GenAI attributes are flattened
95/// into these fields; the unbounded tail stays in `Span::attributes`. Prompt
96/// and completion payloads are content-addressed blobs referenced by hash
97/// (populated in a later phase); only the hashes live here.
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct LlmSpan {
100    pub provider: String,
101    pub model: String,
102    pub operation: LlmOperation,
103
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub input_tokens: Option<u32>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub output_tokens: Option<u32>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub total_tokens: Option<u32>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub cost_usd: Option<f64>,
112
113    /// Time to first token (streaming responses).
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub ttft_ms: Option<f64>,
116    /// Mean inter-token latency (streaming responses).
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub inter_token_ms: Option<f64>,
119
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub prompt_sha256: Option<String>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub completion_sha256: Option<String>,
124
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub finish_reason: Option<String>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub temperature: Option<f64>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132#[serde(rename_all = "lowercase")]
133pub enum SpanStatus {
134    Ok,
135    Error,
136    Unset,
137}
138
139impl std::fmt::Display for SpanStatus {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self {
142            SpanStatus::Ok => write!(f, "ok"),
143            SpanStatus::Error => write!(f, "error"),
144            SpanStatus::Unset => write!(f, "unset"),
145        }
146    }
147}
148
149impl SpanStatus {
150    pub fn from_str(s: &str) -> Self {
151        match s.to_lowercase().as_str() {
152            "ok" => SpanStatus::Ok,
153            "error" => SpanStatus::Error,
154            _ => SpanStatus::Unset,
155        }
156    }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct SpanEvent {
161    pub name: String,
162    pub timestamp: DateTime<Utc>,
163    pub attributes: HashMap<String, String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct TraceComment {
168    pub id: String,
169    pub trace_id: String,
170    pub span_id: Option<String>,
171    pub author: String,
172    pub body: String,
173    pub created_at: String,
174}
175
176#[derive(Debug, Clone, Default, Serialize, Deserialize)]
177pub struct TraceQuery {
178    pub service: Option<String>,
179    pub operation: Option<String>,
180    pub min_duration_ms: Option<f64>,
181    pub max_duration_ms: Option<f64>,
182    pub status: Option<String>,
183    pub last_seconds: Option<i64>,
184    pub limit: Option<u32>,
185    /// Equality filters on span attributes. Each entry is ANDed.
186    /// Keys with characters outside `[A-Za-z0-9._\-:/]` are rejected at the storage layer.
187    #[serde(default)]
188    pub attributes: Vec<(String, String)>,
189    /// Full-text query over LLM prompt/completion payloads (Tantivy syntax).
190    /// Only honored by the `tael-backend` storage engine; ignored by DuckDB
191    /// (which doesn't retain payload text).
192    #[serde(default)]
193    pub text: Option<String>,
194}
195
196/// Per-service rollup returned by `Store::list_services`.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ServiceInfo {
199    pub name: String,
200    pub span_count: i64,
201    pub trace_count: i64,
202    pub avg_duration_ms: f64,
203    pub error_rate: f64,
204}
205
206// ── Log models ──────────────────────────────────────────────────────
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct LogRecord {
210    pub timestamp: DateTime<Utc>,
211    pub observed_timestamp: DateTime<Utc>,
212    pub trace_id: Option<String>,
213    pub span_id: Option<String>,
214    pub severity: LogSeverity,
215    pub severity_text: String,
216    /// The log body. For oversized bodies this is emptied at ingestion and the
217    /// content moved to the blob store, referenced by [`Self::body_sha256`].
218    pub body: String,
219    pub service: String,
220    pub attributes: HashMap<String, String>,
221    /// Set when the body was offloaded to the content-addressed blob store
222    /// (large bodies, e.g. stack traces). Resolve via the blob store to get
223    /// the original text. `None` for inline bodies.
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub body_sha256: Option<String>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
229#[serde(rename_all = "lowercase")]
230pub enum LogSeverity {
231    Trace,
232    Debug,
233    Info,
234    Warn,
235    Error,
236    Fatal,
237    Unspecified,
238}
239
240impl std::fmt::Display for LogSeverity {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        match self {
243            LogSeverity::Trace => write!(f, "trace"),
244            LogSeverity::Debug => write!(f, "debug"),
245            LogSeverity::Info => write!(f, "info"),
246            LogSeverity::Warn => write!(f, "warn"),
247            LogSeverity::Error => write!(f, "error"),
248            LogSeverity::Fatal => write!(f, "fatal"),
249            LogSeverity::Unspecified => write!(f, "unspecified"),
250        }
251    }
252}
253
254impl LogSeverity {
255    pub fn from_str(s: &str) -> Self {
256        match s.to_lowercase().as_str() {
257            "trace" => LogSeverity::Trace,
258            "debug" => LogSeverity::Debug,
259            "info" => LogSeverity::Info,
260            "warn" => LogSeverity::Warn,
261            "error" => LogSeverity::Error,
262            "fatal" => LogSeverity::Fatal,
263            _ => LogSeverity::Unspecified,
264        }
265    }
266
267    pub fn from_severity_number(n: i32) -> Self {
268        match n {
269            1..=4 => LogSeverity::Trace,
270            5..=8 => LogSeverity::Debug,
271            9..=12 => LogSeverity::Info,
272            13..=16 => LogSeverity::Warn,
273            17..=20 => LogSeverity::Error,
274            21..=24 => LogSeverity::Fatal,
275            _ => LogSeverity::Unspecified,
276        }
277    }
278}
279
280// ── Metric models ───────────────────────────────────────────────────
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct MetricPoint {
284    pub timestamp: DateTime<Utc>,
285    pub service: String,
286    pub name: String,
287    pub metric_type: MetricType,
288    pub value: f64,
289    pub unit: String,
290    pub attributes: HashMap<String, String>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
294#[serde(rename_all = "lowercase")]
295pub enum MetricType {
296    Gauge,
297    Sum,
298    Histogram,
299    Summary,
300    Unknown,
301}
302
303impl std::fmt::Display for MetricType {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        match self {
306            MetricType::Gauge => write!(f, "gauge"),
307            MetricType::Sum => write!(f, "sum"),
308            MetricType::Histogram => write!(f, "histogram"),
309            MetricType::Summary => write!(f, "summary"),
310            MetricType::Unknown => write!(f, "unknown"),
311        }
312    }
313}
314
315impl MetricType {
316    pub fn from_str(s: &str) -> Self {
317        match s.to_lowercase().as_str() {
318            "gauge" => MetricType::Gauge,
319            "sum" => MetricType::Sum,
320            "histogram" => MetricType::Histogram,
321            "summary" => MetricType::Summary,
322            _ => MetricType::Unknown,
323        }
324    }
325}
326
327#[derive(Debug, Clone, Default, Serialize, Deserialize)]
328pub struct MetricQuery {
329    pub service: Option<String>,
330    pub name: Option<String>,
331    pub metric_type: Option<String>,
332    pub last_seconds: Option<i64>,
333    pub limit: Option<u32>,
334}
335
336// ── Summary models ──────────────────────────────────────────────────
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct SummaryReport {
340    pub window_seconds: i64,
341    pub service_filter: Option<String>,
342    pub traces: TraceSummary,
343    pub top_services: Vec<ServiceSummary>,
344    pub top_error_operations: Vec<ErrorOperation>,
345    pub logs: LogSummary,
346    pub metrics: MetricSummary,
347}
348
349#[derive(Debug, Clone, Default, Serialize, Deserialize)]
350pub struct TraceSummary {
351    pub span_count: i64,
352    pub trace_count: i64,
353    pub error_count: i64,
354    pub error_rate: f64,
355    pub avg_ms: f64,
356    pub max_ms: f64,
357    pub p50_ms: f64,
358    pub p95_ms: f64,
359    pub p99_ms: f64,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct ServiceSummary {
364    pub service: String,
365    pub span_count: i64,
366    pub error_rate: f64,
367    pub p95_ms: f64,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct ErrorOperation {
372    pub service: String,
373    pub operation: String,
374    pub error_count: i64,
375}
376
377#[derive(Debug, Clone, Default, Serialize, Deserialize)]
378pub struct LogSummary {
379    pub total: i64,
380    pub error: i64,
381    pub warn: i64,
382    pub info: i64,
383    pub debug: i64,
384}
385
386#[derive(Debug, Clone, Default, Serialize, Deserialize)]
387pub struct MetricSummary {
388    pub point_count: i64,
389    pub unique_names: i64,
390}
391
392// ── Anomaly models ──────────────────────────────────────────────────
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct AnomalyReport {
396    pub current_seconds: i64,
397    pub baseline_seconds: i64,
398    pub service_filter: Option<String>,
399    pub anomalies: Vec<Anomaly>,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct Anomaly {
404    pub service: String,
405    pub kind: String,
406    pub severity: String,
407    pub current: f64,
408    pub baseline: f64,
409    pub delta: f64,
410    pub description: String,
411}
412
413// ── Correlate models ────────────────────────────────────────────────
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct CorrelateReport {
417    pub trace_id: String,
418    pub span_count: usize,
419    pub services: Vec<String>,
420    pub start_time: String,
421    pub end_time: String,
422    pub duration_ms: f64,
423    pub error_count: i64,
424    pub logs: Vec<LogRecord>,
425    pub metrics: Vec<MetricPoint>,
426}
427
428#[derive(Debug, Clone, Default, Serialize, Deserialize)]
429pub struct LogQuery {
430    pub service: Option<String>,
431    pub severity: Option<String>,
432    pub body_contains: Option<String>,
433    pub trace_id: Option<String>,
434    pub last_seconds: Option<i64>,
435    pub limit: Option<u32>,
436}