Audit Logging¶
Every tool call flows through the audit logging middleware, which records who called what, when, how long it took, and whether it succeeded. Audit logs are stored in PostgreSQL and automatically cleaned up based on a configurable retention period.
Prerequisites¶
Audit logging requires:
- A PostgreSQL database (version 13+)
- The
database.dsnconfiguration set - Both
audit.enabledandaudit.log_tool_callsset totrue
Database migrations run automatically on startup and create the audit_logs table.
Configuration¶
| Option | Type | Default | Description |
|---|---|---|---|
database.dsn |
string | - | PostgreSQL connection string. Required for audit logging. |
audit.enabled |
bool | false |
Master switch for audit logging. |
audit.log_tool_calls |
bool | false |
Log every tools/call request. Both this and enabled must be true. |
audit.retention_days |
int | 90 |
Days to keep audit logs before automatic cleanup. |
If audit.enabled is true but no database is configured, the platform logs a warning and falls back to a no-op logger.
What Gets Logged¶
Every successful or failed tool call produces one row in audit_logs:
{
"id": "3sK2DlE7x9mPqR4vNw8bYA",
"timestamp": "2026-02-04T10:30:00.123Z",
"duration_ms": 152,
"request_id": "req-a1b2c3d4e5f67890abcdef1234567890",
"user_id": "[email protected]",
"user_email": "[email protected]",
"persona": "analyst",
"tool_name": "trino_query",
"toolkit_kind": "trino",
"toolkit_name": "production",
"connection": "prod-trino",
"parameters": {"sql": "SELECT count(*) FROM orders"},
"success": true,
"error_message": "",
"created_date": "2026-02-04"
}
Field Reference¶
| Field | Type | Description |
|---|---|---|
id |
VARCHAR(32) | Cryptographically random event ID (base64url-encoded, 16 bytes). |
timestamp |
TIMESTAMPTZ | When the tool call started. |
duration_ms |
INTEGER | Wall-clock time from request start to handler completion, in milliseconds. Includes rule enforcement and semantic enrichment time. |
request_id |
VARCHAR(255) | Unique request ID generated by the auth middleware (hex-encoded, 16 bytes). |
user_id |
VARCHAR(255) | Authenticated user identity. From OIDC sub claim, API key name, or OAuth token subject. |
user_email |
VARCHAR(255) | User email from OIDC claims, if available. |
persona |
VARCHAR(100) | Resolved persona name (e.g., analyst, admin). Set by the authorizer based on the user's roles. |
tool_name |
VARCHAR(255) | MCP tool name (e.g., trino_query, datahub_search, s3_list_buckets). |
toolkit_kind |
VARCHAR(100) | Toolkit type: trino, datahub, or s3. |
toolkit_name |
VARCHAR(100) | Toolkit instance name from configuration (e.g., production, staging). |
connection |
VARCHAR(100) | Connection name from toolkit config. Identifies which backend instance handled the request. |
parameters |
JSONB | Tool call arguments with sensitive values redacted. See Parameter Sanitization. |
success |
BOOLEAN | true if the tool handler returned without error and IsError was not set. |
error_message |
TEXT | Error description if success is false. |
created_date |
DATE | Partition key derived from timestamp. Used for retention cleanup. |
Parameter Sanitization¶
Tool call arguments are logged for debugging and compliance, but sensitive values are automatically redacted before storage. The following parameter keys are replaced with [REDACTED]:
passwordsecrettokenapi_keyauthorizationcredentials
Matching is case-sensitive and exact. A parameter named user_password would not be redacted (only password is matched).
Database Schema¶
The audit_logs table is partitioned by created_date for efficient retention management:
CREATE TABLE audit_logs (
id VARCHAR(32) NOT NULL,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
duration_ms INTEGER,
request_id VARCHAR(255),
user_id VARCHAR(255),
user_email VARCHAR(255),
persona VARCHAR(100),
tool_name VARCHAR(255) NOT NULL,
toolkit_kind VARCHAR(100),
toolkit_name VARCHAR(100),
connection VARCHAR(100),
parameters JSONB,
success BOOLEAN NOT NULL,
error_message TEXT,
created_date DATE NOT NULL DEFAULT CURRENT_DATE,
PRIMARY KEY (id, created_date)
) PARTITION BY RANGE (created_date);
Indexes¶
| Index | Column | Use Case |
|---|---|---|
idx_audit_logs_timestamp |
timestamp |
Time-range queries |
idx_audit_logs_user_id |
user_id |
Per-user audit trails |
idx_audit_logs_tool_name |
tool_name |
Tool usage analytics |
idx_audit_logs_success |
success |
Failure investigation |
idx_audit_logs_created_date |
created_date |
Partition pruning, retention cleanup |
Querying Audit Logs¶
Recent tool calls by user¶
SELECT timestamp, tool_name, toolkit_kind, success, duration_ms
FROM audit_logs
WHERE user_id = '[email protected]'
ORDER BY timestamp DESC
LIMIT 20;
Failed calls in the last 24 hours¶
SELECT timestamp, user_id, tool_name, error_message, duration_ms
FROM audit_logs
WHERE success = false
AND timestamp > NOW() - INTERVAL '24 hours'
ORDER BY timestamp DESC;
Tool usage by toolkit¶
SELECT toolkit_kind, tool_name, COUNT(*) as calls,
AVG(duration_ms) as avg_ms, SUM(CASE WHEN success THEN 0 ELSE 1 END) as failures
FROM audit_logs
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY toolkit_kind, tool_name
ORDER BY calls DESC;
Persona activity¶
SELECT persona, COUNT(*) as calls, COUNT(DISTINCT user_id) as users
FROM audit_logs
WHERE timestamp > NOW() - INTERVAL '30 days'
GROUP BY persona
ORDER BY calls DESC;
Slow queries (> 5 seconds)¶
SELECT timestamp, user_id, tool_name, duration_ms,
parameters->>'sql' as query
FROM audit_logs
WHERE duration_ms > 5000
AND toolkit_kind = 'trino'
ORDER BY duration_ms DESC
LIMIT 10;
Retention and Cleanup¶
A background goroutine runs every 24 hours and deletes rows older than retention_days:
The cleanup routine starts automatically when the audit store is initialized. It runs on the application's lifecycle context and stops when the platform shuts down.
The table is partitioned by created_date, so retention deletes operate efficiently on date ranges. For high-volume deployments, consider creating explicit date partitions instead of relying on the default partition:
CREATE TABLE audit_logs_2026_02 PARTITION OF audit_logs
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
How It Works¶
Audit logging is implemented as MCP protocol-level middleware. The middleware chain execution order is:
- MCPToolCallMiddleware (outermost) authenticates the user, resolves the persona, looks up toolkit metadata, and stores everything in a
PlatformContexton the request context. - MCPAuditMiddleware (inner to auth) receives the context with
PlatformContextalready set. It records the start time, calls the next handler, measures duration, then reads all fields fromPlatformContextto build the audit event. - The audit event is written asynchronously in a goroutine to avoid blocking the tool response. If the database write fails, the error is logged via
slog.Errorbut the tool call still succeeds.
Unauthorized requests are rejected by MCPToolCallMiddleware before reaching the audit middleware, so they are not logged. Only authenticated, authorized tool calls appear in audit logs.
Troubleshooting¶
No audit logs appearing¶
- Verify both
audit.enabled: trueANDaudit.log_tool_calls: trueare set in your config. - Verify
database.dsnis configured and the database is reachable. - Check platform startup logs for
"audit logging enabled". If you see"using noop logger", the database connection failed. - Check for
slog.Errormessages containing"failed to log audit event", which indicate database write failures.
Audit logs have empty fields¶
If user_id or other identity fields are empty, check your authentication configuration. API key authentication populates user_id with the key name. OIDC populates it from the sub claim.
If toolkit_kind, toolkit_name, or connection are empty, the tool was not found in any registered toolkit's tool list. This can happen with platform-level tools like platform_info that are not part of a toolkit.