Skip to content

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:

  1. A PostgreSQL database (version 13+)
  2. The database.dsn configuration set
  3. Both audit.enabled and audit.log_tool_calls set to true

Database migrations run automatically on startup and create the audit_logs table.

Configuration

database:
  dsn: "${DATABASE_DSN}"

audit:
  enabled: true
  log_tool_calls: true
  retention_days: 90
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]:

  • password
  • secret
  • token
  • api_key
  • authorization
  • credentials

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:

DELETE FROM audit_logs WHERE timestamp < NOW() - INTERVAL '90 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:

Auth/Authz -> Audit -> Rules -> Enrichment -> Tool Handler
  1. MCPToolCallMiddleware (outermost) authenticates the user, resolves the persona, looks up toolkit metadata, and stores everything in a PlatformContext on the request context.
  2. MCPAuditMiddleware (inner to auth) receives the context with PlatformContext already set. It records the start time, calls the next handler, measures duration, then reads all fields from PlatformContext to build the audit event.
  3. 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.Error but 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

  1. Verify both audit.enabled: true AND audit.log_tool_calls: true are set in your config.
  2. Verify database.dsn is configured and the database is reachable.
  3. Check platform startup logs for "audit logging enabled". If you see "using noop logger", the database connection failed.
  4. Check for slog.Error messages 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.