lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0

//! Metrics export in various formats

use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use super::metrics::{ExportMetric, ExportValues, MetricsRegistry, parse_label_key};
use super::types::MetricType;

/// Export metrics in Prometheus text format
pub fn export_prometheus_format(registry: &MetricsRegistry) -> String {
    let mut output = String::new();
    let metrics = registry.get_all_for_export();

    for metric in metrics {
        // Write HELP line
        output.push_str(&format!("# HELP {} {}\n", metric.name, metric.help));

        // Write TYPE line
        output.push_str(&format!(
            "# TYPE {} {}\n",
            metric.name,
            metric.metric_type.prometheus_type()
        ));

        // Write values
        match metric.values {
            ExportValues::Counter(values) | ExportValues::Gauge(values) => {
                for (label_key, value) in values {
                    let labels = format_labels(&metric.label_names, &label_key);
                    if labels.is_empty() {
                        output.push_str(&format!("{} {}\n", metric.name, value));
                    } else {
                        output.push_str(&format!("{}{{{}}} {}\n", metric.name, labels, value));
                    }
                }
            }
            ExportValues::Histogram(histograms) => {
                for (label_key, hist) in histograms {
                    let base_labels = format_labels(&metric.label_names, &label_key);

                    // Write bucket values
                    for bucket in &hist.buckets {
                        let le_label = if base_labels.is_empty() {
                            format!("le=\"{}\"", bucket.le)
                        } else {
                            format!("{},le=\"{}\"", base_labels, bucket.le)
                        };
                        output.push_str(&format!(
                            "{}_bucket{{{}}} {}\n",
                            metric.name, le_label, bucket.count
                        ));
                    }

                    // Write +Inf bucket
                    let inf_label = if base_labels.is_empty() {
                        "le=\"+Inf\"".to_string()
                    } else {
                        format!("{},le=\"+Inf\"", base_labels)
                    };
                    output.push_str(&format!(
                        "{}_bucket{{{}}} {}\n",
                        metric.name, inf_label, hist.count
                    ));

                    // Write sum and count
                    if base_labels.is_empty() {
                        output.push_str(&format!("{}_sum {}\n", metric.name, hist.sum));
                        output.push_str(&format!("{}_count {}\n", metric.name, hist.count));
                    } else {
                        output.push_str(&format!(
                            "{}_sum{{{}}} {}\n",
                            metric.name, base_labels, hist.sum
                        ));
                        output.push_str(&format!(
                            "{}_count{{{}}} {}\n",
                            metric.name, base_labels, hist.count
                        ));
                    }
                }
            }
            ExportValues::Summary(summaries) => {
                for (label_key, summary) in summaries {
                    let base_labels = format_labels(&metric.label_names, &label_key);

                    // Write quantile values
                    for quantile in &summary.quantiles {
                        let q_label = if base_labels.is_empty() {
                            format!("quantile=\"{}\"", quantile.quantile)
                        } else {
                            format!("{},quantile=\"{}\"", base_labels, quantile.quantile)
                        };
                        output.push_str(&format!(
                            "{}{{{}}} {}\n",
                            metric.name, q_label, quantile.value
                        ));
                    }

                    // Write sum and count
                    if base_labels.is_empty() {
                        output.push_str(&format!("{}_sum {}\n", metric.name, summary.sum));
                        output.push_str(&format!("{}_count {}\n", metric.name, summary.count));
                    } else {
                        output.push_str(&format!(
                            "{}_sum{{{}}} {}\n",
                            metric.name, base_labels, summary.sum
                        ));
                        output.push_str(&format!(
                            "{}_count{{{}}} {}\n",
                            metric.name, base_labels, summary.count
                        ));
                    }
                }
            }
        }

        output.push('\n');
    }

    output
}

/// Export metrics in JSON format
pub fn export_json_format(registry: &MetricsRegistry) -> String {
    let metrics = registry.get_all_for_export();
    let mut output = String::new();

    output.push_str("{\n  \"metrics\": [\n");

    for (i, metric) in metrics.iter().enumerate() {
        output.push_str("    {\n");
        output.push_str(&format!("      \"name\": \"{}\",\n", metric.name));
        output.push_str(&format!(
            "      \"help\": \"{}\",\n",
            escape_json(&metric.help)
        ));
        output.push_str(&format!(
            "      \"type\": \"{}\",\n",
            metric.metric_type.prometheus_type()
        ));

        // Labels
        output.push_str("      \"labels\": [");
        for (j, label) in metric.label_names.iter().enumerate() {
            output.push_str(&format!("\"{}\"", label));
            if j < metric.label_names.len() - 1 {
                output.push_str(", ");
            }
        }
        output.push_str("],\n");

        // Values
        output.push_str("      \"values\": ");
        output.push_str(&format_values_json(&metric.values, &metric.label_names));
        output.push('\n');

        output.push_str("    }");
        if i < metrics.len() - 1 {
            output.push(',');
        }
        output.push('\n');
    }

    output.push_str("  ]\n}\n");
    output
}

