tambo 0.13.2

Install and prove the whole AI knowledge-tool kit
Documentation
use std::{
    collections::{BTreeMap, BTreeSet},
    fs,
    path::Path,
};

use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::json;

use crate::model::{Plan, ToolName};

const CONTRACT_VERSION: u32 = 1;

#[derive(Debug, Deserialize)]
pub struct Targets {
    pub targets: BTreeMap<String, String>,
}

#[derive(Debug, Serialize)]
struct Contract<'a> {
    contract_version: u32,
    tool: &'a str,
    metrics_path: Option<&'a str>,
    required_metrics: &'a [&'a str],
    unavailable_reason: Option<&'a str>,
}

fn contract(tool: ToolName) -> Contract<'static> {
    match tool {
        ToolName::Quipu => Contract {
            contract_version: CONTRACT_VERSION,
            tool: "quipu",
            metrics_path: Some("/metrics"),
            required_metrics: &["quipu_graph_facts", "quipu_http_requests_total"],
            unavailable_reason: None,
        },
        ToolName::Bobbin => Contract {
            contract_version: CONTRACT_VERSION,
            tool: "bobbin",
            metrics_path: Some("/metrics"),
            required_metrics: &["bobbin_up", "bobbin_index_chunks_total"],
            unavailable_reason: None,
        },
        ToolName::Camayoc => unavailable("camayoc", "no standalone metrics exporter"),
        ToolName::Yupana => unavailable("yupana", "no metrics endpoint in the pinned release"),
        ToolName::DesirePath => unavailable(
            "desire-path",
            "no metrics endpoint in the pinned source revision",
        ),
        ToolName::Member(name) => unavailable(name, "a stack member declares no metrics contract"),
    }
}

fn unavailable(tool: &'static str, reason: &'static str) -> Contract<'static> {
    Contract {
        contract_version: CONTRACT_VERSION,
        tool,
        metrics_path: None,
        required_metrics: &[],
        unavailable_reason: Some(reason),
    }
}

fn validate_target(tool: &str, target: &str) -> Result<()> {
    if target.is_empty()
        || target.contains('/')
        || target.contains(char::is_whitespace)
        || !target
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | ':' | '-' | '_' | '[' | ']'))
    {
        bail!("invalid Prometheus target for {tool}: {target}");
    }
    Ok(())
}

pub fn render(plan: &Plan, targets_path: &Path, output: &Path) -> Result<()> {
    let targets: Targets = toml::from_str(
        &fs::read_to_string(targets_path)
            .with_context(|| format!("read observability targets {}", targets_path.display()))?,
    )
    .context("parse observability targets")?;
    let contracts: Vec<_> = plan.tools.iter().copied().map(contract).collect();

    for item in &contracts {
        match item.metrics_path {
            Some(_) => {
                let target = targets
                    .targets
                    .get(item.tool)
                    .with_context(|| format!("missing metrics target for {}", item.tool))?;
                validate_target(item.tool, target)?;
            }
            None if targets.targets.contains_key(item.tool) => {
                bail!(
                    "{} has a target but its versioned contract has no exporter",
                    item.tool
                )
            }
            None => {}
        }
    }
    for name in targets.targets.keys() {
        if !contracts.iter().any(|item| item.tool == name) {
            bail!("metrics target {name} is not selected by the reviewed plan");
        }
    }

    fs::create_dir_all(output)?;
    fs::write(
        output.join("contracts.json"),
        serde_json::to_vec_pretty(&json!({"schema_version": 1, "contracts": contracts}))?,
    )?;

    let observable: Vec<_> = contracts
        .iter()
        .filter(|item| item.metrics_path.is_some())
        .collect();
    let mut prometheus =
        String::from("# Generated by Tambo; review before deployment.\nscrape_configs:\n");
    let mut alerts = String::from("# Generated by Tambo; review before deployment.\ngroups:\n  - name: tambo-starter\n    rules:\n");
    let mut panels = Vec::new();
    for (index, item) in observable.iter().enumerate() {
        let target = &targets.targets[item.tool];
        prometheus.push_str(&format!(
            "  - job_name: 'tambo-{}'\n    metrics_path: '{}'\n    static_configs:\n      - targets: ['{}']\n",
            item.tool,
            item.metrics_path.unwrap(),
            target
        ));
        let metric = item.required_metrics[0];
        alerts.push_str(&format!(
            "      - alert: Tambo{}MetricsAbsent\n        expr: absent({})\n        for: 10m\n        labels:\n          severity: warning\n        annotations:\n          summary: '{} metrics contract is not observable'\n",
            title(item.tool), metric, item.tool
        ));
        panels.push(json!({
            "id": index + 1,
            "title": format!("{} contract signal", title(item.tool)),
            "type": "timeseries",
            "targets": [{"expr": metric, "refId": "A"}]
        }));
    }
    fs::write(output.join("prometheus.yml"), prometheus)?;
    fs::write(output.join("alerts.yml"), alerts)?;
    fs::write(
        output.join("dashboard.json"),
        serde_json::to_vec_pretty(&json!({
            "schemaVersion": 39,
            "title": "Tambo selected corpus",
            "panels": panels
        }))?,
    )?;
    validate(output)?;
    Ok(())
}

