use std::collections::BTreeMap;
pub const DEFAULT_LOOKBACK_MS: i64 = 300_000;
pub type Labels = BTreeMap<String, String>;
#[derive(Debug, Clone, Copy)]
pub struct Sample {
pub timestamp_ms: i64,
pub value: f64,
}
#[derive(Debug, Clone)]
pub struct Series {
pub labels: Labels,
pub samples: Vec<Sample>,
}
impl Series {
pub fn metric_name(&self) -> &str {
self.labels.get("__name__").map_or("", |s| s.as_str())
}
}
#[derive(Debug, Clone)]
pub struct InstantSample {
pub labels: Labels,
pub value: f64,
pub timestamp_ms: i64,
}
#[derive(Debug, Clone)]
pub struct RangeSeries {
pub labels: Labels,
pub samples: Vec<Sample>,
}
#[derive(Debug, Clone)]
pub enum Value {
Scalar(f64, i64),
Vector(Vec<InstantSample>),
Matrix(Vec<RangeSeries>),
}
impl Value {
pub fn result_type(&self) -> &'static str {
match self {
Self::Scalar(..) => "scalar",
Self::Vector(_) => "vector",
Self::Matrix(_) => "matrix",
}
}
}
#[derive(Debug)]
pub struct PromResult {
pub status: &'static str,
pub data: Value,
pub error: Option<String>,
pub error_type: Option<String>,
}
impl PromResult {
pub fn success(data: Value) -> Self {
Self {
status: "success",
data,
error: None,
error_type: None,
}
}
pub fn error(err_type: &str, message: String) -> Self {
Self {
status: "error",
data: Value::Vector(vec![]),
error: Some(message),
error_type: Some(err_type.to_string()),
}
}
pub fn to_json(&self) -> String {
let mut out = String::with_capacity(1024);
out.push_str(r#"{"status":""#);
out.push_str(self.status);
out.push('"');
if let Some(ref e) = self.error {
out.push_str(r#","errorType":""#);
out.push_str(self.error_type.as_deref().unwrap_or("internal"));
out.push_str(r#"","error":""#);
json_escape(&mut out, e);
out.push('"');
}
out.push_str(r#","data":{"resultType":""#);
out.push_str(self.data.result_type());
out.push_str(r#"","result":"#);
write_value_json(&mut out, &self.data);
out.push_str("}}");
out
}
}
fn write_value_json(out: &mut String, val: &Value) {
match val {
Value::Scalar(v, ts) => {
out.push_str(&format!("[{},\"{}\"]", *ts as f64 / 1000.0, format_f64(*v)));
}
Value::Vector(samples) => {
out.push('[');
for (i, s) in samples.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(r#"{"metric":"#);
write_labels_json(out, &s.labels);
out.push_str(r#","value":["#);
out.push_str(&format!("{}", s.timestamp_ms as f64 / 1000.0));
out.push_str(r#",""#);
out.push_str(&format_f64(s.value));
out.push_str("\"]}");
}
out.push(']');
}
Value::Matrix(series) => {
out.push('[');
for (i, s) in series.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(r#"{"metric":"#);
write_labels_json(out, &s.labels);
out.push_str(r#","values":["#);
for (j, sample) in s.samples.iter().enumerate() {
if j > 0 {
out.push(',');
}
out.push('[');
out.push_str(&format!("{}", sample.timestamp_ms as f64 / 1000.0));
out.push_str(",\"");
out.push_str(&format_f64(sample.value));
out.push_str("\"]");
}
out.push_str("]}");
}
out.push(']');
}
}
}
pub fn write_labels_json(out: &mut String, labels: &Labels) {
out.push('{');
for (i, (k, v)) in labels.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('"');
json_escape(out, k);
out.push_str("\":\"");
json_escape(out, v);
out.push('"');
}
out.push('}');
}
pub fn json_escape(out: &mut String, s: &str) {
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c => out.push(c),
}
}
}
fn format_f64(v: f64) -> String {
if v.is_nan() {
"NaN".to_string()
} else if v.is_infinite() {
if v > 0.0 { "+Inf" } else { "-Inf" }.to_string()
} else if v == v.floor() && v.abs() < 1e15 {
format!("{v:.0}")
} else {
format!("{v}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalar_json() {
let r = PromResult::success(Value::Scalar(42.0, 1000));
let json = r.to_json();
assert!(json.contains(r#""resultType":"scalar""#));
assert!(json.contains(r#""status":"success""#));
}
#[test]
fn vector_json() {
let mut labels = Labels::new();
labels.insert("__name__".into(), "up".into());
let r = PromResult::success(Value::Vector(vec![InstantSample {
labels,
value: 1.0,
timestamp_ms: 1000,
}]));
let json = r.to_json();
assert!(json.contains(r#""__name__":"up""#));
assert!(json.contains(r#""1""#)); }
#[test]
fn error_json() {
let r = PromResult::error("bad_data", "parse error".into());
let json = r.to_json();
assert!(json.contains(r#""status":"error""#));
assert!(json.contains(r#""errorType":"bad_data""#));
}
}