Skip to content

feat(guardrails): surface monitor-mode hits on usage events - #731

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/guardrail-monitor-telemetry
Jul 6, 2026
Merged

feat(guardrails): surface monitor-mode hits on usage events#731
jarvis9443 merged 1 commit into
mainfrom
feat/guardrail-monitor-telemetry

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What

enforcement_mode: monitor observations now land on the request's usage event instead of only in gateway logs. Each event carries guardrail_monitor_hits: one entry per suppressed decision — would_block (with the operator-facing reason) or would_mask (with per-detector counts) — plus the guardrail row name and hook. Names only, never matched content (#153).

This closes the monitor-mode observability gap from api7/AISIX-Cloud#562: an operator can stage a policy in monitor mode, audit its would-have-fired rate per request in the dashboard, and only then flip it to block. Previously monitor hits went to DP tracing logs only, so the "observe first, enforce later" loop had no dashboard-visible signal.

How

  • aisix_core::models::GuardrailMonitorHit { guardrail_name, hook, action, reason, counts }, carried on UsageEvent.guardrail_monitor_hits (serde-skipped when empty; older CP images ignore the unknown field — additive wire change).
  • The Guardrail trait gains check_{input,output}[_non_segment]_observed variants returning (verdict, Vec<GuardrailMonitorHit>), with defaults that delegate to the plain checks — concrete kinds are untouched. SegmentsOutcome gains monitor_hits.
  • MonitorGuardrail produces the hits: a downgraded Block → would_block; the suppressed sync redactor (kind=pii) is probed on the hook's scan text for would_mask counts; and it now delegates the segment pass (moderates_segments), so a monitored bedrock/lakera/presidio row is observed through ONE provider call with mask/count fidelity instead of degrading to the blob path.
  • GuardrailChain folds observed results and keeps hits collected before an enforcing peer's Block short-circuit.
  • Every proxy family threads a per-request accumulator into its usage emission (same shape as the #932 redacted_entity_counts plumbing): chat (incl. streaming on_complete + ensemble judge), completions, messages (both stream builders), responses (+bridge), embeddings, rerank, images, audio, jobs, mcp, realtime, passthrough. Failed-attempt / pre-guardrail error events intentionally carry no hits (terminal event does), mirroring how redaction counts already behave.

Tests

  • Unit: monitor-mode observed check reports would_mask (with counts) and would_block (reason, no value leak); enforcing rows produce no hits; a monitor hit survives an enforcing peer's block short-circuit; existing 208-test suite green.
  • E2E (guardrail-monitor-telemetry-e2e): real binary + etcd + mock upstream + real aliyun_sls exporter against a mock SLS endpoint — a monitor-mode pii guardrail lets an email prompt reach the upstream unmasked while the exported usage event carries the would_mask hit, a block-action detector returns 200 while the event carries would_block, and the matched values never appear in the exported events.
  • Touched-path regression: streaming/messages/responses/completions/passthrough guardrail specs + SLS content-capture specs green.

Fixes api7/AISIX-Cloud#562 (link added post-merge for the Development field; the issue is closed manually once the full stage-0-2 scope lands). The CP ingestion + logs filter lands as the paired AISIX-Cloud PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added monitor-mode guardrail telemetry across supported API surfaces, capturing would-block and would-mask observations without changing request outcomes.
    • Usage events now include guardrail monitor-hit details for better visibility into suppressed enforcement actions.
  • Bug Fixes

    • Preserved monitor-hit reporting during streaming, ensemble, and segment-based moderation flows.
    • Ensured monitor telemetry is recorded consistently for success, blocked, and fallback response paths.

enforcement_mode=monitor observations now ride the request's UsageEvent
as guardrail_monitor_hits (would_block with reason / would_mask with
per-detector counts; names only, never matched content) instead of
tracing logs alone, closing the observe-then-enforce loop for
AISIX-Cloud#562.

- Guardrail trait: check_*_observed variants (defaults delegate; kinds
  untouched); SegmentsOutcome.monitor_hits
- MonitorGuardrail emits the hits, probes the suppressed pii redactor
  for would-mask counts, and now delegates the segment pass so a
  monitored bedrock/lakera/presidio row is observed via one provider
  call with mask fidelity
- chain folds keep hits collected before an enforcing peer's block
- every proxy family threads a per-request accumulator into its usage
  emission (streaming on_complete paths included), mirroring the
  redacted_entity_counts plumbing
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces monitor-mode guardrail telemetry across the codebase. A new GuardrailMonitorHit type captures suppressed would_block/would_mask actions from guardrails running in monitor mode. The Guardrail trait, chain, and segment-based guardrails are extended with "observed" variants that return hits alongside verdicts, and every aisix-proxy request handler is updated to collect and emit these hits in UsageEvent.

Changes

Monitor-mode guardrail telemetry

Layer / File(s) Summary
Core monitor hit type
crates/aisix-core/src/lib.rs, crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs
Adds the GuardrailMonitorHit struct and updates re-export lists.
Guardrail trait and segment outcome contract
crates/aisix-guardrails/src/lib.rs, crates/aisix-guardrails/src/bedrock.rs, crates/aisix-guardrails/src/lakera.rs, crates/aisix-guardrails/src/presidio.rs
Adds monitor_hits to SegmentsOutcome, default observed trait methods, and updates segment guardrails to populate the field.
MonitorGuardrail decorator
crates/aisix-guardrails/src/build.rs
Implements would_block/would_mask hit construction, redaction probing, observed checks, and tests.
Guardrail chain aggregation
crates/aisix-guardrails/src/chain.rs
Aggregates monitor hits from chain members across observed checks and segment folding.
Usage telemetry model
crates/aisix-obs/src/usage.rs
Adds guardrail_monitor_hits field to UsageEvent.
Redaction pipeline wiring
crates/aisix-proxy/src/redact.rs
Extends moderate_body with a monitor-hit output parameter.
Chat completions wiring
crates/aisix-proxy/src/chat.rs
Threads monitor hits through dispatch, ensemble, and streaming telemetry.
Messages endpoint wiring
crates/aisix-proxy/src/messages.rs
Threads monitor hits through Anthropic passthrough/cross-provider dispatch and streaming.
Responses endpoint wiring
crates/aisix-proxy/src/responses.rs, crates/aisix-proxy/src/responses_bridge.rs
Threads monitor hits through streaming/non-streaming responses dispatch paths.
Simple endpoints wiring
crates/aisix-proxy/src/completions.rs, .../embeddings.rs, .../images.rs, .../rerank.rs, .../audio.rs
Adds monitor-hit fields to dispatch success structs and observed guardrail checks.
Jobs, MCP, passthrough, realtime wiring
crates/aisix-proxy/src/jobs.rs, .../mcp.rs, .../passthrough.rs, .../realtime.rs
Adds monitor-hit collection and telemetry emission for remaining endpoints.
E2E test
tests/e2e/src/cases/guardrail-monitor-telemetry-e2e.test.ts
Validates monitor-mode hits are exported via telemetry without affecting traffic.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ProxyHandler
  participant GuardrailChain
  participant MonitorGuardrail
  participant UsageEvent

  Client->>ProxyHandler: request
  ProxyHandler->>GuardrailChain: check_input_observed(req)
  GuardrailChain->>MonitorGuardrail: check_input_observed(req)
  MonitorGuardrail-->>GuardrailChain: (downgraded verdict, hits)
  GuardrailChain-->>ProxyHandler: (verdict, monitor_hits)
  ProxyHandler->>ProxyHandler: accumulate monitor_hits
  ProxyHandler->>UsageEvent: emit_usage_event(guardrail_monitor_hits)
  UsageEvent-->>Client: (response unaffected by monitor mode)
Loading

Possibly related PRs

  • api7/aisix#640: Directly builds on the enforcement_mode: monitor decorator behavior in crates/aisix-guardrails/src/build.rs extended in this PR.
  • api7/aisix#683: Modifies the same enforcement_mode wrapping logic in build.rs that this PR extends with observed telemetry wrappers.
  • api7/aisix#672: Modifies the same MCP tool-call guardrail evaluation path in crates/aisix-proxy/src/mcp.rs extended here with observed-mode propagation.
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: exposing monitor-mode guardrail hits in usage events.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
E2e Test Quality Review ✅ Passed The new E2E test is readable and exercises the full API→proxy→upstream→SLS flow with real app wiring and justified mocks.
Security Check ✅ Passed No security regressions found: monitor-hit telemetry carries only names/counts, tests verify matched values never enter hits, and there are no auth/DB/TLS/ownership changes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/guardrail-monitor-telemetry

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/aisix-proxy/src/jobs.rs (1)

639-668: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Monitor hits are dropped on the guardrail-block / error path.

finish receives monitor_hits but only forwards them via emit_job_usage_event on the Ok branch. When scan_input_blob/scan_output_blob returns ProxyError::ContentFiltered (an enforcing guardrail blocked after a monitor-mode guardrail already fired), the accumulated monitor_hits are discarded because the Err branch calls emit_error_usage_event, which has no hits parameter.

This diverges from the chat path, where the input-blocked/error telemetry explicitly carries guardrail_monitor_hits: monitor_hits.clone(), and from the PR's stated goal of preserving collected hits even when a later enforcing guardrail blocks. monitor_hits is already in scope here, so the gap is in the error emission path.

#!/bin/bash
# Confirm emit_error_usage_event has no monitor-hits parameter and compare with chat.rs parity.
rg -nP -A20 'fn emit_error_usage_event' crates/aisix-proxy/src/usage_attr.rs
rg -nP -B2 -A2 'guardrail_monitor_hits' crates/aisix-proxy/src/chat.rs | head -40
🤖 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/aisix-proxy/src/jobs.rs` around lines 639 - 668, The error path in
finish drops monitor_hits when a guardrail-blocked request returns Err, because
it only passes telemetry through emit_error_usage_event. Update the Err branch
in jobs.rs so the accumulated monitor_hits are forwarded into the error usage
emission, following the chat path’s guardrail_monitor_hits handling, and adjust
emit_error_usage_event (and its call sites/signature in usage_attr.rs) to accept
and record those hits.
crates/aisix-proxy/src/passthrough.rs (1)

196-231: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Propagate guardrail hits into the passthrough error event. The Err(err) branch drops the monitor_hits accumulated by dispatch, so ContentFiltered passthrough failures lose guardrail telemetry; pass those hits into emit_error_usage_event instead.

🤖 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/aisix-proxy/src/passthrough.rs` around lines 196 - 231, The Err(err)
branch in passthrough::dispatch is dropping the accumulated monitor_hits, so
guardrail-triggered passthrough failures are not reflected in the error usage
event. Thread the monitor_hits value from dispatch into the error path and pass
it through to crate::usage_attr::emit_error_usage_event alongside the existing
status, err.kind(), and request metadata so ContentFiltered failures retain
guardrail telemetry.
🧹 Nitpick comments (3)
crates/aisix-proxy/src/redact.rs (1)

1806-1858: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test coverage gap: moderate_body's monitor-hit accumulation is never exercised.

StubSegments::moderate always returns monitor_hits: Vec::new() (line 1855), and every moderate_body call in this test module passes a throwaway &mut Vec::new() (lines 1890, 1940, 1970, 1983, 2011) without inspecting it afterward. A regression that drops or double-appends outcome.monitor_hits in moderate_body (lines 218-222) would go undetected by this file's tests — the actual data-flow contract added by this PR is untested here.

Consider extending StubSegments with an optional monitor_hits: Vec<GuardrailMonitorHit> field returned from moderate(), and adding an assertion in moderate_body_masks_chat_slots_positionally (or a new dedicated test) that monitor_hits_out contains the expected entries after the call.

Also applies to: 1868-1988, 2006-2020

🤖 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/aisix-proxy/src/redact.rs` around lines 1806 - 1858, The monitor-hit
path in moderate_body is not covered because StubSegments::moderate always
returns an empty monitor_hits vector and the tests pass throwaway output buffers
without asserting on them. Extend StubSegments to optionally return predefined
monitor_hits from moderate(), then update
moderate_body_masks_chat_slots_positionally or add a dedicated test to assert
that the caller-provided monitor_hits_out receives the expected
GuardrailMonitorHit entries after moderate_body runs.
crates/aisix-guardrails/src/chain.rs (2)

210-329: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct, but the four observed folds duplicate the four existing plain folds almost verbatim.

Logic is verified correct — hits.extend(member_hits) runs before the match, so hits from members preceding (and including) a blocking member are preserved, matching the stated intent. This does double the amount of near-identical bypass/block-tracking boilerplate in the file (8 structurally-identical loops total now). A shared private helper parameterized over a per-member async check would cut the duplication, though the differing ChatFormat/ChatResponse types make this a bit more than a trivial extraction in Rust's async-trait world.

🤖 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/aisix-guardrails/src/chain.rs` around lines 210 - 329, The four
observed fold methods in Chain are correct but duplicate the existing plain fold
logic almost verbatim, so refactor them to reduce repeated bypass/block tracking
boilerplate. Extract a shared private helper in the Chain implementation that
handles iterating over members, collecting hits, and preserving the first bypass
reason, then have check_input_observed, check_output_observed,
check_input_non_segment_observed, and check_output_non_segment_observed delegate
to it with the appropriate per-member async check. Keep the existing behavior
and identifiers like attribute_block, GuardrailVerdict, and GuardrailMonitorHit
unchanged.

210-241: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing chain-level test for hit preservation across a later member's Block.

The doc comment states monitor hits from an earlier member must survive a later member's Block, but no test in this file exercises that multi-member scenario (only build.rs's single-member monitor_mode_observed_check_reports_hits test exists). A test with one monitor-mode member (emits a hit) followed by an enforcing member that blocks would directly validate this chain's core new guarantee.

