Skip to content

Commit 4eae4bf

Browse files
authored
fix(llm-client): redact upstream body from client-call span (#611)
Signed-off-by: Eugen Nekhai <eugen.nekhai@gmail.com>
1 parent 4022b67 commit 4eae4bf

2 files changed

Lines changed: 50 additions & 2 deletions

File tree

‎crates/libsy-llm-client/src/observability.rs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,19 @@ pub(crate) fn observe_client_call(result: Result<Response>) -> Result<Response>
6666
}
6767
Err(error) => {
6868
let error_type = client_call_error_type(&error);
69-
record_client_error(&span, &error_type, &error);
69+
match &error {
70+
LibsyError::ClientCall {
71+
target,
72+
source: LlmClientError::UpstreamHttp { status, .. },
73+
} => record_client_error(
74+
&span,
75+
&error_type,
76+
&format_args!(
77+
"client call to target {target:?} failed: upstream HTTP {status}"
78+
),
79+
),
80+
_ => record_client_error(&span, &error_type, &error),
81+
}
7082
Err(error)
7183
}
7284
}

‎crates/libsy-llm-client/tests/observability.rs‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,9 @@ impl RoutedLlmClient for ClassifierClient {
413413
}
414414
}
415415

416+
// Conversation text that a provider may quote in an upstream error body.
417+
const LEAKED_CONTENT: &str = "patient name is Jane Doe";
418+
416419
enum JudgeOutcome {
417420
CallFailure,
418421
Reply(&'static str),
@@ -440,7 +443,7 @@ impl RoutedLlmClient for JudgeClient {
440443
match &self.outcome {
441444
JudgeOutcome::CallFailure => Err(LlmClientError::UpstreamHttp {
442445
status: http::StatusCode::INTERNAL_SERVER_ERROR,
443-
body: "server error".to_string(),
446+
body: format!(r#"{{"error":{{"message":"server error: {LEAKED_CONTENT}"}}}}"#),
444447
}),
445448
JudgeOutcome::Reply(text) => Ok(Response {
446449
llm_response: LlmResponse::Agg(text_response(None, *text)),
@@ -1213,6 +1216,39 @@ async fn typed_client_failure_records_semantic_error_type() {
12131216
);
12141217
}
12151218

1219+
/// Verifies that the client span retains the HTTP status without the upstream body.
1220+
#[tokio::test]
1221+
async fn upstream_body_is_redacted_from_the_client_call_span() -> switchyard_libsy::Result<()> {
1222+
let _guard = serialize_test().lock().await;
1223+
let (store, _, _, _, _) = telemetry();
1224+
const JUDGE: &str = "redaction-judge";
1225+
let client = Arc::new(JudgeClient {
1226+
judge_model: JUDGE.into(),
1227+
outcome: JudgeOutcome::CallFailure,
1228+
}) as Arc<dyn RoutedLlmClient>;
1229+
run(
1230+
classifier_router(JUDGE, "redaction-weak", "redaction-strong")?,
1231+
client,
1232+
classifier_request(),
1233+
)
1234+
.await?;
1235+
1236+
let spans = store.spans();
1237+
let client_span = find_span(&spans, "libsy.client_call", "selected_model", JUDGE);
1238+
assert_eq!(
1239+
client_span.fields.get("error.type").map(String::as_str),
1240+
Some("500")
1241+
);
1242+
let error = client_span
1243+
.fields
1244+
.get("error")
1245+
.map(String::as_str)
1246+
.unwrap_or("");
1247+
assert!(error.contains("upstream HTTP 500"), "{client_span:?}");
1248+
assert!(!error.contains(LEAKED_CONTENT), "{client_span:?}");
1249+
Ok(())
1250+
}
1251+
12161252
#[tokio::test]
12171253
async fn failed_call_records_error_outcome_and_warn_logs() -> switchyard_libsy::Result<()> {
12181254
let _guard = serialize_test().lock().await;

0 commit comments

Comments
 (0)