use std::fmt;
use std::time::Duration;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum MlsVerbosity {
Off,
#[default]
Lifecycle,
Diagnostic,
}
#[non_exhaustive]
#[derive(Clone)]
pub struct TelemetryConfig {
pub(crate) scrub_ids: bool,
pub(crate) mls_verbosity: MlsVerbosity,
pub(crate) metrics_cadence: Option<Duration>,
pub(crate) scrub_secret: Option<[u8; 16]>,
pub(crate) routing_diagnostic: bool,
pub(crate) mls_sampling_bypass: bool,
}
impl fmt::Debug for TelemetryConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TelemetryConfig")
.field("scrub_ids", &self.scrub_ids)
.field("mls_verbosity", &self.mls_verbosity)
.field("metrics_cadence", &self.metrics_cadence)
.field(
"scrub_secret",
&self.scrub_secret.as_ref().map(|_| "<redacted>"),
)
.field("routing_diagnostic", &self.routing_diagnostic)
.field("mls_sampling_bypass", &self.mls_sampling_bypass)
.finish()
}
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
scrub_ids: true,
mls_verbosity: MlsVerbosity::Lifecycle,
metrics_cadence: Some(Duration::from_secs(5)),
scrub_secret: None,
routing_diagnostic: false,
mls_sampling_bypass: false,
}
}
}
impl TelemetryConfig {
pub fn with_scrub_ids(mut self, scrub_ids: bool) -> Self {
self.scrub_ids = scrub_ids;
self
}
pub fn with_mls_verbosity(mut self, mls_verbosity: MlsVerbosity) -> Self {
self.mls_verbosity = mls_verbosity;
self
}
pub fn with_metrics_cadence(mut self, cadence: Option<Duration>) -> Self {
self.metrics_cadence = cadence;
self
}
pub fn with_scrub_secret(mut self, secret: Option<[u8; 16]>) -> Self {
self.scrub_secret = secret;
self
}
pub fn scrub_ids(&self) -> bool {
self.scrub_ids
}
pub fn mls_verbosity(&self) -> MlsVerbosity {
self.mls_verbosity
}
pub fn metrics_cadence(&self) -> Option<Duration> {
self.metrics_cadence
}
pub fn with_routing_diagnostic(mut self, routing_diagnostic: bool) -> Self {
self.routing_diagnostic = routing_diagnostic;
self
}
pub fn routing_diagnostic(&self) -> bool {
self.routing_diagnostic
}
pub fn with_mls_sampling_bypass(mut self, mls_sampling_bypass: bool) -> Self {
self.mls_sampling_bypass = mls_sampling_bypass;
self
}
pub fn mls_sampling_bypass(&self) -> bool {
self.mls_sampling_bypass
}
#[allow(dead_code)]
pub(crate) fn scrub_secret(&self) -> Option<[u8; 16]> {
self.scrub_secret
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_locks_in_privacy_preserving_knobs() {
let cfg = TelemetryConfig::default();
assert!(cfg.scrub_ids());
assert_eq!(cfg.mls_verbosity(), MlsVerbosity::Lifecycle);
assert_eq!(cfg.metrics_cadence(), Some(Duration::from_secs(5)));
assert!(cfg.scrub_secret().is_none());
assert!(!cfg.routing_diagnostic());
assert!(!cfg.mls_sampling_bypass());
}
#[test]
fn builders_override_defaults() {
let cfg = TelemetryConfig::default()
.with_scrub_ids(false)
.with_mls_verbosity(MlsVerbosity::Off)
.with_metrics_cadence(None)
.with_scrub_secret(Some([7; 16]))
.with_routing_diagnostic(true)
.with_mls_sampling_bypass(true);
assert!(!cfg.scrub_ids());
assert_eq!(cfg.mls_verbosity(), MlsVerbosity::Off);
assert!(cfg.metrics_cadence().is_none());
assert_eq!(cfg.scrub_secret(), Some([7; 16]));
assert!(cfg.routing_diagnostic());
assert!(cfg.mls_sampling_bypass());
}
#[test]
fn metrics_cadence_is_typed_duration() {
let cfg = TelemetryConfig::default().with_metrics_cadence(Some(Duration::from_millis(250)));
assert_eq!(cfg.metrics_cadence(), Some(Duration::from_millis(250)));
}
#[test]
fn debug_redacts_scrub_secret() {
let cfg = TelemetryConfig::default().with_scrub_secret(Some([0xAB; 16]));
let rendered = format!("{cfg:?}");
assert!(
rendered.contains("<redacted>"),
"expected redaction marker, got {rendered}"
);
assert!(
!rendered.contains("ab, ab") && !rendered.contains("171, 171"),
"scrub_secret bytes leaked into Debug output: {rendered}"
);
}
#[test]
fn debug_shows_none_when_no_secret() {
let cfg = TelemetryConfig::default();
let rendered = format!("{cfg:?}");
assert!(rendered.contains("scrub_secret: None"), "got {rendered}");
}
}