pub mod http;
pub mod sql;
use std::sync::Arc;
use crate::event::{EventType, MAX_ID_LENGTH, SpanEvent, sanitize_id};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NormalizedEvent {
pub event: SpanEvent,
pub template: Arc<str>,
pub params: Vec<String>,
}
#[must_use]
pub fn normalize(mut event: SpanEvent) -> NormalizedEvent {
if event.trace_id.len() > MAX_ID_LENGTH {
event.trace_id = sanitize_id(&event.trace_id);
}
if event.span_id.len() > MAX_ID_LENGTH {
event.span_id = sanitize_id(&event.span_id);
}
if let Some(ref pid) = event.parent_span_id
&& pid.len() > MAX_ID_LENGTH
{
event.parent_span_id = Some(sanitize_id(pid));
}
match event.event_type {
EventType::Sql => {
let result = sql::normalize_sql(&event.target);
NormalizedEvent {
event,
template: Arc::from(result.template),
params: result.params,
}
}
EventType::HttpOut => {
let result = http::normalize_http(&event.operation, &event.target);
NormalizedEvent {
event,
template: Arc::from(result.template),
params: result.params,
}
}
}
}
#[must_use]
pub fn normalize_all(events: Vec<SpanEvent>) -> Vec<NormalizedEvent> {
events.into_iter().map(normalize).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{EventSource, EventType, SpanEvent};
fn make_sql_event(target: &str) -> SpanEvent {
SpanEvent {
timestamp: "2025-07-10T14:32:01.123Z".to_string(),
trace_id: "trace-1".to_string(),
span_id: "span-1".to_string(),
parent_span_id: None,
service: Arc::from("test"),
cloud_region: None,
event_type: EventType::Sql,
operation: "SELECT".to_string(),
target: target.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(),
}
}
fn make_http_event(method: &str, target: &str) -> SpanEvent {
SpanEvent {
timestamp: "2025-07-10T14:32:01.123Z".to_string(),
trace_id: "trace-1".to_string(),
span_id: "span-1".to_string(),
parent_span_id: None,
service: Arc::from("test"),
cloud_region: None,
event_type: EventType::HttpOut,
operation: method.to_string(),
target: target.to_string(),
duration_us: 100,
source: EventSource {
endpoint: "GET /test".to_string(),
method: "Test::test".to_string(),
},
status_code: Some(200),
response_size_bytes: None,
code_function: None,
code_filepath: None,
code_lineno: None,
code_namespace: None,
instrumentation_scopes: Vec::new(),
}
}
#[test]
fn normalize_dispatches_sql() {
let event = make_sql_event("SELECT * FROM users WHERE id = 42");
let normalized = normalize(event);
assert_eq!(&*normalized.template, "SELECT * FROM users WHERE id = ?");
assert_eq!(normalized.params, vec!["42"]);
}
#[test]
fn normalize_dispatches_http() {
let event = make_http_event("GET", "/api/users/42");
let normalized = normalize(event);
assert_eq!(&*normalized.template, "GET /api/users/{id}");
}
#[test]
fn normalize_all_processes_batch() {
let events = vec![
make_sql_event("SELECT 1"),
make_http_event("POST", "/api/orders/99/submit"),
];
let normalized = normalize_all(events);
assert_eq!(normalized.len(), 2);
assert_eq!(&*normalized[0].template, "SELECT ?");
assert_eq!(&*normalized[1].template, "POST /api/orders/{id}/submit");
}
#[test]
fn normalize_truncates_oversized_trace_id() {
let mut event = make_sql_event("SELECT 1");
event.trace_id = "x".repeat(200);
event.span_id = "y".repeat(200);
event.parent_span_id = Some("z".repeat(200));
let normalized = normalize(event);
assert_eq!(normalized.event.trace_id.len(), MAX_ID_LENGTH);
assert_eq!(normalized.event.span_id.len(), MAX_ID_LENGTH);
assert_eq!(
normalized.event.parent_span_id.unwrap().len(),
MAX_ID_LENGTH
);
}
#[test]
fn normalize_preserves_normal_ids() {
let event = make_sql_event("SELECT 1");
let original_trace = event.trace_id.clone();
let normalized = normalize(event);
assert_eq!(normalized.event.trace_id, original_trace);
}
}