use std::collections::BTreeMap;
use rustev_contract::redaction::{Action, FieldClass, RedactionPolicy};
pub mod name {
pub const SCREENING: &str = "rustev.screening";
pub const SCREENING_UNITS: &str = "rustev.screening.units";
pub const DECISIONS: &str = "rustev.decisions";
pub const UNRESOLVED: &str = "rustev.unresolved";
pub const FALLBACKS: &str = "rustev.fallbacks";
pub const DECISION_DURATION: &str = "rustev.decision.duration";
pub const ATTEMPT_DURATION: &str = "rustev.attempt.duration";
pub const DECISION_COST: &str = "rustev.decision.cost";
pub const TOKEN_USAGE: &str = "gen_ai.client.token.usage";
pub const OPERATION_DURATION: &str = "gen_ai.client.operation.duration";
pub const ADMISSION_REJECTED: &str = "rustev.admission.rejected";
pub const EVIDENCE_SINK: &str = "rustev.evidence.sink";
pub const TELEMETRY_LOST: &str = "rustev.telemetry.lost";
pub const SAMPLED_OUT: &str = "rustev.telemetry.sampled_out";
}
pub const BOUNDARIES: [f64; 15] = [
0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, 5000.0, 7500.0,
10000.0,
];
pub type Attrs = Vec<(String, String)>;
#[derive(Debug, Clone, PartialEq)]
pub struct Histogram {
pub count: u64,
pub sum: f64,
pub min: f64,
pub max: f64,
pub buckets: Vec<u64>,
}
impl Default for Histogram {
fn default() -> Self {
Histogram {
count: 0,
sum: 0.0,
min: f64::INFINITY,
max: f64::NEG_INFINITY,
buckets: vec![0; BOUNDARIES.len() + 1],
}
}
}
impl Histogram {
pub fn record(&mut self, v: f64) {
self.count += 1;
self.sum += v;
self.min = self.min.min(v);
self.max = self.max.max(v);
let i = BOUNDARIES
.iter()
.position(|b| v <= *b)
.unwrap_or(BOUNDARIES.len());
self.buckets[i] += 1;
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Points {
Counter(BTreeMap<Attrs, u64>),
Histogram(BTreeMap<Attrs, Histogram>),
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MetricsSnapshot {
pub instruments: BTreeMap<&'static str, Points>,
}
impl MetricsSnapshot {
pub fn counter(&self, instrument: &str, attrs: &[(&str, &str)]) -> u64 {
let key: Attrs = attrs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
match self.instruments.get(instrument) {
Some(Points::Counter(m)) => m.get(&key).copied().unwrap_or(0),
_ => 0,
}
}
pub fn counter_total(&self, instrument: &str) -> u64 {
match self.instruments.get(instrument) {
Some(Points::Counter(m)) => m.values().sum(),
_ => 0,
}
}
}
#[derive(Debug, Default)]
pub struct Instruments {
counters: BTreeMap<&'static str, BTreeMap<Attrs, u64>>,
histograms: BTreeMap<&'static str, BTreeMap<Attrs, Histogram>>,
}
pub struct AttrSet<'p> {
policy: &'p RedactionPolicy,
attrs: Attrs,
}
impl<'p> AttrSet<'p> {
pub fn new(policy: &'p RedactionPolicy) -> Self {
AttrSet {
policy,
attrs: vec![],
}
}
pub fn with(mut self, key: &str, field: &str, class: FieldClass, value: &str) -> Self {
let dimension = matches!(
class,
FieldClass::Enum | FieldClass::PlanMetadata | FieldClass::ModelIdentity
);
if dimension && self.policy.action(field, class) == Action::Allow {
self.attrs.push((key.to_string(), value.to_string()));
}
self
}
pub fn done(mut self) -> Attrs {
self.attrs.sort();
self.attrs
}
}
impl Instruments {
pub fn add(&mut self, instrument: &'static str, attrs: Attrs, n: u64) {
*self
.counters
.entry(instrument)
.or_default()
.entry(attrs)
.or_default() += n;
}
pub fn record(&mut self, instrument: &'static str, attrs: Attrs, v: f64) {
self.histograms
.entry(instrument)
.or_default()
.entry(attrs)
.or_default()
.record(v);
}
pub fn snapshot(&self) -> MetricsSnapshot {
let mut s = MetricsSnapshot::default();
for (k, v) in &self.counters {
s.instruments.insert(k, Points::Counter(v.clone()));
}
for (k, v) in &self.histograms {
s.instruments.insert(k, Points::Histogram(v.clone()));
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_dimension_classes_become_metric_attributes_in_every_build() {
let mut p = RedactionPolicy::default();
for c in FieldClass::ALL {
if !c.is_deny() {
p.classes.insert(c, Action::Allow);
}
}
let attrs = AttrSet::new(&p)
.with("a", "rustev.step.id", FieldClass::Enum, "x")
.with("b", "rustev.step.id", FieldClass::SystemOfRecord, "y")
.with(
"c",
"rustev.step.id",
FieldClass::AuthenticatedAppField,
"z",
)
.done();
assert_eq!(attrs, vec![("a".to_string(), "x".to_string())]);
}
}