polaris_dashboard 0.1.3

Opinionated read-only dashboard for Polaris sessions.
Documentation
//! Minimal deserialization of the OTLP/HTTP **JSON** trace export request
//! (`opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest` in its
//! protobuf-JSON mapping).
//!
//! This is deliberately permissive: every emitter — an arbitrary
//! microservice, or Polaris itself — sends the same envelope, and we copy
//! whatever attributes arrive into generic bags downstream. Nothing here is
//! source-specific. Only the fields the dashboard actually renders are
//! modelled; unknown fields are ignored by serde.
//!
//! Per the OTLP/JSON spec, `trace_id`/`span_id` are hex strings and 64-bit
//! integers (`*UnixNano`, `intValue`) are encoded as strings; we tolerate raw
//! JSON numbers as well for non-conforming senders.

use serde::Deserialize;
use serde_json::{Map, Value};

/// Top-level OTLP/HTTP trace export request body.
#[derive(Debug, Default, Deserialize)]
pub struct ExportTraceServiceRequest {
    #[serde(default, rename = "resourceSpans")]
    pub resource_spans: Vec<ResourceSpans>,
}

#[derive(Debug, Default, Deserialize)]
pub struct ResourceSpans {
    #[serde(default)]
    pub resource: Resource,
    #[serde(default, rename = "scopeSpans", alias = "instrumentationLibrarySpans")]
    pub scope_spans: Vec<ScopeSpans>,
}

#[derive(Debug, Default, Deserialize)]
pub struct Resource {
    #[serde(default)]
    pub attributes: Vec<KeyValue>,
}

#[derive(Debug, Default, Deserialize)]
pub struct ScopeSpans {
    #[serde(default, alias = "instrumentationLibrary")]
    pub scope: Scope,
    #[serde(default)]
    pub spans: Vec<Span>,
}

#[derive(Debug, Default, Deserialize)]
pub struct Scope {
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub version: Option<String>,
}

#[derive(Debug, Default, Deserialize)]
pub struct Span {
    #[serde(default, rename = "traceId")]
    pub trace_id: String,
    #[serde(default, rename = "spanId")]
    pub span_id: String,
    #[serde(default, rename = "parentSpanId")]
    pub parent_span_id: Option<String>,
    #[serde(default)]
    pub name: String,
    #[serde(default, rename = "startTimeUnixNano")]
    pub start_time_unix_nano: Option<Value>,
    #[serde(default, rename = "endTimeUnixNano")]
    pub end_time_unix_nano: Option<Value>,
    #[serde(default)]
    pub attributes: Vec<KeyValue>,
    #[serde(default)]
    pub status: Status,
    #[serde(default)]
    pub events: Vec<Event>,
}

#[derive(Debug, Default, Deserialize)]
pub struct Status {
    /// `0` UNSET, `1` OK, `2` ERROR — sent as a number or the string form
    /// (`"STATUS_CODE_ERROR"`) depending on the exporter.
    #[serde(default)]
    pub code: Option<Value>,
    #[serde(default)]
    pub message: Option<String>,
}

impl Status {
    /// Whether this status represents an error (`code == 2`).
    pub fn is_error(&self) -> bool {
        match &self.code {
            Some(Value::Number(n)) => n.as_i64() == Some(2),
            Some(Value::String(s)) => {
                s == "2"
                    || s.eq_ignore_ascii_case("STATUS_CODE_ERROR")
                    || s.eq_ignore_ascii_case("ERROR")
            }
            _ => false,
        }
    }
}

#[derive(Debug, Default, Deserialize)]
pub struct Event {
    #[serde(default)]
    pub name: String,
    #[serde(default, rename = "timeUnixNano")]
    pub time_unix_nano: Option<Value>,
    #[serde(default)]
    pub attributes: Vec<KeyValue>,
}

#[derive(Debug, Default, Deserialize)]
pub struct KeyValue {
    #[serde(default)]
    pub key: String,
    #[serde(default)]
    pub value: AnyValue,
}

/// OTLP `AnyValue` — exactly one variant is populated.
#[derive(Debug, Default, Deserialize)]
pub struct AnyValue {
    #[serde(default, rename = "stringValue")]
    pub string_value: Option<String>,
    #[serde(default, rename = "intValue")]
    pub int_value: Option<Value>,
    #[serde(default, rename = "doubleValue")]
    pub double_value: Option<f64>,
    #[serde(default, rename = "boolValue")]
    pub bool_value: Option<bool>,
    #[serde(default, rename = "arrayValue")]
    pub array_value: Option<ArrayValue>,
    #[serde(default, rename = "kvlistValue")]
    pub kvlist_value: Option<KvList>,
    #[serde(default, rename = "bytesValue")]
    pub bytes_value: Option<String>,
}

#[derive(Debug, Default, Deserialize)]
pub struct ArrayValue {
    #[serde(default)]
    pub values: Vec<AnyValue>,
}

#[derive(Debug, Default, Deserialize)]
pub struct KvList {
    #[serde(default)]
    pub values: Vec<KeyValue>,
}

impl AnyValue {
    /// Collapse the populated variant into a plain JSON value, preserving the
    /// natural type so numbers stay numbers and the frontend's token/cost
    /// readers work without coercion. `intValue` strings are parsed back to
    /// integers where possible.
    pub fn to_json(&self) -> Value {
        if let Some(s) = &self.string_value {
            return Value::String(s.clone());
        }
        if let Some(v) = &self.int_value {
            return match v {
                Value::String(s) => s
                    .parse::<i64>()
                    .map(Value::from)
                    .unwrap_or_else(|_| Value::String(s.clone())),
                other => other.clone(),
            };
        }
        if let Some(d) = self.double_value {
            return serde_json::Number::from_f64(d).map_or(Value::Null, Value::Number);
        }
        if let Some(b) = self.bool_value {
            return Value::Bool(b);
        }
        if let Some(arr) = &self.array_value {
            return Value::Array(arr.values.iter().map(AnyValue::to_json).collect());
        }
        if let Some(kv) = &self.kvlist_value {
            return Value::Object(key_values_to_map(&kv.values));
        }
        if let Some(b) = &self.bytes_value {
            return Value::String(b.clone());
        }
        Value::Null
    }

    /// Stringify for label maps (`Record<string, string>`): plain strings pass
    /// through verbatim, everything else is JSON-encoded.
    pub fn to_label_string(&self) -> String {
        match self.to_json() {
            Value::String(s) => s,
            other => other.to_string(),
        }
    }
}

/// Convert a list of OTLP key/values into a JSON object.
pub fn key_values_to_map(kvs: &[KeyValue]) -> Map<String, Value> {
    let mut map = Map::new();
    for kv in kvs {
        if kv.key.is_empty() {
            continue;
        }
        map.insert(kv.key.clone(), kv.value.to_json());
    }
    map
}