Skip to content

Commit 5fef41b

Browse files
authored
fix: preserve log severity during trajectory redaction (#919)
#### Overview Preserve valid `nemo_relay.log.severity` metadata during trajectory-context redaction so sanitized semantic logs remain exportable. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Preserve supported log-severity values in canonical form without changing the existing policy for other mark metadata. - Keep unsupported severity values subject to the configured custom-mark policy; `redact_all_leaves` redacts them. - Add regression coverage for valid and invalid severity values and downstream subscriber delivery. - Document the trajectory-context preset behavior. Validation: - PII-redaction crate tests: 147 passed - Focused subscriber/exporter regression passed - Workspace Clippy passed - Documentation build passed - Formatting and diff checks passed #### Where should the reviewer start? Start with `crates/pii-redaction/src/trajectory.rs`, especially the severity extraction and restoration around trajectory sanitization. The corresponding regression coverage is in `crates/pii-redaction/tests/unit/component_tests.rs`. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Relates to #916 ## Summary by CodeRabbit - **Bug Fixes** - Preserved valid log severity metadata during trajectory sanitization, including after payload, metadata, or category redaction. - Normalized supported severity values to a canonical form. - Continued redacting invalid or unsupported severity values and other sensitive custom-mark data. - **Documentation** - Clarified severity metadata handling in trajectory export and typed metric mark configuration documentation. Authors: - Maryam Najafian (https://github.com/mnajafian-nv) Approvers: - Will Killian (https://github.com/willkill07) URL: #919
1 parent d953a11 commit 5fef41b

4 files changed

Lines changed: 98 additions & 7 deletions

File tree

crates/pii-redaction/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ metric-schema marks use schema-aware sanitization instead: required measurement
150150
fields and numeric analytics remain valid for metric export, while descriptions
151151
and string attribute values are redacted.
152152
Strings become `[REDACTED]`, numbers become `0`, booleans become `false`, and
153-
nulls, keys, arrays, and object shape are retained.
153+
nulls, keys, arrays, and object shape are retained. On every mark, the preset
154+
preserves the reserved `nemo_relay.log.severity` metadata field in canonical
155+
form when it contains a supported severity. For opaque custom marks that use
156+
`redact_all_leaves`, unsupported severity values remain redacted with the
157+
other string leaves.
154158
Known Relay marks are sanitized semantically so their structural and analytical
155159
fields remain usable. This choice affects canonical event fields before
156160
subscriber fan-out; exporter-owned resource attributes are outside this

crates/pii-redaction/src/trajectory.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use std::sync::Arc;
88
use serde_json::Value as Json;
99

1010
use nemo_relay::api::event::{
11-
CategoryProfile, Event, METRIC_DATA_SCHEMA_NAME, METRIC_DATA_SCHEMA_VERSION, MetricEnvelope,
11+
CategoryProfile, Event, LOG_SEVERITY_METADATA_KEY, LogSeverity, METRIC_DATA_SCHEMA_NAME,
12+
METRIC_DATA_SCHEMA_VERSION, MetricEnvelope,
1213
};
1314
use nemo_relay::codec::request::AnnotatedLlmRequest;
1415
use nemo_relay::codec::response::AnnotatedLlmResponse;
@@ -130,6 +131,7 @@ impl TrajectorySanitizer {
130131
event: &Event,
131132
mut fields: nemo_relay::api::event::EventSanitizeFields,
132133
) -> nemo_relay::api::event::EventSanitizeFields {
134+
let log_severity = valid_mark_log_severity(event, fields.metadata.as_ref());
133135
if is_relay_metric_mark(event) {
134136
fields.data = fields
135137
.data
@@ -140,7 +142,7 @@ impl TrajectorySanitizer {
140142
fields.category_profile = fields
141143
.category_profile
142144
.and_then(|profile| sanitize_category_profile(profile, &self.replacement));
143-
return fields;
145+
return restore_log_severity(fields, log_severity);
144146
}
145147

146148
let category = event.category().map(|category| category.as_str());
@@ -162,7 +164,7 @@ impl TrajectorySanitizer {
162164
.category_profile
163165
.and_then(|profile| redact_custom_category_profile(profile, self));
164166
}
165-
return fields;
167+
return restore_log_severity(fields, log_severity);
166168
}
167169

168170
if !specialized_scope {
@@ -180,7 +182,7 @@ impl TrajectorySanitizer {
180182
fields.category_profile = fields
181183
.category_profile
182184
.and_then(|profile| sanitize_category_profile(profile, &self.replacement));
183-
fields
185+
restore_log_severity(fields, log_severity)
184186
}
185187

186188
/// Redact optional metric text without modifying required export fields.
@@ -202,6 +204,30 @@ impl TrajectorySanitizer {
202204
}
203205
}
204206

207+
fn valid_mark_log_severity(event: &Event, metadata: Option<&Json>) -> Option<LogSeverity> {
208+
if !matches!(event, Event::Mark(_)) {
209+
return None;
210+
}
211+
metadata
212+
.and_then(Json::as_object)
213+
.and_then(|metadata| metadata.get(LOG_SEVERITY_METADATA_KEY))
214+
.and_then(Json::as_str)
215+
.and_then(|value| value.parse::<LogSeverity>().ok())
216+
}
217+
218+
fn restore_log_severity(
219+
mut fields: nemo_relay::api::event::EventSanitizeFields,
220+
severity: Option<LogSeverity>,
221+
) -> nemo_relay::api::event::EventSanitizeFields {
222+
if let (Some(severity), Some(Json::Object(metadata))) = (severity, fields.metadata.as_mut()) {
223+
metadata.insert(
224+
LOG_SEVERITY_METADATA_KEY.to_string(),
225+
Json::String(severity.as_str().to_string()),
226+
);
227+
}
228+
fields
229+
}
230+
205231
/// Return whether an event carries Relay's typed metric schema.
206232
pub(crate) fn is_relay_metric_mark(event: &Event) -> bool {
207233
matches!(event, Event::Mark(_))

crates/pii-redaction/tests/unit/component_tests.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
use super::*;
88
use crate::api::event::{
99
BaseEvent, CategoryProfile, DataSchema, Event, EventCategory, EventSanitizeFields,
10-
METRIC_DATA_SCHEMA_NAME, METRIC_DATA_SCHEMA_VERSION, MarkEvent, MetricEnvelope, ScopeCategory,
11-
ScopeEvent,
10+
LOG_SEVERITY_METADATA_KEY, LogSeverity, METRIC_DATA_SCHEMA_NAME, METRIC_DATA_SCHEMA_VERSION,
11+
MarkEvent, MetricEnvelope, ScopeCategory, ScopeEvent,
1212
};
1313
use crate::api::llm::{
1414
LlmCallExecuteParams, LlmCallParams, LlmRequest, LlmStreamCallExecuteParams, llm_call,
@@ -1135,6 +1135,54 @@ async fn trajectory_custom_mark_policy_is_explicit_and_shape_preserving() {
11351135
assert_eq!(profile.extra["opaque"]["label"], "[REDACTED]");
11361136
}
11371137

1138+
#[tokio::test]
1139+
async fn trajectory_custom_mark_preserves_only_valid_log_severity() {
1140+
let event = Event::Mark(MarkEvent::new(
1141+
BaseEvent::builder().name("neutral.plugin.log").build(),
1142+
Some(EventCategory::custom()),
1143+
None,
1144+
));
1145+
let callback =
1146+
crate::builtin::event_sanitize_callback(trajectory_backend(None, "redact_all_leaves"));
1147+
1148+
let sanitized = callback(
1149+
Arc::new(event.clone()),
1150+
EventSanitizeFields {
1151+
data: Some(json!({"message": "private"})),
1152+
category_profile: None,
1153+
metadata: Some(json!({
1154+
LOG_SEVERITY_METADATA_KEY: "warning",
1155+
"owner": "private owner"
1156+
})),
1157+
},
1158+
)
1159+
.await
1160+
.unwrap();
1161+
assert_eq!(sanitized.data, Some(json!({"message": "[REDACTED]"})));
1162+
assert_eq!(
1163+
sanitized.metadata,
1164+
Some(json!({
1165+
LOG_SEVERITY_METADATA_KEY: "warn",
1166+
"owner": "[REDACTED]"
1167+
}))
1168+
);
1169+
1170+
let sanitized = callback(
1171+
Arc::new(event),
1172+
EventSanitizeFields {
1173+
data: None,
1174+
category_profile: None,
1175+
metadata: Some(json!({LOG_SEVERITY_METADATA_KEY: "private severity"})),
1176+
},
1177+
)
1178+
.await
1179+
.unwrap();
1180+
assert_eq!(
1181+
sanitized.metadata,
1182+
Some(json!({LOG_SEVERITY_METADATA_KEY: "[REDACTED]"}))
1183+
);
1184+
}
1185+
11381186
#[tokio::test]
11391187
async fn trajectory_metric_marks_preserve_typed_measurements_and_redact_text() {
11401188
let data = json!({
@@ -2708,6 +2756,7 @@ fn sanitized_trajectory_content_never_reaches_subscribers_or_exporters() {
27082756
.name("hermes.checkpoint")
27092757
.data(json!({"content": raw_context, "email": raw_pii, "score": 0.95}))
27102758
.metadata(json!({"reviewer": raw_pii}))
2759+
.severity(LogSeverity::Warn)
27112760
.build(),
27122761
)
27132762
.unwrap();
@@ -2786,6 +2835,13 @@ fn sanitized_trajectory_content_never_reaches_subscribers_or_exporters() {
27862835
.unwrap();
27872836
assert_eq!(custom_mark.data().unwrap()["content"], "[REDACTED]");
27882837
assert_eq!(custom_mark.data().unwrap()["score"], 0);
2838+
assert_eq!(
2839+
custom_mark.metadata().unwrap(),
2840+
&json!({
2841+
LOG_SEVERITY_METADATA_KEY: "warn",
2842+
"reviewer": "[REDACTED]"
2843+
})
2844+
);
27892845

27902846
deregister_subscriber("pii-regression-subscriber").unwrap();
27912847
atof.deregister("pii-regression-atof").unwrap();

docs/configure-plugins/pii-redaction/configuration.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ fields. That preset does not accept `target_paths`. Its
267267
`custom_mark_payload_policy` applies only to opaque marks in the `custom`
268268
category; it does not control Relay typed metric envelopes.
269269

270+
On every mark, the preset preserves the reserved `nemo_relay.log.severity`
271+
metadata field in canonical form when it contains a supported severity. For
272+
opaque custom marks using `redact_all_leaves`, unsupported severity values and
273+
all other string leaves remain redacted.
274+
270275
## Action Semantics
271276

272277
### `remove`

0 commit comments

Comments
 (0)