/// Format labels for Prometheus output
fn format_labels(label_names: &[String], label_key: &str) -> String {
    if label_names.is_empty() || label_key.is_empty() {
        return String::new();
    }

    let values = parse_label_key(label_key);
    let pairs: Vec<String> = label_names
        .iter()
        .zip(values.iter())
        .map(|(name, value)| format!("{}=\"{}\"", name, escape_prometheus(value)))
        .collect();

    pairs.join(",")
}

/// Escape string for Prometheus format
fn escape_prometheus(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
}

/// Escape string for JSON format
fn escape_json(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Format metric values as JSON
fn format_values_json(values: &ExportValues, label_names: &[String]) -> String {
    match values {
        ExportValues::Counter(v) | ExportValues::Gauge(v) => {
            let mut output = String::from("[\n");
            let items: Vec<_> = v.iter().collect();
            for (i, (key, value)) in items.iter().enumerate() {
                output.push_str("        {\n");
                output.push_str(&format_label_json(label_names, key));
                output.push_str(&format!("          \"value\": {}\n", value));
                output.push_str("        }");
                if i < items.len() - 1 {
                    output.push(',');
                }
                output.push('\n');
            }
            output.push_str("      ]");
            output
        }
        ExportValues::Histogram(v) => {
            let mut output = String::from("[\n");
            let items: Vec<_> = v.iter().collect();
            for (i, (key, hist)) in items.iter().enumerate() {
                output.push_str("        {\n");
                output.push_str(&format_label_json(label_names, key));
                output.push_str(&format!("          \"sum\": {},\n", hist.sum));
                output.push_str(&format!("          \"count\": {},\n", hist.count));
                output.push_str("          \"buckets\": [");
                for (j, bucket) in hist.buckets.iter().enumerate() {
                    output.push_str(&format!(
                        "{{\"le\": {}, \"count\": {}}}",
                        bucket.le, bucket.count
                    ));
                    if j < hist.buckets.len() - 1 {
                        output.push_str(", ");
                    }
                }
                output.push_str("]\n");
                output.push_str("        }");
                if i < items.len() - 1 {
                    output.push(',');
                }
                output.push('\n');
            }
            output.push_str("      ]");
            output
        }
        ExportValues::Summary(v) => {
            let mut output = String::from("[\n");
            let items: Vec<_> = v.iter().collect();
            for (i, (key, summary)) in items.iter().enumerate() {
                output.push_str("        {\n");
                output.push_str(&format_label_json(label_names, key));
                output.push_str(&format!("          \"sum\": {},\n", summary.sum));
                output.push_str(&format!("          \"count\": {},\n", summary.count));
                output.push_str("          \"quantiles\": [");
                for (j, q) in summary.quantiles.iter().enumerate() {
                    output.push_str(&format!(
                        "{{\"quantile\": {}, \"value\": {}}}",
                        q.quantile, q.value
                    ));
                    if j < summary.quantiles.len() - 1 {
                        output.push_str(", ");
                    }
                }
                output.push_str("]\n");
                output.push_str("        }");
                if i < items.len() - 1 {
                    output.push(',');
                }
                output.push('\n');
            }
            output.push_str("      ]");
            output
        }
    }
}

/// Format labels as JSON
fn format_label_json(label_names: &[String], label_key: &str) -> String {
    if label_names.is_empty() || label_key.is_empty() {
        return String::new();
    }

    let values = parse_label_key(label_key);
    let mut output = String::from("          \"labels\": {");
    let pairs: Vec<String> = label_names
        .iter()
        .zip(values.iter())
        .map(|(name, value)| format!("\"{}\": \"{}\"", name, escape_json(value)))
        .collect();
    output.push_str(&pairs.join(", "));
    output.push_str("},\n");
    output
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::vec;

    #[test]
    fn test_escape_prometheus() {
        assert_eq!(escape_prometheus("hello"), "hello");
        assert_eq!(escape_prometheus("hello\"world"), "hello\\\"world");
        assert_eq!(escape_prometheus("line\nbreak"), "line\\nbreak");
    }

    #[test]
    fn test_escape_json() {
        assert_eq!(escape_json("hello"), "hello");
        assert_eq!(escape_json("tab\there"), "tab\\there");
    }

    #[test]
    fn test_format_labels() {
        let names = vec!["dataset".to_string(), "operation".to_string()];
        let key = "pool1\x00read";
        let result = format_labels(&names, key);
        assert_eq!(result, "dataset=\"pool1\",operation=\"read\"");
    }

    #[test]
    fn test_export_prometheus() {
        let mut registry = MetricsRegistry::new();
        registry
            .register_counter("test_total", "Test counter", &["label1"])
            .unwrap();

        let output = export_prometheus_format(&registry);
        assert!(output.contains("# HELP test_total Test counter"));
        assert!(output.contains("# TYPE test_total counter"));
    }
}