use tracing::{Span, info_span};
pub fn call_span(provider: &str, model: &str, trace_id: &str) -> Span {
info_span!(
"backend.complete",
provider = provider,
model = model,
trace_id = trace_id,
)
}
pub fn stream_span(provider: &str, model: &str, trace_id: &str) -> Span {
info_span!(
"backend.stream",
provider = provider,
model = model,
trace_id = trace_id,
)
}
pub fn on_request_built(
max_tokens: Option<u32>,
temperature: Option<f64>,
n_messages: usize,
n_tools: usize,
) {
tracing::info!(
event = "request_built",
max_tokens = max_tokens.map(|v| v as i64).unwrap_or(-1),
temperature = temperature.unwrap_or(f64::NAN),
n_messages,
n_tools,
);
}
pub fn on_http_send(url: &str, body_size_bytes: usize) {
tracing::info!(event = "http_send", url, body_size_bytes);
}
pub fn on_http_recv(status_code: u16, body_size_bytes: usize, duration_ms: u64) {
tracing::info!(
event = "http_recv",
status_code,
body_size_bytes,
duration_ms,
);
}
pub fn on_retry_scheduled(attempt: u32, after_ms: u64, reason: &str) {
tracing::warn!(
event = "retry_scheduled",
attempt,
after_ms,
reason,
);
}
pub fn on_parsed_response(input_tokens: u32, output_tokens: u32, finish_reason: &str) {
tracing::info!(
event = "parsed_response",
input_tokens,
output_tokens,
finish_reason,
);
}
pub fn on_complete(total_duration_ms: u64, retry_count: u32, success: bool) {
tracing::info!(
event = "complete",
total_duration_ms,
retry_count,
success,
);
}
pub fn on_error(category: &str, status: Option<u16>, message: &str) {
tracing::error!(
event = "error",
category,
status = status.map(|v| v as i64).unwrap_or(-1),
message,
);
}
#[cfg(test)]
mod tests {
use super::*;
use tracing::Instrument;
#[test]
fn call_span_constructs_with_canonical_fields() {
let span = call_span("anthropic", "claude-x", "trace-1");
let _enter = span.enter();
assert_eq!(span.metadata().map(|m| m.name()), Some("backend.complete"));
}
#[test]
fn stream_span_distinct_from_call_span() {
let s = stream_span("openai", "gpt-x", "trace-2");
assert_eq!(s.metadata().map(|m| m.name()), Some("backend.stream"));
}
#[tokio::test]
async fn helpers_emit_without_panicking_inside_a_span() {
let span = call_span("kimi", "kimi-k2.6", "trace-3");
async {
on_request_built(Some(2048), Some(0.7), 4, 0);
on_http_send("https://api.moonshot.ai/v1/chat/completions", 1024);
on_http_recv(200, 2048, 150);
on_parsed_response(120, 80, "stop");
on_complete(150, 0, true);
}
.instrument(span)
.await;
}
#[tokio::test]
async fn retry_and_error_helpers_emit() {
let span = call_span("openai", "gpt-x", "trace-4");
async {
on_retry_scheduled(0, 1000, "429");
on_error("rate_limit", Some(429), "rate limited");
}
.instrument(span)
.await;
}
#[test]
fn request_built_handles_unset_temperature() {
on_request_built(None, None, 0, 0);
}
}