Skip to main content

confium_observability/
observability_and_enterprise.rs

1//! Observability + security assurance + data management + enterprise integration.
2//!
3//! Trace correlation, structured logging, metric cardinality, RNG testing,
4//! zeroization audit, encrypted backup, retention, replication, KMS, syslog.
5
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::sync::Mutex;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12// === Distributed Trace Correlation ===
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TraceContext {
16    pub trace_id: String,
17    pub span_id: String,
18    pub parent_span_id: Option<String>,
19    pub baggage: HashMap<String, String>,
20}
21
22impl Default for TraceContext {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl TraceContext {
29    pub fn new() -> Self {
30        use rand_core::{OsRng, RngCore};
31        let mut trace_bytes = [0u8; 16];
32        let mut span_bytes = [0u8; 8];
33        OsRng.fill_bytes(&mut trace_bytes);
34        OsRng.fill_bytes(&mut span_bytes);
35        Self {
36            trace_id: hex::encode(trace_bytes),
37            span_id: hex::encode(span_bytes),
38            parent_span_id: None,
39            baggage: HashMap::new(),
40        }
41    }
42
43    pub fn child(&self) -> Self {
44        use rand_core::{OsRng, RngCore};
45        let mut span_bytes = [0u8; 8];
46        OsRng.fill_bytes(&mut span_bytes);
47        Self {
48            trace_id: self.trace_id.clone(),
49            span_id: hex::encode(span_bytes),
50            parent_span_id: Some(self.span_id.clone()),
51            baggage: self.baggage.clone(),
52        }
53    }
54
55    pub fn to_w3c_header(&self) -> String {
56        format!("00-{}-{}-01", self.trace_id, self.span_id)
57    }
58
59    pub fn from_w3c_header(header: &str) -> Option<Self> {
60        let parts: Vec<&str> = header.split('-').collect();
61        if parts.len() != 4 {
62            return None;
63        }
64        Some(Self {
65            trace_id: parts[1].into(),
66            span_id: parts[2].into(),
67            parent_span_id: None,
68            baggage: HashMap::new(),
69        })
70    }
71
72    pub fn add_baggage(&mut self, key: &str, value: &str) {
73        self.baggage.insert(key.into(), value.into());
74    }
75}
76
77// === Structured JSON Logging ===
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct LogEntry {
81    pub timestamp: DateTime<Utc>,
82    pub level: String,
83    pub message: String,
84    pub fields: HashMap<String, String>,
85    pub trace_id: Option<String>,
86}
87
88impl LogEntry {
89    pub fn info(msg: &str) -> Self {
90        Self {
91            timestamp: Utc::now(),
92            level: "info".into(),
93            message: msg.into(),
94            fields: HashMap::new(),
95            trace_id: None,
96        }
97    }
98
99    pub fn error(msg: &str) -> Self {
100        Self {
101            timestamp: Utc::now(),
102            level: "error".into(),
103            message: msg.into(),
104            fields: HashMap::new(),
105            trace_id: None,
106        }
107    }
108
109    pub fn warn(msg: &str) -> Self {
110        Self {
111            timestamp: Utc::now(),
112            level: "warn".into(),
113            message: msg.into(),
114            fields: HashMap::new(),
115            trace_id: None,
116        }
117    }
118
119    pub fn field(mut self, key: &str, value: &str) -> Self {
120        self.fields.insert(key.into(), value.into());
121        self
122    }
123
124    pub fn with_trace(mut self, trace_id: &str) -> Self {
125        self.trace_id = Some(trace_id.into());
126        self
127    }
128
129    pub fn to_json(&self) -> String {
130        serde_json::to_string(self).unwrap_or_else(|_| "{}".into())
131    }
132
133    pub fn to_jsonl(&self) -> String {
134        self.to_json() + "\n"
135    }
136}
137
138#[derive(Default)]
139pub struct StructuredLogger {
140    entries: Mutex<Vec<LogEntry>>,
141    min_level: Mutex<String>,
142}
143
144impl StructuredLogger {
145    pub fn new(min_level: &str) -> Self {
146        Self {
147            entries: Mutex::new(Vec::new()),
148            min_level: Mutex::new(min_level.into()),
149        }
150    }
151
152    pub fn log(&self, entry: LogEntry) {
153        let levels = ["trace", "debug", "info", "warn", "error"];
154        let min_idx = levels
155            .iter()
156            .position(|&l| l == *self.min_level.lock().unwrap())
157            .unwrap_or(2);
158        let entry_idx = levels
159            .iter()
160            .position(|&l| l == entry.level.as_str())
161            .unwrap_or(2);
162        if entry_idx >= min_idx {
163            self.entries.lock().unwrap().push(entry);
164        }
165    }
166
167    pub fn entries(&self) -> Vec<LogEntry> {
168        self.entries.lock().unwrap().clone()
169    }
170    pub fn count(&self) -> usize {
171        self.entries.lock().unwrap().len()
172    }
173    pub fn to_jsonl(&self) -> String {
174        self.entries
175            .lock()
176            .unwrap()
177            .iter()
178            .map(|e| e.to_jsonl())
179            .collect()
180    }
181    pub fn flush(&self) -> Vec<LogEntry> {
182        let mut entries = self.entries.lock().unwrap();
183        std::mem::take(&mut *entries)
184    }
185}
186
187// === Metric Cardinality Limiter ===
188
189pub struct CardinalityLimiter {
190    label_counts: Mutex<HashMap<String, HashSet<String>>>,
191    max_values_per_label: usize,
192    total_series: AtomicU64,
193    max_total_series: u64,
194}
195
196impl CardinalityLimiter {
197    pub fn new(max_values_per_label: usize, max_total_series: u64) -> Self {
198        Self {
199            label_counts: Mutex::new(HashMap::new()),
200            max_values_per_label,
201            total_series: AtomicU64::new(0),
202            max_total_series,
203        }
204    }
205
206    pub fn allow_label(&self, label_name: &str, label_value: &str) -> bool {
207        if self.total_series.load(Ordering::SeqCst) >= self.max_total_series {
208            return false;
209        }
210        let mut counts = self.label_counts.lock().unwrap();
211        let values = counts.entry(label_name.into()).or_default();
212        if values.contains(label_value) {
213            return true;
214        }
215        if values.len() >= self.max_values_per_label {
216            return false;
217        }
218        values.insert(label_value.into());
219        self.total_series.fetch_add(1, Ordering::SeqCst);
220        true
221    }
222
223    pub fn label_value_count(&self, label: &str) -> usize {
224        self.label_counts
225            .lock()
226            .unwrap()
227            .get(label)
228            .map(|s| s.len())
229            .unwrap_or(0)
230    }
231
232    pub fn total_series(&self) -> u64 {
233        self.total_series.load(Ordering::SeqCst)
234    }
235    pub fn reset(&self) {
236        self.label_counts.lock().unwrap().clear();
237        self.total_series.store(0, Ordering::SeqCst);
238    }
239}
240
241// === RNG Statistical Testing (NIST SP 800-22 simplified) ===
242
243pub fn frequency_test(data: &[u8]) -> f64 {
244    let n = data.len() as f64 * 8.0;
245    let mut s: f64 = 0.0;
246    for byte in data {
247        for bit in 0..8 {
248            s += if (byte >> bit) & 1 == 1 { 1.0 } else { -1.0 };
249        }
250    }
251    let s_obs = s.abs() / n.sqrt();
252    erfc(s_obs / 2f64.sqrt())
253}
254
255pub fn runs_test(data: &[u8]) -> f64 {
256    let bits: Vec<i8> = data
257        .iter()
258        .flat_map(|b| (0..8).map(move |i| ((b >> i) & 1) as i8))
259        .collect();
260    let n = bits.len() as f64;
261    let pi: f64 = bits.iter().map(|&b| b as f64).sum::<f64>() / n;
262    if (pi - 0.5).abs() >= 2.0 / n.sqrt() {
263        return 0.0;
264    }
265    let mut v_obs = 1.0;
266    for i in 1..bits.len() {
267        if bits[i] != bits[i - 1] {
268            v_obs += 1.0;
269        }
270    }
271    let denom = 2.0 * pi * (1.0 - pi);
272    let s = (2.0 * (n.sqrt())) * denom;
273    erfc((v_obs - s) / (2.0 * s * denom.sqrt()))
274}
275
276pub fn entropy_estimate(data: &[u8]) -> f64 {
277    let mut counts = [0u32; 256];
278    for &b in data {
279        counts[b as usize] += 1;
280    }
281    let n = data.len() as f64;
282    let mut entropy = 0.0;
283    for &count in &counts {
284        if count > 0 {
285            let p = count as f64 / n;
286            entropy -= p * p.log2();
287        }
288    }
289    entropy
290}
291
292fn erfc(x: f64) -> f64 {
293    // Approximate complementary error function
294    let z = x.abs();
295    let t = 1.0 / (1.0 + 0.5 * z);
296    let r = t
297        * (-z * z - 1.26551223
298            + t * (1.00002368
299                + t * (-0.37609177
300                    + t * (0.36496351
301                        + t * (-0.16108111 + t * (0.08095456 + t * (-0.02907307)))))))
302            .exp();
303    if x >= 0.0 { r } else { 2.0 - r }
304}
305
306// === Memory Zeroization Audit ===
307
308pub struct ZeroizationAuditor {
309    tracked_secrets: Mutex<Vec<SecretRecord>>,
310}
311
312#[derive(Debug, Clone)]
313pub struct SecretRecord {
314    pub id: String,
315    pub allocated_at: DateTime<Utc>,
316    pub zeroized: bool,
317    pub size_bytes: usize,
318}
319
320impl ZeroizationAuditor {
321    pub fn new() -> Self {
322        Self {
323            tracked_secrets: Mutex::new(Vec::new()),
324        }
325    }
326
327    pub fn track(&self, id: &str, size: usize) {
328        self.tracked_secrets.lock().unwrap().push(SecretRecord {
329            id: id.into(),
330            allocated_at: Utc::now(),
331            zeroized: false,
332            size_bytes: size,
333        });
334    }
335
336    pub fn mark_zeroized(&self, id: &str) {
337        if let Some(s) = self
338            .tracked_secrets
339            .lock()
340            .unwrap()
341            .iter_mut()
342            .find(|s| s.id == id)
343        {
344            s.zeroized = true;
345        }
346    }
347
348    pub fn unzeroized_count(&self) -> usize {
349        self.tracked_secrets
350            .lock()
351            .unwrap()
352            .iter()
353            .filter(|s| !s.zeroized)
354            .count()
355    }
356
357    pub fn audit_report(&self) -> ZeroizationReport {
358        let secrets = self.tracked_secrets.lock().unwrap();
359        let total = secrets.len();
360        let zeroized = secrets.iter().filter(|s| s.zeroized).count();
361        ZeroizationReport {
362            total_secrets: total,
363            zeroized,
364            unzeroized: total - zeroized,
365        }
366    }
367}
368
369impl Default for ZeroizationAuditor {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct ZeroizationReport {
377    pub total_secrets: usize,
378    pub zeroized: usize,
379    pub unzeroized: usize,
380}
381
382// === Encrypted Share Backup ===
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct EncryptedBackup {
386    pub backup_id: String,
387    pub encrypted_shares_hex: Vec<String>,
388    pub backup_key_threshold: u32,
389    pub created_at: DateTime<Utc>,
390}
391
392pub struct BackupManager {
393    backups: Mutex<HashMap<String, EncryptedBackup>>,
394}
395
396impl BackupManager {
397    pub fn new() -> Self {
398        Self {
399            backups: Mutex::new(HashMap::new()),
400        }
401    }
402
403    pub fn create_backup(
404        &self,
405        backup_id: &str,
406        shares: &[Vec<u8>],
407        threshold: u32,
408    ) -> EncryptedBackup {
409        use rand_core::{OsRng, RngCore};
410        let encrypted: Vec<String> = shares
411            .iter()
412            .map(|s| {
413                let mut key = [0u8; 32];
414                OsRng.fill_bytes(&mut key);
415                let encrypted: Vec<u8> = s
416                    .iter()
417                    .enumerate()
418                    .map(|(i, &b)| b ^ key[i % key.len()])
419                    .collect();
420                hex::encode(encrypted)
421            })
422            .collect();
423        let backup = EncryptedBackup {
424            backup_id: backup_id.into(),
425            encrypted_shares_hex: encrypted,
426            backup_key_threshold: threshold,
427            created_at: Utc::now(),
428        };
429        self.backups
430            .lock()
431            .unwrap()
432            .insert(backup_id.into(), backup.clone());
433        backup
434    }
435
436    pub fn restore_backup(&self, backup_id: &str, keys: &[[u8; 32]; 32]) -> Option<Vec<Vec<u8>>> {
437        let backups = self.backups.lock().unwrap();
438        let backup = backups.get(backup_id)?;
439        if keys.len() < backup.backup_key_threshold as usize {
440            return None;
441        }
442        // Simplified restore: XOR with keys (mock)
443        let restored: Vec<Vec<u8>> = backup
444            .encrypted_shares_hex
445            .iter()
446            .map(|hex_str| {
447                let encrypted = hex::decode(hex_str).unwrap_or_default();
448                let key = &keys[0];
449                encrypted
450                    .iter()
451                    .enumerate()
452                    .map(|(i, &b)| b ^ key[i % key.len()])
453                    .collect()
454            })
455            .collect();
456        Some(restored)
457    }
458
459    pub fn backup_count(&self) -> usize {
460        self.backups.lock().unwrap().len()
461    }
462    pub fn list_backups(&self) -> Vec<String> {
463        self.backups.lock().unwrap().keys().cloned().collect()
464    }
465}
466
467impl Default for BackupManager {
468    fn default() -> Self {
469        Self::new()
470    }
471}
472
473// === Data Retention Policy Engine ===
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct RetentionPolicy {
477    pub session_retention_days: u32,
478    pub audit_retention_days: u32,
479    pub wal_retention_entries: u64,
480    pub purge_interval_hours: u32,
481}
482
483impl Default for RetentionPolicy {
484    fn default() -> Self {
485        Self {
486            session_retention_days: 7,
487            audit_retention_days: 365,
488            wal_retention_entries: 100_000,
489            purge_interval_hours: 1,
490        }
491    }
492}
493
494pub struct RetentionEngine {
495    policy: RetentionPolicy,
496    purged_sessions: AtomicU64,
497    purged_audit_entries: AtomicU64,
498    last_purge: Mutex<Option<DateTime<Utc>>>,
499}
500
501impl RetentionEngine {
502    pub fn new(policy: RetentionPolicy) -> Self {
503        Self {
504            policy,
505            purged_sessions: AtomicU64::new(0),
506            purged_audit_entries: AtomicU64::new(0),
507            last_purge: Mutex::new(None),
508        }
509    }
510
511    pub fn should_purge(&self) -> bool {
512        if let Some(last) = *self.last_purge.lock().unwrap() {
513            Utc::now() - last >= Duration::hours(self.policy.purge_interval_hours as i64)
514        } else {
515            true
516        }
517    }
518
519    pub fn purge_sessions(&self, sessions: &mut Vec<(String, DateTime<Utc>)>) -> usize {
520        let cutoff = Utc::now() - Duration::days(self.policy.session_retention_days as i64);
521        let before = sessions.len();
522        sessions.retain(|(_, ts)| *ts > cutoff);
523        let purged = before - sessions.len();
524        self.purged_sessions
525            .fetch_add(purged as u64, Ordering::SeqCst);
526        *self.last_purge.lock().unwrap() = Some(Utc::now());
527        purged
528    }
529
530    pub fn truncate_wal(&self, wal: &mut Vec<String>) -> usize {
531        let max = self.policy.wal_retention_entries as usize;
532        if wal.len() <= max {
533            return 0;
534        }
535        let excess = wal.len() - max;
536        wal.drain(..excess);
537        self.purged_audit_entries
538            .fetch_add(excess as u64, Ordering::SeqCst);
539        excess
540    }
541
542    pub fn total_purged_sessions(&self) -> u64 {
543        self.purged_sessions.load(Ordering::SeqCst)
544    }
545    pub fn total_purged_entries(&self) -> u64 {
546        self.purged_audit_entries.load(Ordering::SeqCst)
547    }
548}
549
550// === Cross-Region Replication ===
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct ReplicaState {
554    pub region: String,
555    pub last_seq: u64,
556    pub last_sync: DateTime<Utc>,
557    pub lag_seconds: i64,
558}
559
560pub struct ReplicationManager {
561    replicas: Mutex<HashMap<String, ReplicaState>>,
562    local_seq: AtomicU64,
563}
564
565impl ReplicationManager {
566    pub fn new() -> Self {
567        Self {
568            replicas: Mutex::new(HashMap::new()),
569            local_seq: AtomicU64::new(0),
570        }
571    }
572
573    pub fn register_replica(&self, region: &str) {
574        self.replicas.lock().unwrap().insert(
575            region.into(),
576            ReplicaState {
577                region: region.into(),
578                last_seq: 0,
579                last_sync: Utc::now(),
580                lag_seconds: 0,
581            },
582        );
583    }
584
585    pub fn record_replication(&self, region: &str, seq: u64) {
586        let mut replicas = self.replicas.lock().unwrap();
587        if let Some(r) = replicas.get_mut(region) {
588            r.last_seq = seq;
589            r.last_sync = Utc::now();
590            let _local = self.local_seq.load(Ordering::SeqCst);
591            r.lag_seconds = (Utc::now() - r.last_sync).num_seconds();
592        }
593    }
594
595    pub fn advance_local(&self) -> u64 {
596        self.local_seq.fetch_add(1, Ordering::SeqCst) + 1
597    }
598    pub fn local_seq(&self) -> u64 {
599        self.local_seq.load(Ordering::SeqCst)
600    }
601
602    pub fn replica_lag(&self, region: &str) -> Option<u64> {
603        let replicas = self.replicas.lock().unwrap();
604        let r = replicas.get(region)?;
605        Some(self.local_seq.load(Ordering::SeqCst) - r.last_seq)
606    }
607
608    pub fn replica_count(&self) -> usize {
609        self.replicas.lock().unwrap().len()
610    }
611    pub fn all_replicas(&self) -> Vec<ReplicaState> {
612        self.replicas.lock().unwrap().values().cloned().collect()
613    }
614}
615
616impl Default for ReplicationManager {
617    fn default() -> Self {
618        Self::new()
619    }
620}
621
622// === Cloud KMS Integration Trait ===
623
624pub trait CloudKms: Send + Sync {
625    fn encrypt(&self, plaintext: &[u8], key_id: &str) -> Result<Vec<u8>, String>;
626    fn decrypt(&self, ciphertext: &[u8], key_id: &str) -> Result<Vec<u8>, String>;
627    fn create_key(&self, key_id: &str) -> Result<(), String>;
628    fn delete_key(&self, key_id: &str) -> Result<(), String>;
629    fn key_exists(&self, key_id: &str) -> bool;
630    fn name(&self) -> &str;
631}
632
633pub struct MockKms {
634    keys: Mutex<HashMap<String, [u8; 32]>>,
635}
636
637impl MockKms {
638    pub fn new() -> Self {
639        Self {
640            keys: Mutex::new(HashMap::new()),
641        }
642    }
643}
644
645impl CloudKms for MockKms {
646    fn encrypt(&self, plaintext: &[u8], key_id: &str) -> Result<Vec<u8>, String> {
647        let keys = self.keys.lock().unwrap();
648        let key = keys.get(key_id).ok_or("key not found")?;
649        Ok(plaintext
650            .iter()
651            .enumerate()
652            .map(|(i, &b)| b ^ key[i % 32])
653            .collect())
654    }
655    fn decrypt(&self, ciphertext: &[u8], key_id: &str) -> Result<Vec<u8>, String> {
656        self.encrypt(ciphertext, key_id) // XOR is symmetric
657    }
658    fn create_key(&self, key_id: &str) -> Result<(), String> {
659        use rand_core::{OsRng, RngCore};
660        let mut key = [0u8; 32];
661        OsRng.fill_bytes(&mut key);
662        self.keys.lock().unwrap().insert(key_id.into(), key);
663        Ok(())
664    }
665    fn delete_key(&self, key_id: &str) -> Result<(), String> {
666        self.keys.lock().unwrap().remove(key_id);
667        Ok(())
668    }
669    fn key_exists(&self, key_id: &str) -> bool {
670        self.keys.lock().unwrap().contains_key(key_id)
671    }
672    fn name(&self) -> &str {
673        "mock-kms"
674    }
675}
676
677impl Default for MockKms {
678    fn default() -> Self {
679        Self::new()
680    }
681}
682
683// === Syslog Forwarding ===
684
685#[derive(Debug, Clone, Serialize, Deserialize)]
686pub struct SyslogMessage {
687    pub facility: u8,
688    pub severity: u8,
689    pub timestamp: DateTime<Utc>,
690    pub hostname: String,
691    pub app_name: String,
692    pub proc_id: u32,
693    pub msg_id: String,
694    pub message: String,
695}
696
697impl SyslogMessage {
698    pub fn new(severity: u8, message: &str) -> Self {
699        Self {
700            facility: 4,
701            severity,
702            timestamp: Utc::now(),
703            hostname: "confium".into(),
704            app_name: "coordinator".into(),
705            proc_id: 1,
706            msg_id: "audit".into(),
707            message: message.into(),
708        }
709    }
710
711    pub fn priority(&self) -> u8 {
712        self.facility * 8 + self.severity
713    }
714
715    pub fn to_rfc5424(&self) -> String {
716        format!(
717            "<{}>1 {} {} {} {} {} {}",
718            self.priority(),
719            self.timestamp.format("%Y-%m-%dT%H:%M:%SZ"),
720            self.hostname,
721            self.app_name,
722            self.proc_id,
723            self.msg_id,
724            self.message
725        )
726    }
727}
728
729#[derive(Default)]
730pub struct SyslogForwarder {
731    sent: Mutex<Vec<SyslogMessage>>,
732    enabled: Mutex<bool>,
733}
734
735impl SyslogForwarder {
736    pub fn new() -> Self {
737        Self {
738            sent: Mutex::new(Vec::new()),
739            enabled: Mutex::new(true),
740        }
741    }
742
743    pub fn forward(&self, msg: SyslogMessage) -> bool {
744        if !*self.enabled.lock().unwrap() {
745            return false;
746        }
747        self.sent.lock().unwrap().push(msg);
748        true
749    }
750
751    pub fn forward_audit(&self, event: &str) -> bool {
752        self.forward(SyslogMessage::new(5, event)) // 5 = notice
753    }
754
755    pub fn forward_error(&self, error: &str) -> bool {
756        self.forward(SyslogMessage::new(3, error)) // 3 = error
757    }
758
759    pub fn sent_count(&self) -> usize {
760        self.sent.lock().unwrap().len()
761    }
762    pub fn set_enabled(&self, enabled: bool) {
763        *self.enabled.lock().unwrap() = enabled;
764    }
765    pub fn messages(&self) -> Vec<SyslogMessage> {
766        self.sent.lock().unwrap().clone()
767    }
768    pub fn flush(&self) -> Vec<SyslogMessage> {
769        let mut sent = self.sent.lock().unwrap();
770        std::mem::take(&mut *sent)
771    }
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    // Trace correlation
779    #[test]
780    fn trace_context_child_inherits_trace_id() {
781        let parent = TraceContext::new();
782        let child = parent.child();
783        assert_eq!(parent.trace_id, child.trace_id);
784        assert_eq!(child.parent_span_id, Some(parent.span_id.clone()));
785    }
786
787    #[test]
788    fn trace_w3c_header_round_trips() {
789        let ctx = TraceContext::new();
790        let header = ctx.to_w3c_header();
791        let parsed = TraceContext::from_w3c_header(&header).unwrap();
792        assert_eq!(ctx.trace_id, parsed.trace_id);
793    }
794
795    #[test]
796    fn trace_baggage() {
797        let mut ctx = TraceContext::new();
798        ctx.add_baggage("user", "alice");
799        let child = ctx.child();
800        assert_eq!(child.baggage.get("user"), Some(&"alice".to_string()));
801    }
802
803    // Structured logging
804    #[test]
805    fn log_entry_to_json() {
806        let entry = LogEntry::info("test message").field("key", "value");
807        let json = entry.to_json();
808        assert!(json.contains("test message"));
809        assert!(json.contains("\"key\":\"value\""));
810    }
811
812    #[test]
813    fn logger_filters_by_level() {
814        let logger = StructuredLogger::new("warn");
815        logger.log(LogEntry::info("info msg"));
816        logger.log(LogEntry::warn("warn msg"));
817        logger.log(LogEntry::error("error msg"));
818        assert_eq!(logger.count(), 2); // info filtered out
819    }
820
821    #[test]
822    fn logger_jsonl_output() {
823        let logger = StructuredLogger::new("info");
824        logger.log(LogEntry::info("msg1"));
825        logger.log(LogEntry::info("msg2"));
826        let jsonl = logger.to_jsonl();
827        assert!(jsonl.contains("msg1"));
828        assert!(jsonl.contains("msg2"));
829    }
830
831    // Metric cardinality
832    #[test]
833    fn cardinality_allows_new_values() {
834        let limiter = CardinalityLimiter::new(5, 100);
835        assert!(limiter.allow_label("user", "alice"));
836        assert!(limiter.allow_label("user", "bob"));
837        assert_eq!(limiter.label_value_count("user"), 2);
838    }
839
840    #[test]
841    fn cardinality_rejects_excess() {
842        let limiter = CardinalityLimiter::new(2, 100);
843        assert!(limiter.allow_label("ip", "1.1.1.1"));
844        assert!(limiter.allow_label("ip", "2.2.2.2"));
845        assert!(!limiter.allow_label("ip", "3.3.3.3"));
846    }
847
848    #[test]
849    fn cardinality_total_series_limit() {
850        let limiter = CardinalityLimiter::new(100, 3);
851        limiter.allow_label("a", "1");
852        limiter.allow_label("a", "2");
853        limiter.allow_label("a", "3");
854        assert_eq!(limiter.total_series(), 3);
855        assert!(!limiter.allow_label("a", "4"));
856    }
857
858    // RNG testing
859    #[test]
860    fn frequency_test_random_data() {
861        use rand_core::RngCore;
862        let mut data = vec![0u8; 1000];
863        rand_core::OsRng.fill_bytes(&mut data);
864        let p_value = frequency_test(&data);
865        // p-value threshold 0.001 (1 in 1000) — at 0.01 the test would
866        // flap ~1% of runs by definition. Even at 0.001 the test is
867        // probabilistic; treat values in the borderline range as
868        // informational rather than a hard failure.
869        if p_value <= 0.001 {
870            panic!("p-value {p_value} is far below the random-data expectation");
871        }
872    }
873
874    #[test]
875    fn frequency_test_all_zeros() {
876        let data = vec![0u8; 100];
877        let p_value = frequency_test(&data);
878        assert!(p_value < 0.01, "p-value should be < 0.01 for all-zeros");
879    }
880
881    #[test]
882    fn entropy_random_is_high() {
883        use rand_core::RngCore;
884        let mut data = vec![0u8; 1000];
885        rand_core::OsRng.fill_bytes(&mut data);
886        let entropy = entropy_estimate(&data);
887        assert!(
888            entropy > 7.0,
889            "entropy should be > 7.0 bits/byte for random data"
890        );
891    }
892
893    #[test]
894    fn entropy_constant_is_low() {
895        let data = vec![0x42u8; 1000];
896        let entropy = entropy_estimate(&data);
897        assert!(entropy < 1.0);
898    }
899
900    // Zeroization audit
901    #[test]
902    fn audit_tracks_secrets() {
903        let auditor = ZeroizationAuditor::new();
904        auditor.track("s1", 32);
905        auditor.track("s2", 64);
906        assert_eq!(auditor.unzeroized_count(), 2);
907        auditor.mark_zeroized("s1");
908        assert_eq!(auditor.unzeroized_count(), 1);
909    }
910
911    #[test]
912    fn audit_report() {
913        let auditor = ZeroizationAuditor::new();
914        auditor.track("s1", 32);
915        auditor.track("s2", 64);
916        auditor.mark_zeroized("s1");
917        let report = auditor.audit_report();
918        assert_eq!(report.total_secrets, 2);
919        assert_eq!(report.zeroized, 1);
920        assert_eq!(report.unzeroized, 1);
921    }
922
923    // Encrypted backup
924    #[test]
925    fn backup_create_and_list() {
926        let mgr = BackupManager::new();
927        mgr.create_backup("b1", &[vec![0xAA; 32], vec![0xBB; 32]], 2);
928        assert_eq!(mgr.backup_count(), 1);
929        assert!(mgr.list_backups().contains(&"b1".to_string()));
930    }
931
932    // Retention
933    #[test]
934    fn retention_purges_old_sessions() {
935        let engine = RetentionEngine::new(RetentionPolicy {
936            session_retention_days: 1,
937            ..Default::default()
938        });
939        let mut sessions = vec![
940            ("s1".into(), Utc::now() - Duration::days(5)),
941            ("s2".into(), Utc::now()),
942        ];
943        let purged = engine.purge_sessions(&mut sessions);
944        assert_eq!(purged, 1);
945        assert_eq!(sessions.len(), 1);
946    }
947
948    #[test]
949    fn retention_truncates_wal() {
950        let engine = RetentionEngine::new(RetentionPolicy {
951            wal_retention_entries: 5,
952            ..Default::default()
953        });
954        let mut wal: Vec<String> = (0..10).map(|i| format!("entry-{i}")).collect();
955        let purged = engine.truncate_wal(&mut wal);
956        assert_eq!(purged, 5);
957        assert_eq!(wal.len(), 5);
958    }
959
960    // Replication
961    #[test]
962    fn replication_tracks_lag() {
963        let mgr = ReplicationManager::new();
964        mgr.register_replica("us-east");
965        mgr.advance_local();
966        mgr.advance_local();
967        mgr.advance_local();
968        mgr.record_replication("us-east", 1);
969        assert_eq!(mgr.replica_lag("us-east"), Some(2));
970    }
971
972    #[test]
973    fn replication_multiple_regions() {
974        let mgr = ReplicationManager::new();
975        mgr.register_replica("us-east");
976        mgr.register_replica("eu-west");
977        assert_eq!(mgr.replica_count(), 2);
978    }
979
980    // KMS
981    #[test]
982    fn kms_encrypt_decrypt() {
983        let kms = MockKms::new();
984        kms.create_key("key1").unwrap();
985        let ct = kms.encrypt(b"secret", "key1").unwrap();
986        let pt = kms.decrypt(&ct, "key1").unwrap();
987        assert_eq!(pt, b"secret");
988    }
989
990    #[test]
991    fn kms_key_exists() {
992        let kms = MockKms::new();
993        assert!(!kms.key_exists("k1"));
994        kms.create_key("k1").unwrap();
995        assert!(kms.key_exists("k1"));
996        kms.delete_key("k1").unwrap();
997        assert!(!kms.key_exists("k1"));
998    }
999
1000    // Syslog
1001    #[test]
1002    fn syslog_format_rfc5424() {
1003        let msg = SyslogMessage::new(5, "test event");
1004        let formatted = msg.to_rfc5424();
1005        assert!(formatted.starts_with("<"));
1006        assert!(formatted.contains("test event"));
1007    }
1008
1009    #[test]
1010    fn syslog_forward_audit() {
1011        let fwd = SyslogForwarder::new();
1012        fwd.forward_audit("session created");
1013        fwd.forward_error("signing failed");
1014        assert_eq!(fwd.sent_count(), 2);
1015    }
1016
1017    #[test]
1018    fn syslog_disabled() {
1019        let fwd = SyslogForwarder::new();
1020        fwd.set_enabled(false);
1021        fwd.forward_audit("test");
1022        assert_eq!(fwd.sent_count(), 0);
1023    }
1024
1025    #[test]
1026    fn syslog_flush() {
1027        let fwd = SyslogForwarder::new();
1028        fwd.forward_audit("msg1");
1029        fwd.forward_audit("msg2");
1030        let flushed = fwd.flush();
1031        assert_eq!(flushed.len(), 2);
1032        assert_eq!(fwd.sent_count(), 0);
1033    }
1034}