use crate::correlate::Trace;
use crate::event::{EventSource, EventType, SpanEvent};
use crate::normalize;
use crate::report::GreenSummary;
use crate::report::interpret::InterpretationLevel;
#[must_use]
pub fn make_test_green_summary(
total_io_ops: usize,
avoidable_io_ops: usize,
io_waste_ratio: f64,
) -> GreenSummary {
GreenSummary {
total_io_ops,
avoidable_io_ops,
io_waste_ratio,
io_waste_ratio_band: InterpretationLevel::for_waste_ratio(io_waste_ratio),
top_offenders: vec![],
co2: None,
regions: vec![],
transport_gco2: None,
}
}
pub fn make_sql_event(trace_id: &str, span_id: &str, target: &str, ts: &str) -> SpanEvent {
make_sql_event_with_duration(trace_id, span_id, target, ts, 800)
}
pub fn make_http_event(trace_id: &str, span_id: &str, target: &str, ts: &str) -> SpanEvent {
make_http_event_with_duration(trace_id, span_id, target, ts, 12000)
}
pub fn make_sql_event_with_duration(
trace_id: &str,
span_id: &str,
target: &str,
ts: &str,
duration_us: u64,
) -> SpanEvent {
SpanEvent {
timestamp: ts.to_string(),
trace_id: trace_id.to_string(),
span_id: span_id.to_string(),
parent_span_id: None,
service: "order-svc".to_string(),
cloud_region: None,
event_type: EventType::Sql,
operation: "SELECT".to_string(),
target: target.to_string(),
duration_us,
source: EventSource {
endpoint: "POST /api/orders/42/submit".to_string(),
method: "OrderService::create_order".to_string(),
},
status_code: None,
response_size_bytes: None,
code_function: None,
code_filepath: None,
code_lineno: None,
code_namespace: None,
}
}
pub fn make_http_event_with_duration(
trace_id: &str,
span_id: &str,
target: &str,
ts: &str,
duration_us: u64,
) -> SpanEvent {
SpanEvent {
timestamp: ts.to_string(),
trace_id: trace_id.to_string(),
span_id: span_id.to_string(),
parent_span_id: None,
service: "order-svc".to_string(),
cloud_region: None,
event_type: EventType::HttpOut,
operation: "GET".to_string(),
target: target.to_string(),
duration_us,
source: EventSource {
endpoint: "POST /api/orders/42/submit".to_string(),
method: "OrderService::create_order".to_string(),
},
status_code: Some(200),
response_size_bytes: None,
code_function: None,
code_filepath: None,
code_lineno: None,
code_namespace: None,
}
}
pub fn make_http_event_with_size(
trace_id: &str,
span_id: &str,
target: &str,
ts: &str,
response_size_bytes: Option<u64>,
) -> SpanEvent {
let mut event = make_http_event(trace_id, span_id, target, ts);
event.response_size_bytes = response_size_bytes;
event
}
pub fn make_redundant_events() -> Vec<SpanEvent> {
(1..=3_i32)
.map(|i| {
make_sql_event(
"trace-1",
&format!("span-{i}"),
"SELECT * FROM order_item WHERE order_id = 42",
&format!("2025-07-10T14:32:01.{:03}Z", i * 50),
)
})
.collect()
}
pub fn make_sql_series_events_with_stride(count: i32, stride_ms: i32) -> Vec<SpanEvent> {
(1..=count)
.map(|i| {
make_sql_event(
"trace-1",
&format!("span-{i}"),
&format!("SELECT * FROM order_item WHERE order_id = {i}"),
&format!("2025-07-10T14:32:01.{:03}Z", i * stride_ms),
)
})
.collect()
}
pub fn make_sql_series_events(count: i32) -> Vec<SpanEvent> {
make_sql_series_events_with_stride(count, 50)
}
pub fn make_n_plus_one_events() -> Vec<SpanEvent> {
make_sql_series_events(6)
}
pub fn make_finding(
finding_type: crate::detect::FindingType,
severity: crate::detect::Severity,
) -> crate::detect::Finding {
crate::detect::Finding {
finding_type,
severity,
trace_id: "trace-1".to_string(),
service: "order-svc".to_string(),
source_endpoint: "POST /api/orders/42/submit".to_string(),
pattern: crate::detect::Pattern {
template: "SELECT * FROM t WHERE id = ?".to_string(),
occurrences: 6,
window_ms: 200,
distinct_params: 6,
},
suggestion: "batch".to_string(),
first_timestamp: "2025-07-10T14:32:01.000Z".to_string(),
last_timestamp: "2025-07-10T14:32:01.250Z".to_string(),
green_impact: Some(crate::detect::GreenImpact {
estimated_extra_io_ops: 5,
io_intensity_score: 6.0,
io_intensity_band: InterpretationLevel::for_iis(6.0),
}),
confidence: crate::detect::Confidence::default(),
code_location: None,
suggested_fix: None,
}
}
pub fn make_trace(events: Vec<SpanEvent>) -> Trace {
assert!(!events.is_empty(), "make_trace requires at least one event");
let trace_id = events[0].trace_id.clone();
let spans = normalize::normalize_all(events);
Trace { trace_id, spans }
}
#[cfg(any(feature = "daemon", feature = "tempo"))]
pub async fn spawn_one_shot_server(
response_body: Vec<u8>,
) -> (String, tokio::task::JoinHandle<()>) {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let endpoint = format!("http://{addr}");
let handle = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 4096];
let _ = socket.read(&mut buf).await;
let _ = socket.write_all(&response_body).await;
let _ = socket.flush().await;
let _ = socket.shutdown().await;
});
(endpoint, handle)
}
#[must_use]
#[cfg(any(feature = "daemon", feature = "tempo"))]
pub fn http_200_text(content_type: &str, body: &str) -> Vec<u8> {
format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: {content_type}\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
\r\n\
{body}",
body.len()
)
.into_bytes()
}
#[must_use]
#[cfg(feature = "tempo")]
pub fn http_200_bytes(content_type: &str, body: &[u8]) -> Vec<u8> {
let headers = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: {content_type}\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
\r\n",
body.len()
);
let mut out = headers.into_bytes();
out.extend_from_slice(body);
out
}
#[must_use]
#[cfg(any(feature = "daemon", feature = "tempo"))]
pub fn http_status(code: u16, reason: &str) -> Vec<u8> {
format!(
"HTTP/1.1 {code} {reason}\r\n\
Content-Length: 0\r\n\
Connection: close\r\n\
\r\n"
)
.into_bytes()
}