Observability
Guardrails exposes every check result through a typed callback — on_guardrail_event — so you can pipe events into any storage backend, monitoring system, or OpenTelemetry-compatible trace collector without touching the core guardrail pipeline.
How it works
After each guardrail check (including allow events) the SDK calls every function you registered as on_guardrail_event. Callbacks receive two arguments:
| Argument | Type | Description |
|---|---|---|
result |
GuardrailResult |
Raw engine result — enforcement_triggered, execution_failed, info dict |
event |
GuardrailEvent |
Typed, fully-resolved event built from result |
The SDK fans out to all callbacks concurrently (asyncio.gather) and isolates failures so one bad sink never blocks another or the Mend server delivery. Both sync and async callables are supported.
GuardrailEvent
GuardrailEvent is a frozen dataclass that gives you clean, typed access to every field you need for logging or tracing.
from mendguardrails import GuardrailEvent, GuardrailCheckResult
Top-level fields
| Field | Type | Description |
|---|---|---|
id |
str |
Trace UUID (V3 wire id) |
event_id |
str |
Unique event identifier |
timestamp |
str |
ISO-8601 UTC timestamp |
action |
str |
"allow", "block", "alert", "obfuscate", or "error" |
direction |
str |
"input" or "output" |
severity |
str |
"info", "high", or "error" |
guardrail_name |
str |
e.g. "PromptInjection" |
guardrail_category |
str |
Optional category label |
scope |
str |
Policy stage name |
event_type |
str |
Human-readable type e.g. "Prompt Injection" |
detection |
str |
Short description of what was found |
model |
str |
Inference model from the surrounding LLM call |
protected_entity |
str |
Logical entity being protected |
endpoint_type |
str |
"Direct" or "Agent" |
integration_type |
str |
e.g. "OpenAI", "Native SDK" |
guardrail_id |
str \| None |
Connection UUID for trace correlation |
check_result |
GuardrailCheckResult |
Detailed per-check execution data |
metadata |
dict |
Caller-supplied metadata forwarded from AuditConfig |
check_result fields (GuardrailCheckResult)
| Field | Type | Description |
|---|---|---|
enforcement_triggered |
bool |
Whether the guardrail triggered enforcement |
execution_failed |
bool |
Whether the guardrail raised an unhandled exception |
stage |
str |
Stage name ("pre_flight", "input", "output") |
guardrail_name |
str |
Guardrail name |
details |
dict |
Raw info dict from the engine result |
checked_text |
str \| None |
The exact prompt or response that was evaluated |
token_usage |
dict \| None |
LLM token counters for guardrails that invoke an LLM |
exception |
str \| None |
Exception string when execution_failed=True |
Basic usage
Single callback
from mendguardrails import MendGuardrailsAsyncOpenAI, GuardrailEvent
from mendguardrails.types import GuardrailResult
async def my_sink(result: GuardrailResult, event: GuardrailEvent) -> None:
print(
f"[{event.action}] {event.guardrail_name} "
f"direction={event.direction} "
f"text={event.check_result.checked_text!r}"
)
client = MendGuardrailsAsyncOpenAI(
mend_key="...",
on_guardrail_event=my_sink,
)
Multiple sinks
Pass a list to fan out to several backends at once. Each sink runs concurrently; a failure in one does not affect the others.
from mendguardrails.observability.otel import OtelGuardrailHandler
client = MendGuardrailsAsyncOpenAI(
mend_key="...",
on_guardrail_event=[
OtelGuardrailHandler(), # OTel spans
my_database_sink, # custom async DB writer
my_slack_alerter, # sync alerting function
],
)
Native client
on_guardrail_event works identically on MendGuardrailsClient and MendGuardrailsSyncClient.
from mendguardrails import MendGuardrailsClient
client = MendGuardrailsClient(
name="my-service",
on_guardrail_event=my_sink,
)
Saving every prompt to a database
import asyncio
from mendguardrails import MendGuardrailsAsyncOpenAI, GuardrailEvent
from mendguardrails.types import GuardrailResult
async def save_to_db(result: GuardrailResult, event: GuardrailEvent) -> None:
"""Persist every guardrail check to your audit table."""
await db.execute(
"INSERT INTO guardrail_log (event_id, action, guardrail, direction, text, ts) "
"VALUES ($1, $2, $3, $4, $5, $6)",
event.event_id,
event.action,
event.guardrail_name,
event.direction,
event.check_result.checked_text, # the raw prompt / response text
event.timestamp,
)
client = MendGuardrailsAsyncOpenAI(mend_key="...", on_guardrail_event=save_to_db)
The checked_text field on event.check_result always contains the exact string that was evaluated — the user's prompt for input / pre_flight events, and the model's reply for output events.
OpenTelemetry integration
Use OtelGuardrailHandler to emit an OTel span for every guardrail check. See the dedicated OpenTelemetry Integration page for install instructions, span structure, exporter configuration, and the full attribute reference.
from mendguardrails.observability.otel import OtelGuardrailHandler
client = MendGuardrailsAsyncOpenAI(
mend_key="...",
on_guardrail_event=OtelGuardrailHandler(),
)
GuardrailEventCallback type
The full type alias for callbacks is:
from mendguardrails import GuardrailEventCallback
GuardrailEventCallback = Callable[
[GuardrailResult, GuardrailEvent],
Awaitable[None] | None,
]
Both sync and async callables are accepted. Async callbacks are awaited directly; sync callbacks are called and any return value is ignored.
Summary
| Feature | How |
|---|---|
| Single sink | on_guardrail_event=my_fn |
| Multiple sinks | on_guardrail_event=[fn1, fn2, fn3] |
| Access prompt text | event.check_result.checked_text |
| Access action | event.action — "allow", "block", "alert", "obfuscate" |
| OTel spans | on_guardrail_event=OtelGuardrailHandler() |
| OTel + custom sink | on_guardrail_event=[OtelGuardrailHandler(), my_db_sink] |
| Prompt text in spans | OtelGuardrailHandler(capture_prompt_text=True) |