use opentelemetry::Context;
use opentelemetry_sdk::error::OTelSdkResult;
use opentelemetry_sdk::trace::{SpanData, SpanProcessor};
use std::time::Duration;
fn scrub_string_attributes(attrs: &mut Vec<opentelemetry::KeyValue>) {
for attr in attrs {
if let opentelemetry::Value::String(ref mut s) = attr.value {
let scrubbed = zeph_core::redact::scrub_content(s.as_str());
if let std::borrow::Cow::Owned(new_val) = scrubbed {
*s = new_val.into();
}
}
}
}
#[derive(Debug)]
pub(crate) struct RedactingSpanProcessor<P: SpanProcessor> {
inner: P,
}
impl<P: SpanProcessor> RedactingSpanProcessor<P> {
pub(crate) fn new(inner: P) -> Self {
Self { inner }
}
}
impl<P: SpanProcessor> SpanProcessor for RedactingSpanProcessor<P> {
fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &Context) {
self.inner.on_start(span, cx);
}
fn on_end(&self, mut span: SpanData) {
scrub_string_attributes(&mut span.attributes);
for event in &mut span.events.events {
scrub_string_attributes(&mut event.attributes);
}
self.inner.on_end(span);
}
fn force_flush(&self) -> OTelSdkResult {
self.inner.force_flush()
}
fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult {
self.inner.shutdown_with_timeout(timeout)
}
}
#[cfg(test)]
mod tests {
use super::*;
use opentelemetry::{KeyValue, Value};
use opentelemetry_sdk::error::OTelSdkResult;
use opentelemetry_sdk::trace::{SpanData, SpanProcessor};
use std::sync::{Arc, Mutex};
#[derive(Debug, Default)]
struct CapturingProcessor {
captured: Arc<Mutex<Option<SpanData>>>,
}
impl SpanProcessor for CapturingProcessor {
fn on_start(
&self,
_span: &mut opentelemetry_sdk::trace::Span,
_cx: &opentelemetry::Context,
) {
}
fn on_end(&self, span: SpanData) {
*self.captured.lock().unwrap() = Some(span);
}
fn force_flush(&self) -> OTelSdkResult {
Ok(())
}
fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
Ok(())
}
}
fn make_span_data(attrs: Vec<KeyValue>) -> SpanData {
use opentelemetry::trace::{
SpanContext, SpanId, SpanKind, TraceFlags, TraceId, TraceState,
};
use opentelemetry_sdk::trace::{SpanEvents, SpanLinks};
SpanData {
span_context: SpanContext::new(
TraceId::from(1_u128),
SpanId::from(1_u64),
TraceFlags::SAMPLED,
false,
TraceState::default(),
),
dropped_attributes_count: 0,
parent_span_id: SpanId::INVALID,
parent_span_is_remote: false,
span_kind: SpanKind::Internal,
name: "test".into(),
start_time: std::time::SystemTime::UNIX_EPOCH,
end_time: std::time::SystemTime::UNIX_EPOCH,
attributes: attrs,
events: SpanEvents::default(),
links: SpanLinks::default(),
status: opentelemetry::trace::Status::Unset,
instrumentation_scope: opentelemetry::InstrumentationScope::builder("test").build(),
}
}
#[test]
fn string_attribute_with_secret_is_scrubbed() {
let captured = Arc::new(Mutex::new(None));
let inner = CapturingProcessor {
captured: Arc::clone(&captured),
};
let processor = RedactingSpanProcessor::new(inner);
let secret_val = "Authorization: Bearer sk-proj-abcdefghijklmnopqrstuvwxyz0123456789";
let span = make_span_data(vec![KeyValue::new("auth_header", secret_val)]);
processor.on_end(span);
let guard = captured.lock().unwrap();
let result = guard.as_ref().unwrap();
if let Value::String(ref s) = result.attributes[0].value {
assert!(
!s.as_str().contains("sk-proj-"),
"secret must be redacted, got: {s}"
);
} else {
panic!("expected String attribute");
}
}
#[test]
fn non_string_attributes_pass_through_unchanged() {
let captured = Arc::new(Mutex::new(None));
let inner = CapturingProcessor {
captured: Arc::clone(&captured),
};
let processor = RedactingSpanProcessor::new(inner);
let span = make_span_data(vec![
KeyValue::new("count", 42_i64),
KeyValue::new("flag", true),
KeyValue::new("ratio", 0.5_f64),
]);
processor.on_end(span);
let guard = captured.lock().unwrap();
let result = guard.as_ref().unwrap();
assert_eq!(result.attributes.len(), 3);
assert!(matches!(result.attributes[0].value, Value::I64(42)));
assert!(matches!(result.attributes[1].value, Value::Bool(true)));
assert!(
matches!(result.attributes[2].value, Value::F64(v) if (v - 0.5).abs() < f64::EPSILON)
);
}
#[test]
fn event_string_attributes_are_scrubbed() {
let captured = Arc::new(Mutex::new(None));
let inner = CapturingProcessor {
captured: Arc::clone(&captured),
};
let processor = RedactingSpanProcessor::new(inner);
let secret_val = "token=sk-proj-abcdefghijklmnopqrstuvwxyz0123456789";
let event = opentelemetry::trace::Event::new(
"log",
std::time::SystemTime::UNIX_EPOCH,
vec![KeyValue::new("message", secret_val)],
0,
);
let mut span = make_span_data(vec![]);
span.events.events.push(event);
processor.on_end(span);
let guard = captured.lock().unwrap();
let result = guard.as_ref().unwrap();
let event_attr = &result.events.events[0].attributes[0];
if let Value::String(ref s) = event_attr.value {
assert!(
!s.as_str().contains("sk-proj-"),
"event attribute secret must be redacted, got: {s}"
);
} else {
panic!("expected String attribute in event");
}
}
}