feat(libsy): count classifier fail-open fallbacks on /metrics - #205
Conversation
52c8d5b to
2becceb
Compare
WalkthroughChangesThe judge classifier now categorizes fail-open errors, logs them, and records a metric tagged by model and reason. Integration tests cover failure modes and confirm valid verdicts are not counted. Classifier fail-open telemetry
Estimated code review effort: 3 (Moderate) | ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
crates/libsy/tests/observability.rs (1)
858-863: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: no case covers
timeoutor the catch-all reasons.
client_error_reasonmapsTimeout→"timeout"and falls back to"client_error"/"call_error"; neither label is exercised. AJudgeOutcome::CallError(timeout_error)case would close the boundary most likely to be hit in production.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/libsy/tests/observability.rs` around lines 858 - 863, Extend the observability test cases using the JudgeOutcome enum to cover client_error_reason’s Timeout mapping and fallback labels. Add a JudgeOutcome::CallError case that produces a timeout error and assert the expected "timeout" reason, while also exercising the catch-all client-error/call-error fallback as appropriate.crates/libsy/src/observability.rs (1)
221-232: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCache the fail-open counter handle
meter().u64_counter(...).build()runs on every fail-open, adding avoidable overhead on a hot outage path. ALazyLock<Counter<u64>>(or similar cached handle) would keep recording cheap; this test setup installs the global meter provider once beforeinitialize_metrics(), so a process-wide handle should fit.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/libsy/src/observability.rs` around lines 221 - 232, Cache the counter handle used by record_classifier_fail_open in a process-wide LazyLock (or equivalent), initializing it with meter().u64_counter("switchyard.classifier_fail_open").build() once. Update record_classifier_fail_open to reuse the cached handle while preserving the existing value and judge_model/reason labels.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/libsy/src/observability.rs`:
- Around line 221-232: Cache the counter handle used by
record_classifier_fail_open in a process-wide LazyLock (or equivalent),
initializing it with
meter().u64_counter("switchyard.classifier_fail_open").build() once. Update
record_classifier_fail_open to reuse the cached handle while preserving the
existing value and judge_model/reason labels.
In `@crates/libsy/tests/observability.rs`:
- Around line 858-863: Extend the observability test cases using the
JudgeOutcome enum to cover client_error_reason’s Timeout mapping and fallback
labels. Add a JudgeOutcome::CallError case that produces a timeout error and
assert the expected "timeout" reason, while also exercising the catch-all
client-error/call-error fallback as appropriate.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: daa96b57-cce4-48f5-a503-07972298ded6
📒 Files selected for processing (3)
crates/libsy/src/algorithms/util/llm_judge.rscrates/libsy/src/observability.rscrates/libsy/tests/observability.rs
7f02755 to
1e763a9
Compare
1e763a9 to
c6f0a31
Compare
c6f0a31 to
d12bdf7
Compare
|
Signed-off-by: Elyas Mehtabuddin <emehtabuddin@nvidia.com>
d12bdf7 to
36b7867
Compare
When the classifier's judge target returns HTTP 500, Switchyard still sends the request to the capable target. The request succeeds, but
/metricsdoes not show that the classifier routed without a verdict:Fix
A failed judge should not fail the caller's request. The classifier still routes without a verdict, and now increments
switchyard_classifier_fail_open_totalfor each judge failure.The counter has two labels:
judge_modelnames the configured judge target, andreasonis one of eight fixed categories. The code derivesreasonfrom the typed error and HTTP status only. It never uses request or response text as a label.The non-5xx HTTP category is named
upstream_non_5xx; an upstream error can carry any status below 500, not only a 4xx status.Before and after
Before this change, the request succeeds but the metric query returns nothing. After the change, the same judge failure produces:
A valid judge verdict does not increment the counter, even when it selects the same capable target as the fallback. Routing behavior and the caller's response are unchanged; this PR only adds the counter and its documentation. There is no performance claim.
How tested
The first test covers all eight bounded reason categories, including
call_error. The second drives the realLlmTaskClassifierthrough judge-call, stream-decoding, and verdict-parsing failures, then checks that a valid verdict is not counted.