use flowscope::Timestamp;
impl From<flowscope::event::Severity> for Severity {
fn from(s: flowscope::event::Severity) -> Self {
match s {
flowscope::event::Severity::Info => Severity::Info,
flowscope::event::Severity::Warning => Severity::Warning,
flowscope::event::Severity::Error => Severity::Error,
flowscope::event::Severity::Critical => Severity::Critical,
_ => Severity::Warning,
}
}
}
impl From<Severity> for flowscope::event::Severity {
fn from(s: Severity) -> Self {
match s {
Severity::Info => flowscope::event::Severity::Info,
Severity::Warning => flowscope::event::Severity::Warning,
Severity::Error => flowscope::event::Severity::Error,
Severity::Critical => flowscope::event::Severity::Critical,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum Severity {
#[default]
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Anomaly<K> {
pub kind: &'static str,
pub severity: Severity,
pub key: Option<K>,
pub ts: Timestamp,
pub context: AnomalyContext,
}
impl Severity {
pub const fn as_str(&self) -> &'static str {
match self {
Severity::Info => "info",
Severity::Warning => "warning",
Severity::Error => "error",
Severity::Critical => "critical",
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl<K: std::fmt::Debug> std::fmt::Display for Anomaly<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {} ts={}", self.severity, self.kind, self.ts)?;
if let Some(k) = &self.key {
write!(f, " key={k:?}")?;
}
for (label, value) in &self.context.observations {
write!(f, " {label}={value}")?;
}
for (label, value) in &self.context.metrics {
write!(f, " {label}={value:.2}")?;
}
Ok(())
}
}
impl<K> Anomaly<K> {
pub fn new(kind: &'static str, severity: Severity, ts: Timestamp) -> Self {
Self {
kind,
severity,
key: None,
ts,
context: AnomalyContext::default(),
}
}
pub fn with_key(mut self, key: K) -> Self {
self.key = Some(key);
self
}
pub fn with_key_opt(mut self, key: Option<K>) -> Self {
self.key = key;
self
}
pub fn with_observation(mut self, label: &'static str, value: impl Into<String>) -> Self {
self.context.observations.push((label, value.into()));
self
}
pub fn with_metric(mut self, label: &'static str, value: f64) -> Self {
self.context.metrics.push((label, value));
self
}
}
impl<K: std::fmt::Debug> Anomaly<K> {
pub fn to_json_line(&self) -> String {
let mut s = String::with_capacity(96);
s.push('{');
s.push_str("\"severity\":");
json_string(&mut s, &self.severity.to_string());
s.push_str(",\"kind\":");
json_string(&mut s, self.kind);
s.push_str(",\"ts_secs\":");
s.push_str(&self.ts.sec.to_string());
s.push_str(",\"ts_nanos\":");
s.push_str(&self.ts.nsec.to_string());
if let Some(k) = &self.key {
s.push_str(",\"key\":");
let dbg = format!("{k:?}");
json_string(&mut s, &dbg);
}
s.push_str(",\"observations\":{");
for (i, (label, value)) in self.context.observations.iter().enumerate() {
if i > 0 {
s.push(',');
}
json_string(&mut s, label);
s.push(':');
json_string(&mut s, value);
}
s.push('}');
s.push_str(",\"metrics\":{");
for (i, (label, value)) in self.context.metrics.iter().enumerate() {
if i > 0 {
s.push(',');
}
json_string(&mut s, label);
s.push(':');
if value.is_finite() {
s.push_str(&format!("{value}"));
} else {
s.push_str("null");
}
}
s.push('}');
s.push('}');
s
}
}
#[cfg(feature = "serde")]
impl<K: serde::Serialize> Anomaly<K> {
pub fn to_json_value(&self) -> serde_json::Value {
serde_json::to_value(self).expect("Anomaly is always serializable")
}
}
impl<K: std::fmt::Debug> Anomaly<K> {
pub fn emit_tracing(&self) {
let payload = self.to_json_line();
let key_dbg = self.key.as_ref().map(|k| format!("{k:?}"));
let key_str: &str = key_dbg.as_deref().unwrap_or("");
match self.severity {
Severity::Info => tracing::event!(
target: "netring.anomaly",
tracing::Level::INFO,
kind = self.kind,
severity = "info",
ts_secs = self.ts.sec,
ts_nanos = self.ts.nsec,
key = key_str,
payload = %payload,
),
Severity::Warning => tracing::event!(
target: "netring.anomaly",
tracing::Level::WARN,
kind = self.kind,
severity = "warning",
ts_secs = self.ts.sec,
ts_nanos = self.ts.nsec,
key = key_str,
payload = %payload,
),
Severity::Error => tracing::event!(
target: "netring.anomaly",
tracing::Level::ERROR,
kind = self.kind,
severity = "error",
ts_secs = self.ts.sec,
ts_nanos = self.ts.nsec,
key = key_str,
payload = %payload,
),
Severity::Critical => tracing::event!(
target: "netring.anomaly",
tracing::Level::ERROR,
critical = true,
kind = self.kind,
severity = "critical",
ts_secs = self.ts.sec,
ts_nanos = self.ts.nsec,
key = key_str,
payload = %payload,
),
}
}
}
fn json_string(out: &mut String, value: &str) {
out.push('"');
for c in value.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x08' => out.push_str("\\b"),
'\x0c' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct AnomalyContext {
pub observations: Vec<(&'static str, String)>,
pub metrics: Vec<(&'static str, f64)>,
}