use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Success,
Failure,
}
impl Outcome {
pub fn as_str(self) -> &'static str {
match self {
Outcome::Success => "success",
Outcome::Failure => "failure",
}
}
}
#[derive(Debug, Clone)]
pub struct TelemetryEvent {
pub operation: &'static str,
pub outcome: Outcome,
pub duration: Duration,
pub attributes: Vec<(&'static str, String)>,
}
pub trait Telemetry: Send + Sync {
fn record(&self, event: &TelemetryEvent);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopTelemetry;
impl Telemetry for NoopTelemetry {
fn record(&self, _event: &TelemetryEvent) {}
}
impl NoopTelemetry {
pub fn shared() -> Arc<dyn Telemetry> {
Arc::new(NoopTelemetry)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TracingTelemetry;
impl TracingTelemetry {
pub fn shared() -> Arc<dyn Telemetry> {
Arc::new(TracingTelemetry)
}
}
impl Telemetry for TracingTelemetry {
fn record(&self, event: &TelemetryEvent) {
let duration_ms = event.duration.as_secs_f64() * 1000.0;
let attrs = event
.attributes
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(" ");
match event.outcome {
Outcome::Success => tracing::debug!(
target: "proton_sdk::telemetry",
operation = event.operation,
outcome = event.outcome.as_str(),
duration_ms,
attributes = %attrs,
"operation completed",
),
Outcome::Failure => tracing::warn!(
target: "proton_sdk::telemetry",
operation = event.operation,
outcome = event.outcome.as_str(),
duration_ms,
attributes = %attrs,
"operation failed",
),
}
}
}
pub struct OpTimer {
telemetry: Arc<dyn Telemetry>,
operation: &'static str,
start: std::time::Instant,
outcome: Outcome,
attributes: Vec<(&'static str, String)>,
}
impl OpTimer {
pub fn success(&mut self) {
self.outcome = Outcome::Success;
}
pub fn set_outcome(&mut self, outcome: Outcome) {
self.outcome = outcome;
}
pub fn attr(&mut self, key: &'static str, value: impl ToString) {
self.attributes.push((key, value.to_string()));
}
}
impl Drop for OpTimer {
fn drop(&mut self) {
let event = TelemetryEvent {
operation: self.operation,
outcome: self.outcome,
duration: self.start.elapsed(),
attributes: std::mem::take(&mut self.attributes),
};
self.telemetry.record(&event);
}
}
pub trait TelemetryExt {
fn start(&self, operation: &'static str) -> OpTimer;
}
impl TelemetryExt for Arc<dyn Telemetry> {
fn start(&self, operation: &'static str) -> OpTimer {
OpTimer {
telemetry: Arc::clone(self),
operation,
start: std::time::Instant::now(),
outcome: Outcome::Failure,
attributes: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[derive(Default)]
struct Capture(Mutex<Vec<TelemetryEvent>>);
impl Telemetry for Capture {
fn record(&self, event: &TelemetryEvent) {
self.0.lock().unwrap().push(event.clone());
}
}
fn shared_capture() -> (Arc<dyn Telemetry>, Arc<Capture>) {
let capture = Arc::new(Capture::default());
(capture.clone() as Arc<dyn Telemetry>, capture)
}
#[test]
fn timer_records_failure_by_default() {
let (sink, capture) = shared_capture();
{
let _timer = sink.start("op_a");
}
let events = capture.0.lock().unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].operation, "op_a");
assert_eq!(events[0].outcome, Outcome::Failure);
}
#[test]
fn timer_records_success_and_attributes() {
let (sink, capture) = shared_capture();
{
let mut timer = sink.start("op_b");
timer.attr("block_count", 3usize);
timer.success();
}
let events = capture.0.lock().unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].outcome, Outcome::Success);
assert_eq!(events[0].attributes, vec![("block_count", "3".to_string())]);
}
#[test]
fn noop_records_nothing() {
let sink = NoopTelemetry::shared();
let mut timer = sink.start("op_c");
timer.success();
}
#[test]
fn outcome_label_form() {
assert_eq!(Outcome::Success.as_str(), "success");
assert_eq!(Outcome::Failure.as_str(), "failure");
}
}