pub mod window;
use crate::normalize::NormalizedEvent;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Trace {
pub trace_id: String,
pub spans: Vec<NormalizedEvent>,
}
#[must_use]
pub fn correlate(events: Vec<NormalizedEvent>) -> Vec<Trace> {
let estimated_traces = (events.len() / 10).max(events.len().min(1));
let mut map: HashMap<String, Vec<NormalizedEvent>> = HashMap::with_capacity(estimated_traces);
for event in events {
if let Some(vec) = map.get_mut(event.event.trace_id.as_str()) {
vec.push(event);
} else {
let key = event.event.trace_id.clone();
map.insert(key, vec![event]);
}
}
map.into_iter()
.map(|(trace_id, spans)| Trace { trace_id, spans })
.collect()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::event::{EventSource, EventType, SpanEvent};
use crate::normalize;
fn make_event(trace_id: &str, span_id: &str) -> SpanEvent {
SpanEvent {
timestamp: "2025-07-10T14:32:01.123Z".to_string(),
trace_id: trace_id.to_string(),
span_id: span_id.to_string(),
parent_span_id: None,
service: Arc::from("test"),
cloud_region: None,
event_type: EventType::Sql,
operation: "SELECT".to_string(),
target: "SELECT 1".to_string(),
duration_us: 100,
source: EventSource {
endpoint: "GET /test".to_string(),
method: "Test::test".to_string(),
},
status_code: None,
response_size_bytes: None,
code_function: None,
code_filepath: None,
code_lineno: None,
code_namespace: None,
instrumentation_scopes: Vec::new(),
}
}
#[test]
fn empty_input_gives_empty_output() {
let traces = correlate(vec![]);
assert!(traces.is_empty());
}
#[test]
fn groups_spans_by_trace_id() {
let events = vec![
make_event("trace-1", "span-1"),
make_event("trace-2", "span-2"),
make_event("trace-1", "span-3"),
];
let normalized = normalize::normalize_all(events);
let traces = correlate(normalized);
assert_eq!(traces.len(), 2);
let t1 = traces.iter().find(|t| t.trace_id == "trace-1").unwrap();
assert_eq!(t1.spans.len(), 2);
let t2 = traces.iter().find(|t| t.trace_id == "trace-2").unwrap();
assert_eq!(t2.spans.len(), 1);
}
}