use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DogfoodEvent {
ExampleSuppressed {
detector: String,
path: Option<String>,
credential_redacted: String,
reason: Cow<'static, str>,
},
}
#[derive(Default)]
struct Telemetry {
dogfood_enabled: AtomicBool,
example_suppressions: AtomicUsize,
events: Mutex<Vec<DogfoodEvent>>,
}
static FILES_SCANNED: AtomicUsize = AtomicUsize::new(0);
static BYTES_SCANNED: AtomicUsize = AtomicUsize::new(0);
static SKIPPED_FILES: AtomicUsize = AtomicUsize::new(0);
static TOTAL_MATCHES: AtomicUsize = AtomicUsize::new(0);
static GPU_DISPATCHES: AtomicUsize = AtomicUsize::new(0);
static DOGFOOD_ENABLED: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct TelemetrySnapshot {
pub files_scanned: usize,
pub bytes_scanned: usize,
pub skipped_files: usize,
pub total_matches: usize,
pub gpu_dispatches: usize,
pub example_suppressions: usize,
}
fn cell() -> &'static Telemetry {
static CELL: OnceLock<Telemetry> = OnceLock::new();
CELL.get_or_init(Telemetry::default)
}
pub fn enable_dogfood() {
DOGFOOD_ENABLED.store(true, Ordering::Relaxed);
cell().dogfood_enabled.store(true, Ordering::Relaxed);
}
pub fn is_dogfood_enabled() -> bool {
DOGFOOD_ENABLED.load(Ordering::Relaxed)
}
pub fn record_example_suppression(
detector: &str,
path: Option<&str>,
credential: &str,
reason: &'static str,
) {
let t = cell();
t.example_suppressions.fetch_add(1, Ordering::Relaxed);
if !is_dogfood_enabled() {
return;
}
let redacted = redact_credential(credential);
if let Ok(mut events) = t.events.lock() {
events.push(DogfoodEvent::ExampleSuppressed {
detector: detector.to_string(),
path: path.map(str::to_string),
credential_redacted: redacted,
reason: Cow::Borrowed(reason),
});
}
}
pub fn example_suppression_count() -> usize {
cell().example_suppressions.load(Ordering::Relaxed)
}
pub fn reset_example_suppression_count() {
cell().example_suppressions.store(0, Ordering::Relaxed);
}
pub fn add_example_suppressions(n: usize) {
cell().example_suppressions.fetch_add(n, Ordering::Relaxed);
}
pub fn append_events<I: IntoIterator<Item = DogfoodEvent>>(events: I) {
let t = cell();
if let Ok(mut buf) = t.events.lock() {
buf.extend(events);
}
}
pub fn drain_events() -> Vec<DogfoodEvent> {
let t = cell();
if let Ok(mut events) = t.events.lock() {
std::mem::take(&mut *events)
} else {
Vec::new()
}
}
pub fn record_file_scanned(bytes: usize) {
FILES_SCANNED.fetch_add(1, Ordering::Relaxed);
BYTES_SCANNED.fetch_add(bytes, Ordering::Relaxed);
}
pub fn record_file_skipped() {
SKIPPED_FILES.fetch_add(1, Ordering::Relaxed);
}
pub fn record_match_found() {
TOTAL_MATCHES.fetch_add(1, Ordering::Relaxed);
}
pub fn record_gpu_dispatch() {
GPU_DISPATCHES.fetch_add(1, Ordering::Relaxed);
}
pub fn get_telemetry_snapshot() -> TelemetrySnapshot {
TelemetrySnapshot {
files_scanned: FILES_SCANNED.load(Ordering::Relaxed),
bytes_scanned: BYTES_SCANNED.load(Ordering::Relaxed),
skipped_files: SKIPPED_FILES.load(Ordering::Relaxed),
total_matches: TOTAL_MATCHES.load(Ordering::Relaxed),
gpu_dispatches: GPU_DISPATCHES.load(Ordering::Relaxed),
example_suppressions: example_suppression_count(),
}
}
#[doc(hidden)]
pub fn reset() {
let t = cell();
DOGFOOD_ENABLED.store(false, Ordering::Relaxed);
t.dogfood_enabled.store(false, Ordering::Relaxed);
t.example_suppressions.store(0, Ordering::Relaxed);
FILES_SCANNED.store(0, Ordering::Relaxed);
BYTES_SCANNED.store(0, Ordering::Relaxed);
SKIPPED_FILES.store(0, Ordering::Relaxed);
TOTAL_MATCHES.store(0, Ordering::Relaxed);
GPU_DISPATCHES.store(0, Ordering::Relaxed);
if let Ok(mut events) = t.events.lock() {
events.clear();
}
}
fn redact_credential(credential: &str) -> String {
const PREFIX_KEEP: usize = 6;
let take = credential.char_indices().nth(PREFIX_KEEP);
match take {
Some((end_byte, _)) => format!(
"{}…[redacted {} chars]",
&credential[..end_byte],
credential.chars().count().saturating_sub(PREFIX_KEEP)
),
None => "[redacted]".to_string(),
}
}