assay_core/
otel.rs

1use crate::model::TestResultRow;
2
3#[derive(Debug, Clone, Default)]
4pub struct OTelConfig {
5    pub jsonl_path: Option<std::path::PathBuf>,
6    pub redact_prompts: bool,
7}
8
9pub fn export_jsonl(
10    cfg: &OTelConfig,
11    _suite: &str,
12    results: &[TestResultRow],
13) -> anyhow::Result<()> {
14    let Some(path) = &cfg.jsonl_path else {
15        return Ok(());
16    };
17    let mut f = std::fs::OpenOptions::new()
18        .create(true)
19        .append(true)
20        .open(path)?;
21    for r in results {
22        // GenAI Semantic Conventions (simplified for MVP)
23        // https://opentelemetry.io/docs/specs/semconv/gen-ai/
24        let row = serde_json::json!({
25            "timestamp": chrono::Utc::now().to_rfc3339(),
26            "attributes": {
27                "gen_ai.system": "assay",
28                "gen_ai.request.model": "unknown", // can be enriched if we track it better
29                "gen_ai.response.completion_tokens": 0, // placeholder
30                "assay.test_id": r.test_id,
31                "assay.status": format!("{:?}", r.status),
32                "assay.score": r.score,
33                "assay.cached": r.cached,
34                "assay.duration_ms": r.duration_ms,
35            }
36        });
37
38        // Use details/meta if available to populate standard fields
39        // checking details logic would go here
40
41        use std::io::Write;
42        writeln!(f, "{}", row)?;
43    }
44    Ok(())
45}