1#[cfg(feature = "decode")]
18use keyhog_core::ChunkMetadata;
19use serde::{Deserialize, Serialize};
20use std::borrow::Cow;
21use std::cell::RefCell;
22use std::collections::{BTreeMap, HashSet};
23use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
24use std::sync::{Arc, Mutex, OnceLock};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(tag = "kind", rename_all = "snake_case")]
31pub enum DogfoodEvent {
32 ExampleSuppressed {
41 detector: String,
42 path: Option<String>,
43 credential_redacted: String,
44 reason: Cow<'static, str>,
45 },
46 ShapeSuppressed {
58 path: Option<String>,
59 credential_redacted: String,
60 reason: Cow<'static, str>,
61 },
62 StaticRecoveryRejected {
66 path: Option<String>,
67 expression_offset: usize,
68 decoder: Cow<'static, str>,
69 reason: Cow<'static, str>,
70 },
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub(crate) enum StaticRecoveryRejection {
77 LiteralByteArrayElement,
78 JsonBase64,
79 JsonUtf8,
80 JsonByteArray,
81 XorPlaintextUtf8,
82 StringJoinJson,
83 BufferBase64,
84 BufferHex,
85 AesKeyLength,
86 AesIvLength,
87 AesCiphertextBlockLength,
88 AesPadding,
89 AesPlaintextUtf8,
90}
91
92impl StaticRecoveryRejection {
93 const ALL: [Self; 13] = [
94 Self::LiteralByteArrayElement,
95 Self::JsonBase64,
96 Self::JsonUtf8,
97 Self::JsonByteArray,
98 Self::XorPlaintextUtf8,
99 Self::StringJoinJson,
100 Self::BufferBase64,
101 Self::BufferHex,
102 Self::AesKeyLength,
103 Self::AesIvLength,
104 Self::AesCiphertextBlockLength,
105 Self::AesPadding,
106 Self::AesPlaintextUtf8,
107 ];
108
109 const fn index(self) -> usize {
110 match self {
111 Self::LiteralByteArrayElement => 0,
112 Self::JsonBase64 => 1,
113 Self::JsonUtf8 => 2,
114 Self::JsonByteArray => 3,
115 Self::XorPlaintextUtf8 => 4,
116 Self::StringJoinJson => 5,
117 Self::BufferBase64 => 6,
118 Self::BufferHex => 7,
119 Self::AesKeyLength => 8,
120 Self::AesIvLength => 9,
121 Self::AesCiphertextBlockLength => 10,
122 Self::AesPadding => 11,
123 Self::AesPlaintextUtf8 => 12,
124 }
125 }
126
127 pub(crate) const fn as_str(self) -> &'static str {
128 match self {
129 Self::LiteralByteArrayElement => "literal_byte_array_element",
130 Self::JsonBase64 => "json_base64",
131 Self::JsonUtf8 => "json_utf8",
132 Self::JsonByteArray => "json_byte_array",
133 Self::XorPlaintextUtf8 => "xor_plaintext_utf8",
134 Self::StringJoinJson => "string_join_json",
135 Self::BufferBase64 => "buffer_base64",
136 Self::BufferHex => "buffer_hex",
137 Self::AesKeyLength => "aes_key_length",
138 Self::AesIvLength => "aes_iv_length",
139 Self::AesCiphertextBlockLength => "aes_ciphertext_block_length",
140 Self::AesPadding => "aes_padding",
141 Self::AesPlaintextUtf8 => "aes_plaintext_utf8",
142 }
143 }
144}
145
146pub const DOGFOOD_DETAIL_EVENT_LIMIT: usize = 1024;
149
150fn record_dropped_detail(counter: &AtomicUsize) {
151 let mut current = counter.load(Ordering::Relaxed);
152 while current != usize::MAX {
153 match counter.compare_exchange_weak(
154 current,
155 current + 1,
156 Ordering::Relaxed,
157 Ordering::Relaxed,
158 ) {
159 Ok(_) => return,
160 Err(observed) => current = observed,
161 }
162 }
163}
164
165fn push_dogfood_detail(
166 events: &Mutex<Vec<DogfoodEvent>>,
167 detail_events_dropped: &AtomicUsize,
168 event: DogfoodEvent,
169) -> bool {
170 match events.lock() {
171 Ok(mut events) if events.len() < DOGFOOD_DETAIL_EVENT_LIMIT => {
172 events.push(event);
173 true
174 }
175 Ok(_) | Err(_) => {
176 record_dropped_detail(detail_events_dropped);
178 false
179 }
180 }
181}
182
183fn recover_telemetry_lock<'a, T>(mutex: &'a Mutex<T>) -> std::sync::MutexGuard<'a, T> {
184 match mutex.lock() {
185 Ok(guard) => guard,
186 Err(poisoned) => {
187 let guard = poisoned.into_inner();
188 mutex.clear_poison();
189 guard
190 }
191 }
192}
193
194#[derive(Default)]
195struct StaticRecoveryTelemetry {
196 counts: [AtomicU64; StaticRecoveryRejection::ALL.len()],
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Hash)]
200enum EmittedDogfoodKey {
201 Suppression(String),
202 #[cfg(feature = "decode")]
203 StaticRecovery {
204 source_type: Arc<str>,
205 path: Option<Arc<str>>,
206 commit: Option<Arc<str>>,
207 expression_offset: usize,
208 reason: &'static str,
209 },
210}
211
212impl StaticRecoveryTelemetry {
213 fn record(&self, reason: StaticRecoveryRejection) {
214 self.add(reason, 1);
215 }
216
217 fn add(&self, reason: StaticRecoveryRejection, amount: u64) {
218 let counter = &self.counts[reason.index()];
219 let mut current = counter.load(Ordering::Relaxed);
220 while current != u64::MAX {
221 let next = current.saturating_add(amount);
222 match counter.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed)
223 {
224 Ok(_) => return,
225 Err(observed) => current = observed,
226 }
227 }
228 }
229
230 fn snapshot(&self) -> BTreeMap<String, u64> {
231 StaticRecoveryRejection::ALL
232 .iter()
233 .filter_map(|reason| {
234 let count = self.counts[reason.index()].load(Ordering::Relaxed);
235 (count != 0).then(|| (reason.as_str().to_owned(), count))
236 })
237 .collect()
238 }
239
240 fn reset(&self) {
241 for count in &self.counts {
242 count.store(0, Ordering::Relaxed);
243 }
244 }
245}
246
247#[derive(Default)]
248struct Telemetry {
249 dogfood_enabled: AtomicBool,
250 example_suppressions: AtomicUsize,
251 events: Mutex<Vec<DogfoodEvent>>,
252 emitted_suppression_events: Mutex<HashSet<EmittedDogfoodKey>>,
262 detail_events_dropped: AtomicUsize,
263 static_recovery: StaticRecoveryTelemetry,
264}
265
266#[derive(Default)]
274pub struct ScanTelemetry {
275 dogfood_enabled: AtomicBool,
276 example_suppressions: AtomicUsize,
277 events: Mutex<Vec<DogfoodEvent>>,
278 emitted_suppression_events: Mutex<HashSet<EmittedDogfoodKey>>,
279 detail_events_dropped: AtomicUsize,
280 static_recovery: StaticRecoveryTelemetry,
281}
282
283impl ScanTelemetry {
284 pub fn new() -> Self {
285 Self::default()
286 }
287
288 pub fn enable_dogfood(&self) {
289 self.dogfood_enabled.store(true, Ordering::Relaxed);
290 }
291
292 fn is_dogfood_enabled(&self) -> bool {
293 self.dogfood_enabled.load(Ordering::Relaxed)
294 }
295
296 fn example_suppression_count(&self) -> usize {
297 self.example_suppressions.load(Ordering::Relaxed)
298 }
299
300 fn drain_events(&self) -> Vec<DogfoodEvent> {
301 drain_event_buffers(&self.events, &self.emitted_suppression_events)
302 }
303
304 pub fn drain(&self) -> ScanTelemetrySnapshot {
305 ScanTelemetrySnapshot {
306 example_suppressions: self.example_suppression_count() as u64,
307 dogfood_events: self.drain_events(),
308 dogfood_detail_events_dropped: self.detail_events_dropped.load(Ordering::Relaxed)
309 as u64,
310 static_recovery_rejections: self.static_recovery.snapshot(),
311 }
312 }
313}
314
315pub struct ScanTelemetrySnapshot {
316 pub example_suppressions: u64,
317 pub dogfood_events: Vec<DogfoodEvent>,
318 pub dogfood_detail_events_dropped: u64,
319 pub static_recovery_rejections: BTreeMap<String, u64>,
320}
321
322thread_local! {
323 static CURRENT_SCAN_TELEMETRY: RefCell<Option<Arc<ScanTelemetry>>> = RefCell::new(None);
324}
325
326struct ScanTelemetryRestore {
327 previous: Option<Arc<ScanTelemetry>>,
328}
329
330impl Drop for ScanTelemetryRestore {
331 fn drop(&mut self) {
332 let previous = self.previous.take();
333 CURRENT_SCAN_TELEMETRY.with(|slot| {
334 *slot.borrow_mut() = previous;
335 });
336 }
337}
338
339pub fn with_scan_telemetry<R>(telemetry: &Arc<ScanTelemetry>, f: impl FnOnce() -> R) -> R {
343 let previous = CURRENT_SCAN_TELEMETRY.with(|slot| {
344 let mut slot = slot.borrow_mut();
345 slot.replace(Arc::clone(telemetry))
346 });
347 let _restore = ScanTelemetryRestore { previous };
348 f()
349}
350
351fn current_scan_telemetry() -> Option<Arc<ScanTelemetry>> {
352 CURRENT_SCAN_TELEMETRY.with(|slot| slot.borrow().clone())
353}
354
355pub(crate) fn capture_scan_telemetry() -> Option<Arc<ScanTelemetry>> {
358 current_scan_telemetry()
359}
360
361pub(crate) fn with_captured_scan_telemetry<R>(
364 telemetry: Option<&Arc<ScanTelemetry>>,
365 f: impl FnOnce() -> R,
366) -> R {
367 match telemetry {
368 Some(telemetry) => with_scan_telemetry(telemetry, f),
369 None => f(),
370 }
371}
372
373fn current_scan_dogfood_enabled() -> Option<bool> {
374 CURRENT_SCAN_TELEMETRY.with(|slot| {
375 slot.borrow()
376 .as_ref()
377 .map(|telemetry| telemetry.is_dogfood_enabled())
378 })
379}
380
381static FILES_SCANNED: AtomicUsize = AtomicUsize::new(0);
383static BYTES_SCANNED: AtomicUsize = AtomicUsize::new(0);
384static SKIPPED_FILES: AtomicUsize = AtomicUsize::new(0);
385static TOTAL_MATCHES: AtomicUsize = AtomicUsize::new(0);
386static GPU_DISPATCHES: AtomicUsize = AtomicUsize::new(0);
387static STRUCTURED_PARSE_FAILURES: AtomicUsize = AtomicUsize::new(0);
396static STRUCTURED_OVERSIZE_SKIPS: AtomicUsize = AtomicUsize::new(0);
405static DECODE_TRUNCATIONS: AtomicUsize = AtomicUsize::new(0);
409#[cfg(test)]
410thread_local! {
411 static THREAD_DECODE_TRUNCATIONS: std::cell::Cell<usize> =
412 const { std::cell::Cell::new(0) };
413}
414static INVALID_PATTERN_INDEX_SKIPS: AtomicUsize = AtomicUsize::new(0);
418static BOUNDARY_RESULT_CARDINALITY_MISMATCHES: AtomicUsize = AtomicUsize::new(0);
421static LINE_OFFSET_MAPPING_MISMATCHES: AtomicUsize = AtomicUsize::new(0);
424static CHUNK_DEADLINE_ABORTS: AtomicUsize = AtomicUsize::new(0);
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432pub(crate) enum ScannerCoverageGapEvent {
433 StructuredParseFailure,
434 StructuredOversizeSkip,
435 DecodeTruncation,
436 InvalidPatternIndexSkip,
437 BoundaryResultCardinalityMismatch,
438 LineOffsetMappingMismatch,
439 ChunkDeadlineAbort,
440}
441
442impl ScannerCoverageGapEvent {
443 pub(crate) const ALL: [Self; 7] = [
446 Self::StructuredParseFailure,
447 Self::StructuredOversizeSkip,
448 Self::DecodeTruncation,
449 Self::InvalidPatternIndexSkip,
450 Self::BoundaryResultCardinalityMismatch,
451 Self::LineOffsetMappingMismatch,
452 Self::ChunkDeadlineAbort,
453 ];
454
455 pub(crate) fn counter(self) -> &'static AtomicUsize {
456 match self {
457 Self::StructuredParseFailure => &STRUCTURED_PARSE_FAILURES,
458 Self::StructuredOversizeSkip => &STRUCTURED_OVERSIZE_SKIPS,
459 Self::DecodeTruncation => &DECODE_TRUNCATIONS,
460 Self::InvalidPatternIndexSkip => &INVALID_PATTERN_INDEX_SKIPS,
461 Self::BoundaryResultCardinalityMismatch => &BOUNDARY_RESULT_CARDINALITY_MISMATCHES,
462 Self::LineOffsetMappingMismatch => &LINE_OFFSET_MAPPING_MISMATCHES,
463 Self::ChunkDeadlineAbort => &CHUNK_DEADLINE_ABORTS,
464 }
465 }
466
467 const fn label(self) -> &'static str {
468 match self {
469 Self::StructuredParseFailure => "structured_parse_failures",
470 Self::StructuredOversizeSkip => "structured_oversize_skips",
471 Self::DecodeTruncation => "decode_truncations",
472 Self::InvalidPatternIndexSkip => "invalid_pattern_index_skips",
473 Self::BoundaryResultCardinalityMismatch => "boundary_result_cardinality_mismatches",
474 Self::LineOffsetMappingMismatch => "line_offset_mapping_mismatches",
475 Self::ChunkDeadlineAbort => "chunk_deadline_aborts",
476 }
477 }
478}
479
480#[derive(Clone, Copy, Default, Eq, PartialEq)]
485pub struct ScannerCoverageSnapshot {
486 counts: [usize; ScannerCoverageGapEvent::ALL.len()],
487}
488
489impl ScannerCoverageSnapshot {
490 #[must_use]
491 pub fn capture() -> Self {
492 Self {
493 counts: std::array::from_fn(|index| {
494 ScannerCoverageGapEvent::ALL[index]
495 .counter()
496 .load(Ordering::Relaxed)
497 }),
498 }
499 }
500
501 #[must_use]
502 pub fn saturating_delta(self, earlier: Self) -> Self {
503 Self {
504 counts: std::array::from_fn(|index| {
505 self.counts[index].saturating_sub(earlier.counts[index])
506 }),
507 }
508 }
509
510 #[must_use]
511 pub fn is_empty(self) -> bool {
512 self.counts.iter().all(|count| *count == 0)
513 }
514}
515
516impl std::fmt::Debug for ScannerCoverageSnapshot {
517 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518 let mut gaps = formatter.debug_map();
519 for (event, count) in ScannerCoverageGapEvent::ALL.into_iter().zip(self.counts) {
520 if count > 0 {
521 gaps.entry(&event.label(), &count);
522 }
523 }
524 gaps.finish()
525 }
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
530#[must_use = "scanner coverage gaps must be recorded through the typed recorder so partial coverage remains surfaced"]
531pub(crate) struct RecordedScannerCoverageGap {
532 event: ScannerCoverageGapEvent,
533 previous: usize,
534 delta: usize,
535}
536
537pub(crate) fn record_scanner_coverage_gap(
538 event: ScannerCoverageGapEvent,
539) -> RecordedScannerCoverageGap {
540 let previous = event.counter().fetch_add(1, Ordering::Relaxed);
541 RecordedScannerCoverageGap {
542 event,
543 previous,
544 delta: 1,
545 }
546}
547
548static DOGFOOD_ENABLED: AtomicBool = AtomicBool::new(false);
550
551fn cell() -> &'static Telemetry {
552 static CELL: OnceLock<Telemetry> = OnceLock::new();
553 CELL.get_or_init(Telemetry::default)
554}
555
556pub fn enable_dogfood() {
558 DOGFOOD_ENABLED.store(true, Ordering::Relaxed);
559 cell().dogfood_enabled.store(true, Ordering::Relaxed);
560}
561
562pub fn is_dogfood_enabled() -> bool {
563 if let Some(enabled) = current_scan_dogfood_enabled() {
564 return enabled;
565 }
566 DOGFOOD_ENABLED.load(Ordering::Relaxed)
567}
568
569pub fn record_example_suppression(
573 detector: &str,
574 path: Option<&str>,
575 credential: &str,
576 reason: &'static str,
577) {
578 if let Some(t) = current_scan_telemetry() {
579 record_example_suppression_in(
580 &t.example_suppressions,
581 &t.events,
582 &t.emitted_suppression_events,
583 &t.detail_events_dropped,
584 detector,
585 path,
586 credential,
587 reason,
588 );
589 return;
590 }
591
592 let t = cell();
593 record_example_suppression_in(
594 &t.example_suppressions,
595 &t.events,
596 &t.emitted_suppression_events,
597 &t.detail_events_dropped,
598 detector,
599 path,
600 credential,
601 reason,
602 );
603}
604
605fn record_example_suppression_in(
606 example_suppressions: &AtomicUsize,
607 events: &Mutex<Vec<DogfoodEvent>>,
608 emitted_suppression_events: &Mutex<HashSet<EmittedDogfoodKey>>,
609 detail_events_dropped: &AtomicUsize,
610 detector: &str,
611 path: Option<&str>,
612 credential: &str,
613 reason: &'static str,
614) {
615 example_suppressions.fetch_add(1, Ordering::Relaxed);
616
617 if !is_dogfood_enabled() {
619 return;
620 }
621
622 let credential_hash = keyhog_core::hex_encode(&keyhog_core::sha256_hash(credential));
623 if !mark_suppression_event_emitted(
627 emitted_suppression_events,
628 detail_events_dropped,
629 &credential_hash,
630 ) {
631 return;
632 }
633
634 let redacted = keyhog_core::redact(credential).into_owned();
638 push_dogfood_detail(
639 events,
640 detail_events_dropped,
641 DogfoodEvent::ExampleSuppressed {
642 detector: detector.to_string(),
643 path: path.map(str::to_string),
644 credential_redacted: redacted,
645 reason: Cow::Borrowed(reason),
646 },
647 );
648}
649
650fn mark_suppression_event_emitted(
662 emitted_suppression_events: &Mutex<HashSet<EmittedDogfoodKey>>,
663 detail_events_dropped: &AtomicUsize,
664 credential_hash: &str,
665) -> bool {
666 match emitted_suppression_events.lock() {
667 Ok(mut emitted) => {
668 let key = EmittedDogfoodKey::Suppression(credential_hash.to_owned());
669 if emitted.contains(&key) {
670 return false;
671 }
672 if emitted.len() >= DOGFOOD_DETAIL_EVENT_LIMIT {
673 record_dropped_detail(detail_events_dropped);
674 return false;
675 }
676 emitted.insert(key)
677 }
678 Err(_) => {
679 record_dropped_detail(detail_events_dropped);
681 false }
683 }
684}
685
686pub(crate) fn record_shape_suppression(path: Option<&str>, credential: &str, reason: &'static str) {
696 if !is_dogfood_enabled() {
698 return;
699 }
700 if let Some(t) = current_scan_telemetry() {
701 record_shape_suppression_in(
702 &t.events,
703 &t.emitted_suppression_events,
704 &t.detail_events_dropped,
705 path,
706 credential,
707 reason,
708 );
709 return;
710 }
711 let t = cell();
712 record_shape_suppression_in(
713 &t.events,
714 &t.emitted_suppression_events,
715 &t.detail_events_dropped,
716 path,
717 credential,
718 reason,
719 );
720}
721
722#[cfg(feature = "decode")]
725pub(crate) fn record_static_recovery_rejection(
726 metadata: &ChunkMetadata,
727 expression_offset: usize,
728 reason: StaticRecoveryRejection,
729) {
730 if !is_dogfood_enabled() {
731 return;
732 }
733 if let Some(t) = current_scan_telemetry() {
734 t.static_recovery.record(reason);
735 if !mark_static_recovery_event_emitted(
736 &t.emitted_suppression_events,
737 &t.detail_events_dropped,
738 metadata,
739 expression_offset,
740 reason,
741 ) {
742 return;
743 }
744 push_dogfood_detail(
745 &t.events,
746 &t.detail_events_dropped,
747 static_recovery_event(metadata, expression_offset, reason),
748 );
749 return;
750 }
751 let t = cell();
752 t.static_recovery.record(reason);
753 if !mark_static_recovery_event_emitted(
754 &t.emitted_suppression_events,
755 &t.detail_events_dropped,
756 metadata,
757 expression_offset,
758 reason,
759 ) {
760 return;
761 }
762 push_dogfood_detail(
763 &t.events,
764 &t.detail_events_dropped,
765 static_recovery_event(metadata, expression_offset, reason),
766 );
767}
768
769#[cfg(feature = "decode")]
770fn static_recovery_event(
771 metadata: &ChunkMetadata,
772 expression_offset: usize,
773 reason: StaticRecoveryRejection,
774) -> DogfoodEvent {
775 DogfoodEvent::StaticRecoveryRejected {
776 path: metadata.path.as_deref().map(str::to_owned),
777 expression_offset,
778 decoder: Cow::Borrowed("javascript-static"),
779 reason: Cow::Borrowed(reason.as_str()),
780 }
781}
782
783#[cfg(feature = "decode")]
784fn mark_static_recovery_event_emitted(
785 emitted_events: &Mutex<HashSet<EmittedDogfoodKey>>,
786 detail_events_dropped: &AtomicUsize,
787 metadata: &ChunkMetadata,
788 expression_offset: usize,
789 reason: StaticRecoveryRejection,
790) -> bool {
791 let key = EmittedDogfoodKey::StaticRecovery {
792 source_type: Arc::clone(&metadata.source_type),
793 path: metadata.path.clone(),
794 commit: metadata.commit.clone(),
795 expression_offset,
796 reason: reason.as_str(),
797 };
798 match emitted_events.lock() {
799 Ok(mut emitted) => {
800 if emitted.contains(&key) {
801 return false;
802 }
803 if emitted.len() >= DOGFOOD_DETAIL_EVENT_LIMIT {
804 record_dropped_detail(detail_events_dropped);
805 return false;
806 }
807 emitted.insert(key)
808 }
809 Err(_) => {
810 record_dropped_detail(detail_events_dropped);
812 false }
814 }
815}
816
817fn record_shape_suppression_in(
818 events: &Mutex<Vec<DogfoodEvent>>,
819 emitted_suppression_events: &Mutex<HashSet<EmittedDogfoodKey>>,
820 detail_events_dropped: &AtomicUsize,
821 path: Option<&str>,
822 credential: &str,
823 reason: &'static str,
824) {
825 let credential_hash = keyhog_core::hex_encode(&keyhog_core::sha256_hash(credential));
826 if !mark_suppression_event_emitted(
833 emitted_suppression_events,
834 detail_events_dropped,
835 &credential_hash,
836 ) {
837 return;
838 }
839 let redacted = keyhog_core::redact(credential).into_owned();
840 push_dogfood_detail(
841 events,
842 detail_events_dropped,
843 DogfoodEvent::ShapeSuppressed {
844 path: path.map(str::to_string),
845 credential_redacted: redacted,
846 reason: Cow::Borrowed(reason),
847 },
848 );
849}
850
851pub fn example_suppression_count() -> usize {
853 cell().example_suppressions.load(Ordering::Relaxed)
854}
855
856#[cfg(test)]
861pub(crate) fn reset_example_suppression_count() {
862 cell().example_suppressions.store(0, Ordering::Relaxed);
863}
864
865pub fn add_example_suppressions(n: usize) {
870 cell().example_suppressions.fetch_add(n, Ordering::Relaxed);
871}
872
873pub(crate) fn record_structured_parse_failure() {
879 let _receipt = record_scanner_coverage_gap(ScannerCoverageGapEvent::StructuredParseFailure);
880}
881
882pub fn structured_parse_failure_count() -> usize {
884 STRUCTURED_PARSE_FAILURES.load(Ordering::Relaxed)
885}
886
887pub(crate) fn record_structured_oversize_skip() {
893 let _receipt = record_scanner_coverage_gap(ScannerCoverageGapEvent::StructuredOversizeSkip);
894}
895
896pub fn structured_oversize_skip_count() -> usize {
899 STRUCTURED_OVERSIZE_SKIPS.load(Ordering::Relaxed)
900}
901
902pub(crate) fn record_decode_truncation() {
905 let _receipt = record_scanner_coverage_gap(ScannerCoverageGapEvent::DecodeTruncation);
906 #[cfg(test)]
907 THREAD_DECODE_TRUNCATIONS.with(|count| count.set(count.get() + 1));
908}
909
910#[cfg(not(test))]
912pub fn decode_truncation_count() -> usize {
913 DECODE_TRUNCATIONS.load(Ordering::Relaxed)
914}
915
916#[cfg(test)]
920pub fn decode_truncation_count() -> usize {
921 THREAD_DECODE_TRUNCATIONS.with(|count| count.get())
922}
923
924pub(crate) fn record_invalid_pattern_index_skip() {
927 let _receipt = record_scanner_coverage_gap(ScannerCoverageGapEvent::InvalidPatternIndexSkip);
928}
929
930pub fn invalid_pattern_index_skip_count() -> usize {
933 INVALID_PATTERN_INDEX_SKIPS.load(Ordering::Relaxed)
934}
935
936pub(crate) fn record_boundary_result_cardinality_mismatch() {
939 let _receipt =
940 record_scanner_coverage_gap(ScannerCoverageGapEvent::BoundaryResultCardinalityMismatch);
941}
942
943pub fn boundary_result_cardinality_mismatch_count() -> usize {
946 BOUNDARY_RESULT_CARDINALITY_MISMATCHES.load(Ordering::Relaxed)
947}
948
949#[cfg(feature = "multiline")]
952pub(crate) fn record_line_offset_mapping_mismatch() {
953 let _receipt = record_scanner_coverage_gap(ScannerCoverageGapEvent::LineOffsetMappingMismatch);
954}
955
956pub(crate) fn record_chunk_deadline_abort() {
958 let _receipt = record_scanner_coverage_gap(ScannerCoverageGapEvent::ChunkDeadlineAbort);
959}
960
961pub fn chunk_deadline_abort_count() -> usize {
963 CHUNK_DEADLINE_ABORTS.load(Ordering::Relaxed)
964}
965
966pub fn line_offset_mapping_mismatch_count() -> usize {
969 LINE_OFFSET_MAPPING_MISMATCHES.load(Ordering::Relaxed)
970}
971
972pub fn append_events<I: IntoIterator<Item = DogfoodEvent>>(events: I) {
977 append_event_details(events, true);
978}
979
980pub fn append_daemon_events<I: IntoIterator<Item = DogfoodEvent>>(events: I) {
986 append_event_details(events, false);
987}
988
989fn append_event_details<I: IntoIterator<Item = DogfoodEvent>>(
990 events: I,
991 infer_static_recovery_counts: bool,
992) {
993 let t = cell();
994 for event in events {
995 if infer_static_recovery_counts {
996 let DogfoodEvent::StaticRecoveryRejected { reason, .. } = &event else {
997 push_dogfood_detail(&t.events, &t.detail_events_dropped, event);
998 continue;
999 };
1000 if let Some(reason) = StaticRecoveryRejection::ALL
1001 .iter()
1002 .find(|candidate| candidate.as_str() == reason.as_ref())
1003 {
1004 t.static_recovery.record(*reason);
1005 }
1006 }
1007 push_dogfood_detail(&t.events, &t.detail_events_dropped, event);
1008 }
1009}
1010
1011pub fn merge_daemon_aggregates(
1018 static_recovery_rejections: &BTreeMap<String, u64>,
1019 detail_events_dropped: u64,
1020) -> Result<(), String> {
1021 let mut resolved = Vec::with_capacity(static_recovery_rejections.len());
1022 for (name, count) in static_recovery_rejections {
1023 let Some(reason) = StaticRecoveryRejection::ALL
1024 .iter()
1025 .copied()
1026 .find(|candidate| candidate.as_str() == name)
1027 else {
1028 return Err(format!(
1029 "daemon returned unknown static-recovery rejection reason {name:?}; restart it with this KeyHog build"
1030 ));
1031 };
1032 resolved.push((reason, *count));
1033 }
1034
1035 let telemetry = cell();
1036 for (reason, count) in resolved {
1037 telemetry.static_recovery.add(reason, count);
1038 }
1039 let dropped = usize::try_from(detail_events_dropped).unwrap_or(usize::MAX); let counter = &telemetry.detail_events_dropped;
1041 let mut current = counter.load(Ordering::Relaxed);
1042 while current != usize::MAX {
1043 match counter.compare_exchange_weak(
1044 current,
1045 current.saturating_add(dropped),
1046 Ordering::Relaxed,
1047 Ordering::Relaxed,
1048 ) {
1049 Ok(_) => break,
1050 Err(observed) => current = observed,
1051 }
1052 }
1053 Ok(())
1054}
1055
1056pub fn static_recovery_rejection_counts() -> BTreeMap<String, u64> {
1060 cell().static_recovery.snapshot()
1061}
1062
1063pub fn dogfood_detail_events_dropped() -> usize {
1065 cell().detail_events_dropped.load(Ordering::Relaxed)
1066}
1067
1068pub fn drain_events() -> Vec<DogfoodEvent> {
1071 let t = cell();
1072 drain_event_buffers(&t.events, &t.emitted_suppression_events)
1073}
1074
1075fn drain_event_buffers(
1076 events: &Mutex<Vec<DogfoodEvent>>,
1077 emitted_suppression_events: &Mutex<HashSet<EmittedDogfoodKey>>,
1078) -> Vec<DogfoodEvent> {
1079 recover_telemetry_lock(emitted_suppression_events).clear();
1083 std::mem::take(&mut *recover_telemetry_lock(events))
1084}
1085
1086pub(crate) fn record_file_scanned(bytes: usize) {
1088 FILES_SCANNED.fetch_add(1, Ordering::Relaxed);
1089 BYTES_SCANNED.fetch_add(bytes, Ordering::Relaxed);
1090}
1091
1092pub(crate) fn global_scan_counts() -> (usize, usize) {
1093 (
1094 FILES_SCANNED.load(Ordering::Relaxed),
1095 BYTES_SCANNED.load(Ordering::Relaxed),
1096 )
1097}
1098
1099pub(crate) fn record_file_skipped() {
1100 SKIPPED_FILES.fetch_add(1, Ordering::Relaxed);
1101}
1102
1103pub(crate) fn record_match_found() {
1104 TOTAL_MATCHES.fetch_add(1, Ordering::Relaxed);
1105}
1106
1107pub(crate) fn record_gpu_dispatch() {
1108 GPU_DISPATCHES.fetch_add(1, Ordering::Relaxed);
1109}
1110
1111pub fn reset_for_scan() {
1118 let t = cell();
1119 DOGFOOD_ENABLED.store(false, Ordering::Relaxed);
1120 t.dogfood_enabled.store(false, Ordering::Relaxed);
1121 t.example_suppressions.store(0, Ordering::Relaxed);
1122 t.detail_events_dropped.store(0, Ordering::Relaxed);
1123 t.static_recovery.reset();
1124 FILES_SCANNED.store(0, Ordering::Relaxed);
1125 BYTES_SCANNED.store(0, Ordering::Relaxed);
1126 SKIPPED_FILES.store(0, Ordering::Relaxed);
1127 TOTAL_MATCHES.store(0, Ordering::Relaxed);
1128 GPU_DISPATCHES.store(0, Ordering::Relaxed);
1129 for gap in ScannerCoverageGapEvent::ALL {
1130 gap.counter().store(0, Ordering::Relaxed);
1131 }
1132 #[cfg(test)]
1133 THREAD_DECODE_TRUNCATIONS.with(|count| count.set(0));
1134 recover_telemetry_lock(&t.events).clear();
1135 recover_telemetry_lock(&t.emitted_suppression_events).clear();
1136 CURRENT_SCAN_TELEMETRY.with(|slot| {
1137 *slot.borrow_mut() = None;
1138 });
1139}
1140
1141#[cfg(test)]
1142#[doc(hidden)]
1143pub mod testing {
1144 use std::sync::Arc;
1145
1146 pub fn reset() {
1148 super::reset_for_scan();
1149 }
1150
1151 pub(crate) fn poison_events(telemetry: &Arc<super::ScanTelemetry>) {
1152 let telemetry = Arc::clone(telemetry);
1153 let _ = std::thread::spawn(move || {
1154 let Ok(_events) = telemetry.events.lock() else {
1156 panic!("fresh telemetry event buffer was already poisoned");
1157 };
1158 panic!("poison scoped telemetry event buffer");
1159 })
1160 .join();
1161 }
1162}