Alerts and audit
A guardrail violation produces two independent records: an alert delivered to the Levo platform, and an audit line written to the gateway's stdout for your own log pipeline. They are not the same thing and they do not currently cover the same events.
Alerts in the Levo dashboard
Violations appear under Issues → AI Governance, scoped to the environment selected in the switcher.
| Column | Meaning |
|---|---|
| Severity | Critical / High / Medium / Low, from the finding |
| Alert title | What fired — the detector or scanner name |
| Action | What the gateway did: blocked, masked, or flagged |
| Origin | Which policy produced it |
| Application | The traffic this was seen on |
| Dir | Request or response leg |
| User | The identity attributed to the call, when one was extracted |
| First seen / Last seen | Deduplication window bounds |
| Status | Triage state |
Alerts fire in MONITOR mode too. A monitored policy that blocks nothing still
raises the same alert, tagged with the enforcement mode it came from, which is
what makes a monitor-mode rollout measurable.
Repeated identical findings are deduplicated rather than emitted per request —
First seen and Last seen are the bounds of that window.
The audit record
Set logFullAudit: true and the gateway writes one structured JSON line per
request to stdout, under the ai_gateway_audit target. Ship stdout with your
existing log shipper; no separate exporter is needed.
config:
policies:
logFullAudit: true
A blocked request looks like this:
{
"timestamp": "2026-09-02T04:31:12.900Z",
"request_id": "8cdb2887-ff56-4dac-9f9a-25eb03811657",
"source_ip": "10.0.4.19",
"source_identity": null,
"user_id": null,
"user_email": null,
"destination_host": "api.openai.com",
"resolved_upstream": "api.openai.com:443",
"destination_path": "/v1/chat/completions",
"method": "POST",
"user_agent": "curl/8.5.0",
"classification": {
"category": "llm_provider", "provider": "openai",
"confidence": 1.0, "matched_rule": null
},
"governance": {
"decision": "AllowWithInspection", "blocked": false,
"matched_policy": "inspect-all-traffic", "reason": null
},
"inspection": {
"action": "Reject",
"finding_count": 1,
"highest_severity": "high",
"findings": [
{
"category": "InjectionHeuristics",
"finding_type": "InjectionHeuristics",
"severity": "high",
"scanner": "InjectionHeuristics",
"description": "LLM Bastion scanner 'InjectionHeuristics' blocked request content"
}
]
},
"action": {
"decision": "Block",
"actions_taken": [
{ "name": "inspection-reject", "description": "Request blocked by inspection findings" }
],
"headers_injected": 0,
"body_rewritten": false
},
"model_name": "gpt-4o-mini",
"total_latency_ms": 9.3,
"classification_latency_us": 6,
"governance_latency_us": 66,
"inspection_latency_us": 9276
}
A masked request carries "action": "Masked" and "body_rewritten": true, with
the finding's byte offsets:
"inspection": {
"action": "Masked",
"finding_count": 1,
"highest_severity": "critical",
"findings": [
{ "category": "pii", "finding_type": "ssn", "severity": "critical",
"scanner": "unified-pii", "description": "ssn detected",
"location": { "start": 71, "end": 82 } }
]
}
The fields worth building detections on:
| Field | Use it for |
|---|---|
request_id | Joining the request and response lines for one call. Also returned in the body of a block and injected as a response header |
inspection.action | Allow, Flagged, Masked, Reject |
inspection.highest_severity | Alert routing |
inspection.findings[].finding_type | The specific pattern or scanner |
governance.decision | Whether inspection ran at all — a stream of "Allow" with "inspection": null means nothing is being scanned |
action.decision | Pass or Block |
That fifth row is the one to build a dashboard panel on. A governance decision
of Allow together with "inspection": null is the machine-readable form of
the governance gate problem.
Keeping lines a sane size
A large prompt can produce hundreds of findings on one line.
| Setting | Effect |
|---|---|
logFindingsMinSeverity | Only findings at or above this severity are written into findings[] |
logFindingsMaxPerLine | Caps how many are written |
config:
policies:
logFullAudit: true
logFindingsMinSeverity: high
logFindingsMaxPerLine: 20
Filtering affects the log line only. Everything still counts in
finding_count, still sets highest_severity, and still raises an alert — so a
filtered line cannot hide a violation, only its detail.
Known gap: the response leg writes no findings
The request leg writes the full record above. The response leg writes only a thin line:
ai_gateway_audit upstream response
request_id=3d86dd84-… destination=api.openai.com:443
status_code=403 latency_ms=2.21
response_snippet="{\"error\":\"blocked\",\"reason\":\"InjectionHeuristics detected
in LLM response\",\"policy\":\"response-inspection\",\"direction\":\"response\"}"
event_type="upstream_response"
There is no findings array, no finding_count, no highest_severity and no
category. Response-side findings — which is where indirect prompt injection
is caught — are delivered to the Levo platform as alerts and are visible under
Issues → AI Governance, but they do not appear in the audit log in a form a SIEM
can parse as a finding.
Until this is closed, detect response-leg blocks on the shape of the thin line
instead: event_type="upstream_response" together with status_code=403 and a
response_snippet containing "direction":"response". That tells you a block
happened and which request it belongs to, but not which scanner fired or at what
severity.
This page will be updated when the response leg emits a full record.
A worked example
Ship stdout to your SIEM and these are the two detections worth having on day one.
Something was blocked.
event_type != "upstream_response" AND action.decision = "Block"
| stats count by inspection.findings{}.finding_type, source_ip
Nothing is being inspected. A steady rate of this over a window where you expect scanning means the governance gate is not open:
governance.decision = "Allow" AND isnull(inspection)
| timechart count
For the Guardrail API rather than the proxy, the same records are written for
input_type: request calls. See
Guardrail API.