🤖 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/aisix-guardrails/src/chain.rs` around lines 210 - 241, The new
observed-fold behavior in check_input_observed needs a chain-level regression
test to prove earlier monitor hits survive a later Block. Add a test in chain.rs
that builds a multi-member chain with one member returning monitor-mode hits and
a later enforcing member returning GuardrailVerdict::Block, then assert the
final verdict blocks and the collected Vec<GuardrailMonitorHit> still includes
the earlier hit. Use the check_input_observed method and the existing
GuardrailVerdict/GuardrailMonitorHit types to locate the right behavior.
🤖 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.

Outside diff comments:
In `@crates/aisix-proxy/src/jobs.rs`:
- Around line 639-668: The error path in finish drops monitor_hits when a
guardrail-blocked request returns Err, because it only passes telemetry through
emit_error_usage_event. Update the Err branch in jobs.rs so the accumulated
monitor_hits are forwarded into the error usage emission, following the chat
path’s guardrail_monitor_hits handling, and adjust emit_error_usage_event (and
its call sites/signature in usage_attr.rs) to accept and record those hits.

In `@crates/aisix-proxy/src/passthrough.rs`:
- Around line 196-231: The Err(err) branch in passthrough::dispatch is dropping
the accumulated monitor_hits, so guardrail-triggered passthrough failures are
not reflected in the error usage event. Thread the monitor_hits value from
dispatch into the error path and pass it through to
crate::usage_attr::emit_error_usage_event alongside the existing status,
err.kind(), and request metadata so ContentFiltered failures retain guardrail
telemetry.

---

Nitpick comments:
In `@crates/aisix-guardrails/src/chain.rs`:
- Around line 210-329: The four observed fold methods in Chain are correct but
duplicate the existing plain fold logic almost verbatim, so refactor them to
reduce repeated bypass/block tracking boilerplate. Extract a shared private
helper in the Chain implementation that handles iterating over members,
collecting hits, and preserving the first bypass reason, then have
check_input_observed, check_output_observed, check_input_non_segment_observed,
and check_output_non_segment_observed delegate to it with the appropriate
per-member async check. Keep the existing behavior and identifiers like
attribute_block, GuardrailVerdict, and GuardrailMonitorHit unchanged.
- Around line 210-241: The new observed-fold behavior in check_input_observed
needs a chain-level regression test to prove earlier monitor hits survive a
later Block. Add a test in chain.rs that builds a multi-member chain with one
member returning monitor-mode hits and a later enforcing member returning
GuardrailVerdict::Block, then assert the final verdict blocks and the collected
Vec<GuardrailMonitorHit> still includes the earlier hit. Use the
check_input_observed method and the existing
GuardrailVerdict/GuardrailMonitorHit types to locate the right behavior.

In `@crates/aisix-proxy/src/redact.rs`:
- Around line 1806-1858: The monitor-hit path in moderate_body is not covered
because StubSegments::moderate always returns an empty monitor_hits vector and
the tests pass throwaway output buffers without asserting on them. Extend
StubSegments to optionally return predefined monitor_hits from moderate(), then
update moderate_body_masks_chat_slots_positionally or add a dedicated test to
assert that the caller-provided monitor_hits_out receives the expected
GuardrailMonitorHit entries after moderate_body runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba10953f-7557-47e0-835b-ce6355a3fa8b

📥 Commits

Reviewing files that changed from the base of the PR and between 098d9d5 and a23eafe.

📒 Files selected for processing (25)
  • crates/aisix-core/src/lib.rs
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-guardrails/src/bedrock.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/chain.rs
  • crates/aisix-guardrails/src/lakera.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-guardrails/src/presidio.rs
  • crates/aisix-obs/src/usage.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/redact.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/responses_bridge.rs
  • tests/e2e/src/cases/guardrail-monitor-telemetry-e2e.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant