Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion crates/libsy-llm-client/src/observability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,19 @@ pub(crate) fn observe_client_call(result: Result<Response>) -> Result<Response>
}
Err(error) => {
let error_type = client_call_error_type(&error);
record_client_error(&span, &error_type, &error);
match &error {
LibsyError::ClientCall {
target,
source: LlmClientError::UpstreamHttp { status, .. },
} => record_client_error(
&span,
&error_type,
&format_args!(
"client call to target {target:?} failed: upstream HTTP {status}"
),
),
_ => record_client_error(&span, &error_type, &error),
}
Err(error)
}
}
Expand Down
38 changes: 37 additions & 1 deletion crates/libsy-llm-client/tests/observability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,9 @@ impl RoutedLlmClient for ClassifierClient {
}
}

// Conversation text that a provider may quote in an upstream error body.
const LEAKED_CONTENT: &str = "patient name is Jane Doe";

enum JudgeOutcome {
CallFailure,
Reply(&'static str),
Expand Down Expand Up @@ -440,7 +443,7 @@ impl RoutedLlmClient for JudgeClient {
match &self.outcome {
JudgeOutcome::CallFailure => Err(LlmClientError::UpstreamHttp {
status: http::StatusCode::INTERNAL_SERVER_ERROR,
body: "server error".to_string(),
body: format!(r#"{{"error":{{"message":"server error: {LEAKED_CONTENT}"}}}}"#),
}),
JudgeOutcome::Reply(text) => Ok(Response {
llm_response: LlmResponse::Agg(text_response(None, *text)),
Expand Down Expand Up @@ -1213,6 +1216,39 @@ async fn typed_client_failure_records_semantic_error_type() {
);
}

/// Verifies that the client span retains the HTTP status without the upstream body.
#[tokio::test]
async fn upstream_body_is_redacted_from_the_client_call_span() -> switchyard_libsy::Result<()> {
let _guard = serialize_test().lock().await;
let (store, _, _, _, _) = telemetry();
const JUDGE: &str = "redaction-judge";
let client = Arc::new(JudgeClient {
judge_model: JUDGE.into(),
outcome: JudgeOutcome::CallFailure,
}) as Arc<dyn RoutedLlmClient>;
run(
classifier_router(JUDGE, "redaction-weak", "redaction-strong")?,
client,
classifier_request(),
)
.await?;

let spans = store.spans();
let client_span = find_span(&spans, "libsy.client_call", "selected_model", JUDGE);
assert_eq!(
client_span.fields.get("error.type").map(String::as_str),
Some("500")
);
let error = client_span
.fields
.get("error")
.map(String::as_str)
.unwrap_or("");
assert!(error.contains("upstream HTTP 500"), "{client_span:?}");
assert!(!error.contains(LEAKED_CONTENT), "{client_span:?}");
Ok(())
}

#[tokio::test]
async fn failed_call_records_error_outcome_and_warn_logs() -> switchyard_libsy::Result<()> {
let _guard = serialize_test().lock().await;
Expand Down
Loading