pub fn validate(output: &Path) -> Result<()> {
    let contracts: serde_json::Value =
        serde_json::from_slice(&fs::read(output.join("contracts.json"))?)?;
    let entries = contracts["contracts"]
        .as_array()
        .context("contracts.json omitted contracts array")?;
    let mut metrics = BTreeSet::new();
    let mut observable = 0;
    for entry in entries {
        if entry["contract_version"].as_u64() != Some(CONTRACT_VERSION.into()) {
            bail!("unsupported or missing observability contract version");
        }
        if !entry["required_metrics"]
            .as_array()
            .context("contract omitted required_metrics")?
            .is_empty()
        {
            observable += 1;
        }
        for metric in entry["required_metrics"].as_array().unwrap() {
            metrics.insert(
                metric
                    .as_str()
                    .context("contract metric is not a string")?
                    .to_owned(),
            );
        }
    }
    let prometheus = fs::read_to_string(output.join("prometheus.yml"))?;
    if prometheus.matches("job_name:").count() != observable {
        bail!("scrape config does not cover every observable contract");
    }
    let alerts = fs::read_to_string(output.join("alerts.yml"))?;
    for line in alerts
        .lines()
        .filter(|line| line.trim_start().starts_with("expr: absent("))
    {
        let metric = line
            .trim()
            .strip_prefix("expr: absent(")
            .and_then(|value| value.strip_suffix(')'))
            .context("malformed generated alert expression")?;
        if !metrics.contains(metric) {
            bail!("alert references uncovered metric {metric}");
        }
    }
    let dashboard: serde_json::Value =
        serde_json::from_slice(&fs::read(output.join("dashboard.json"))?)?;
    for panel in dashboard["panels"]
        .as_array()
        .context("dashboard omitted panels")?
    {
        let metric = panel["targets"][0]["expr"]
            .as_str()
            .context("dashboard panel omitted expression")?;
        if !metrics.contains(metric) {
            bail!("dashboard references uncovered metric {metric}");
        }
    }
    Ok(())
}

fn title(name: &str) -> String {
    name.split(['-', '_'])
        .map(|part| {
            let mut chars = part.chars();
            chars
                .next()
                .map(|first| first.to_uppercase().collect::<String>() + chars.as_str())
                .unwrap_or_default()
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::Profile;

    #[test]
    fn render_is_deterministic_and_covers_every_selected_tool() {
        let root = tempfile::tempdir().unwrap();
        let targets = root.path().join("targets.toml");
        fs::write(
            &targets,
            "[targets]\nquipu='127.0.0.1:8080'\nbobbin='127.0.0.1:3030'\n",
        )
        .unwrap();
        let first = root.path().join("first");
        let second = root.path().join("second");
        let plan = Plan::for_profile(Profile::Everything);
        render(&plan, &targets, &first).unwrap();
        render(&plan, &targets, &second).unwrap();
        for name in [
            "contracts.json",
            "prometheus.yml",
            "alerts.yml",
            "dashboard.json",
        ] {
            assert_eq!(
                fs::read(first.join(name)).unwrap(),
                fs::read(second.join(name)).unwrap()
            );
        }
        let contracts = fs::read_to_string(first.join("contracts.json")).unwrap();
        for tool in ["quipu", "camayoc", "bobbin", "yupana", "desire-path"] {
            assert!(contracts.contains(tool));
        }
    }

    #[test]
    fn missing_or_malformed_contract_targets_fail() {
        let root = tempfile::tempdir().unwrap();
        let targets = root.path().join("targets.toml");
        let plan = Plan::for_profile(Profile::Retrieval);
        fs::write(&targets, "[targets]\nquipu='127.0.0.1:8080'\n").unwrap();
        assert!(render(&plan, &targets, &root.path().join("missing")).is_err());
        fs::write(
            &targets,
            "[targets]\nquipu='http://host'\nbobbin='127.0.0.1:3030'\n",
        )
        .unwrap();
        assert!(render(&plan, &targets, &root.path().join("malformed")).is_err());
    }

    #[test]
    fn uncovered_dashboard_metric_fails_validation() {
        let root = tempfile::tempdir().unwrap();
        let targets = root.path().join("targets.toml");
        let output = root.path().join("output");
        fs::write(
            &targets,
            "[targets]\nquipu='127.0.0.1:8080'\nbobbin='127.0.0.1:3030'\n",
        )
        .unwrap();
        render(&Plan::for_profile(Profile::Retrieval), &targets, &output).unwrap();
        let path = output.join("dashboard.json");
        let mut dashboard: serde_json::Value =
            serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
        dashboard["panels"][0]["targets"][0]["expr"] = json!("invented_metric");
        fs::write(&path, serde_json::to_vec_pretty(&dashboard).unwrap()).unwrap();
        assert!(validate(&output).is_err());
    }
}