use std::sync::atomic::{AtomicU64, Ordering};
use crate::TelemetryEvent;
macro_rules! counters {
($($(#[$doc:meta])* $name:ident,)*) => {
#[derive(Default)]
pub(crate) struct Counters {
$($(#[$doc])* pub $name: AtomicU64,)*
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct Snapshot {
$(pub $name: u64,)*
}
impl Counters {
pub fn snapshot(&self) -> Snapshot {
Snapshot { $($name: self.$name.load(Ordering::Relaxed),)* }
}
}
impl Snapshot {
pub fn since(&self, earlier: &Snapshot) -> Snapshot {
Snapshot { $($name: self.$name.saturating_sub(earlier.$name),)* }
}
}
};
}
counters! {
queue_full,
cache_error,
cache_full,
expired,
corrupt,
invalid,
rejected,
oversized,
throttled,
disabled,
rate_limited,
purged,
uploads_sent,
upload_bytes,
upload_failures,
upload_timeouts,
auth_denied,
hold_cap_hits,
cache_write_errors,
}
impl Counters {
pub fn add(counter: &AtomicU64, n: u64) {
counter.fetch_add(n, Ordering::Relaxed);
}
}
impl Snapshot {
pub fn dropped(&self) -> u64 {
self.problem_drops() + self.disabled + self.purged
}
pub fn failed_attempts(&self) -> u64 {
self.upload_failures + self.upload_timeouts
}
pub fn problem_drops(&self) -> u64 {
self.queue_full
+ self.cache_error
+ self.cache_full
+ self.expired
+ self.corrupt
+ self.invalid
+ self.rejected
+ self.oversized
+ self.throttled
+ self.rate_limited
}
pub fn has_problems(&self) -> bool {
self.problem_drops() > 0
|| self.upload_failures > 0
|| self.upload_timeouts > 0
|| self.auth_denied > 0
|| self.hold_cap_hits > 0
|| self.cache_write_errors > 0
}
pub fn report(&self, cached_batches: u64) -> TelemetryEvent {
let mut event = TelemetryEvent::new("lk.telemetry.report")
.with_body(format!(
"telemetry: {} batches sent ({} B), {} failed, {} dropped, {} cached",
self.uploads_sent,
self.upload_bytes,
self.failed_attempts(),
self.problem_drops(),
cached_batches
))
.with_attribute("lk.telemetry.uploads.sent", int(self.uploads_sent))
.with_attribute("lk.telemetry.uploads.bytes", int(self.upload_bytes))
.with_attribute("lk.telemetry.uploads.failed", int(self.upload_failures))
.with_attribute("lk.telemetry.cache.batches", int(cached_batches));
for (key, value) in [
("lk.telemetry.uploads.timeouts", self.upload_timeouts),
("lk.telemetry.uploads.unauthorized", self.auth_denied),
("lk.telemetry.holds.capped", self.hold_cap_hits),
("lk.telemetry.cache.write_errors", self.cache_write_errors),
("lk.telemetry.dropped.queue_full", self.queue_full),
("lk.telemetry.dropped.cache_error", self.cache_error),
("lk.telemetry.dropped.cache_full", self.cache_full),
("lk.telemetry.dropped.expired", self.expired),
("lk.telemetry.dropped.corrupt", self.corrupt),
("lk.telemetry.dropped.invalid", self.invalid),
("lk.telemetry.dropped.rejected", self.rejected),
("lk.telemetry.dropped.oversized", self.oversized),
("lk.telemetry.dropped.throttled", self.throttled),
("lk.telemetry.dropped.rate_limited", self.rate_limited),
] {
if value > 0 {
event = event.with_attribute(key, int(value));
}
}
event
}
}
fn int(count: u64) -> i64 {
i64::try_from(count).unwrap_or(i64::MAX)
}
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TelemetryStatus {
Ok,
Held,
Paused,
Throttled,
Waiting,
Off,
}
impl std::fmt::Display for TelemetryStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Ok => "ok",
Self::Held => "held",
Self::Paused => "paused",
Self::Throttled => "throttled",
Self::Waiting => "waiting",
Self::Off => "off",
})
}
}
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TelemetryStats {
pub status: TelemetryStatus,
pub dropped: u64,
pub dropped_queue_full: u64,
pub dropped_cache_error: u64,
pub dropped_cache_full: u64,
pub dropped_expired: u64,
pub dropped_corrupt: u64,
pub dropped_invalid: u64,
pub dropped_rejected: u64,
pub dropped_oversized: u64,
pub dropped_throttled: u64,
pub dropped_disabled: u64,
pub dropped_rate_limited: u64,
pub dropped_purged: u64,
pub uploads_sent: u64,
pub upload_bytes: u64,
pub upload_failures: u64,
pub upload_timeouts: u64,
pub uploads_unauthorized: u64,
pub holds_capped: u64,
pub cache_write_errors: u64,
pub cached_batches: u64,
}
impl std::fmt::Display for TelemetryStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}, sent {} ({} B), backlog {}, failed {}, lost {} (queue {}, cache {}, expired {}, \
corrupt {}, invalid {}, rejected {}, oversized {}, throttled {}, rate-limited {}, \
disabled {}, purged {}; unauthorized {}, holds capped {}, write errors {})",
self.status,
self.uploads_sent,
self.upload_bytes,
self.cached_batches,
self.failed_attempts(),
self.dropped,
self.dropped_queue_full,
self.dropped_cache_full + self.dropped_cache_error,
self.dropped_expired,
self.dropped_corrupt,
self.dropped_invalid,
self.dropped_rejected,
self.dropped_oversized,
self.dropped_throttled,
self.dropped_rate_limited,
self.dropped_disabled,
self.dropped_purged,
self.uploads_unauthorized,
self.holds_capped,
self.cache_write_errors,
)
}
}
impl TelemetryStats {
pub fn failed_attempts(&self) -> u64 {
self.upload_failures + self.upload_timeouts
}
pub(crate) fn new(snapshot: Snapshot, cached_batches: u64, status: TelemetryStatus) -> Self {
Self {
status,
dropped: snapshot.dropped(),
dropped_queue_full: snapshot.queue_full,
dropped_cache_error: snapshot.cache_error,
dropped_cache_full: snapshot.cache_full,
dropped_expired: snapshot.expired,
dropped_corrupt: snapshot.corrupt,
dropped_invalid: snapshot.invalid,
dropped_rejected: snapshot.rejected,
dropped_oversized: snapshot.oversized,
dropped_throttled: snapshot.throttled,
dropped_disabled: snapshot.disabled,
dropped_rate_limited: snapshot.rate_limited,
dropped_purged: snapshot.purged,
uploads_sent: snapshot.uploads_sent,
upload_bytes: snapshot.upload_bytes,
upload_failures: snapshot.upload_failures,
upload_timeouts: snapshot.upload_timeouts,
uploads_unauthorized: snapshot.auth_denied,
holds_capped: snapshot.hold_cap_hits,
cache_write_errors: snapshot.cache_write_errors,
cached_batches,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AttributeValue;
#[test]
fn report_counts_saturate_instead_of_wrapping() {
let snapshot = Snapshot { upload_failures: u64::MAX, ..Snapshot::default() };
let report = snapshot.report(u64::MAX);
let value =
|key: &str| report.attributes.iter().find(|a| a.key == key).map(|a| a.value.clone());
assert_eq!(value("lk.telemetry.uploads.failed"), Some(AttributeValue::Int(i64::MAX)));
assert_eq!(value("lk.telemetry.cache.batches"), Some(AttributeValue::Int(i64::MAX)));
}
}