✅ COMPLETE — all three phases merged
- A1 (DP) ai-gateway#526 (
deacb20) · A2 (cp-api) AISIX-Cloud#702 (260ae765) · A3 (dashboard) AISIX-Cloud#704 (e969a7b5)
- Full round-trip verified end-to-end against a live DP (CI
e2e-playwright green). Follow-up test-gap tracked in AISIX-Cloud#703.
Goal
Record which guardrails governed each request (kind + hook) and surface it in the dashboard request logs, so operators see which guardrail acted — not just the boolean guardrail_blocked. Cross-repo: A1 (DP, this repo) → A2 (cp-api) → A3 (dashboard), both A2/A3 in api7/AISIX-Cloud.
Settled design (decided with the maintainer)
- Granularity (v1): the attached set per request =
{kind, hook} list. Per-guardrail verdicts are V2.
- Ordering: cp-api's
/dp/telemetry uses gin ShouldBindJSON (lenient — ignores unknown fields), so the DP can add the new UsageEvent field FIRST with zero CP breakage. Sequence A1 → A2 → A3.
- Capture point: at chain-build time (the builder has each row's
kind + hook_point) — no wide trait change.
- Shared type:
AppliedGuardrail { kind, hook } in aisix-core (aisix-obs already depends on aisix-core).
A1 (DP — this repo) — ✅ DONE — merged in #526 (squash deacb20)
All seven steps landed and the worktree/branch are cleaned up:
Audit LOW-1 (no unit coverage of the proxy out-param plumbing — dispatch needs a live AppState) folds into the A2/A3 e2e: once cp-api ingests the field, add an e2e that fires a guardrailed (incl. input-blocked) /v1/chat/completions + /v1/messages and asserts the telemetry row's applied_guardrails.
A2 (cp-api — AISIX-Cloud) — ✅ DONE — merged in AISIX-Cloud#702 (squash 260ae765)
Mirrored the existing routing_attempts JSONB-array column across all 8 layers:
A3 (dashboard — AISIX-Cloud) — ✅ DONE — merged in AISIX-Cloud#704 (squash e969a7b5)
Related
✅ COMPLETE — all three phases merged
deacb20) · A2 (cp-api) AISIX-Cloud#702 (260ae765) · A3 (dashboard) AISIX-Cloud#704 (e969a7b5)e2e-playwrightgreen). Follow-up test-gap tracked in AISIX-Cloud#703.Goal
Record which guardrails governed each request (kind + hook) and surface it in the dashboard request logs, so operators see which guardrail acted — not just the boolean
guardrail_blocked. Cross-repo: A1 (DP, this repo) → A2 (cp-api) → A3 (dashboard), both A2/A3 in api7/AISIX-Cloud.Settled design (decided with the maintainer)
{kind, hook}list. Per-guardrail verdicts are V2./dp/telemetryuses ginShouldBindJSON(lenient — ignores unknown fields), so the DP can add the newUsageEventfield FIRST with zero CP breakage. Sequence A1 → A2 → A3.kind+hook_point) — no wide trait change.AppliedGuardrail { kind, hook }in aisix-core (aisix-obs already depends on aisix-core).A1 (DP — this repo) — ✅ DONE — merged in #526 (squash
deacb20)All seven steps landed and the worktree/branch are cleaned up:
AppliedGuardrailfrom aisix-core (lib.rs+models/mod.rs).GuardrailChain:appliedfield +applied()accessor +new_with_applied(newkeeps applied empty).build_chain_from_snapshotand the per-requestGuardrailIndex::resolve(IndexEntrycarries each member's descriptor;resolvecollects from the deduplicated entries, soappliedmirrors the chain 1:1). Push onOk(Some)only.UsageEvent.applied_guardrails(skip_serializing_if = "Vec::is_empty") + wire-shape tests.chat.rs+messages.rs(non-stream success, guardrail-block, streaming) — captured once afterresolve()via a&mut Vec<_>out-param so the input-block path surfaces it too; closes the anthropic gap in guardrail metrics: cover the /v1/messages (anthropic) path #519 for this field.UsageEventwire shape.cargo fmt/test/clippyclean; independent audit (APPROVE, no HIGH/MEDIUM); CI green incl. e2e; squash-merged; worktree removed.Audit LOW-1 (no unit coverage of the proxy out-param plumbing —
dispatchneeds a liveAppState) folds into the A2/A3 e2e: once cp-api ingests the field, add an e2e that fires a guardrailed (incl. input-blocked)/v1/chat/completions+/v1/messagesand asserts the telemetry row'sapplied_guardrails.A2 (cp-api — AISIX-Cloud) — ✅ DONE — merged in AISIX-Cloud#702 (squash
260ae765)Mirrored the existing
routing_attemptsJSONB-array column across all 8 layers:telemetryEvent.applied_guardrails+parseTelemetryEventvalidates JSON-array & copies verbatim (rejects non-array).dpmgr_usage_events.applied_guardrailsJSONB column — GORM AutoMigrate adds it (this repo uses AutoMigrate, not SQL migration files; additive + nullable). Threaded throughdpstore.UsageEvent+InsertBatch/nullableJSONB+usageEventScanRow+toUsageEvent+models.UsageEvent./usage_events):usageEventView.applied_guardrailspassthrough,omitempty.InsertBatch→ListByEnvround-trip test gap filed as AISIX-Cloud#703.A3 (dashboard — AISIX-Cloud) — ✅ DONE — merged in AISIX-Cloud#704 (squash
e969a7b5)/logsrow expansion: "Guardrails that ran" panel — akind · hookbadge per applied guardrail, gated on a non-empty set. Threadsapplied_guardrailsthrough the dashboard'sUsageEventwire→view mapping (mirrorsrouting_attempts:AppliedGuardrailtype +isAppliedGuardrailguard + filter).guardrails-live.spec.ts: the keyword-block test expands the blocked row and asserts the panel names the guardrail (the DP records the applied set even on the request it rejects). Verified locally green against cp-api/dpm built from current main (A2) + a post-A1 DP image, and CIe2e-playwrightgreen.dangerouslySetInnerHTML) — A2 audit LOW addressed.Related