Skip to content

Observability & Audit

Observability & Audit

The admin system provides three layers of observability: structured logging, Analytics Engine events, and a queryable audit log.

Structured Logging

All admin operations produce structured JSON logs via AdminLogger (worker/services/admin-logger.ts). Logs are emitted to console.log() which Cloudflare Workers captures in Workers Logs.

Log Format

Every log entry includes:

{
"level": "info",
"message": "Role assigned successfully",
"requestId": "a1b2c3d4",
"operation": "role.assign",
"actorId": "user_2abc123",
"resourceType": "admin_role_assignment",
"resourceId": "user_2xyz789",
"durationMs": 42,
"status": "success",
"timestamp": "2025-01-15T10:30:00.000Z"
}

Log Fields

FieldTypeDescription
levelstringinfo, warn, or error
messagestringHuman-readable description
requestIdstring8-char hex trace ID (unique per request)
operationstringAction identifier (e.g. role.assign, tier.update)
actorIdstringClerk user ID of the admin performing the action
resourceTypestringWhat kind of resource was affected
resourceIdstringSpecific resource identifier
durationMsnumberOperation duration in milliseconds
statusstringsuccess, error, or denied
timestampstringISO 8601 timestamp

Request Tracing

Each request gets a unique requestId generated by slicing a UUID:

function createRequestId(): string {
return crypto.randomUUID().replace(/-/g, '').slice(0, 8);
}

This ID is included in every log line for the request, making it easy to correlate related log entries:

Terminal window
# Find all logs for a specific request
wrangler tail --format json | jq 'select(.requestId == "a1b2c3d4")'

Operation Tracing

The withAdminTracing() wrapper automatically logs operation start, duration, and success/failure:

const result = await withAdminTracing(logger, 'tier.update', async () => {
// Operation code here
return updatedTier;
});
// Automatically logs: start, success + durationMs, or error + durationMs

PII Sanitization

Before logging objects that may contain sensitive data, the sanitizeForLog() function deep-clones the object and redacts any keys matching:

/^(password|secret|token|key|authorization)$/i

Redacted values are replaced with '[REDACTED]'.

Logger API

// Create a logger for a request
const logger = createAdminLogger(requestId, { operation: 'role.assign' });
// Chain context
const scopedLogger = logger
.withOperation('tier.update')
.withActor('user_2abc123');
// Log at different levels
scopedLogger.info('Tier updated', { tierName: 'pro', newRateLimit: 500 });
scopedLogger.warn('Deprecated tier referenced', { tierName: 'legacy' });
scopedLogger.error('D1 query failed', { error: err.message });

Analytics Engine Events

Admin actions are reported to Cloudflare Analytics Engine for dashboarding and alerting. Events use the ANALYTICS_ENGINE binding.

Event Types

EventTriggerKey Data Points
admin_actionAny successful admin mutationactor, action, resource_type, duration_ms
admin_auth_failureJWT validation failure or missing admin roleip_address, user_agent, attempted_path
admin_config_changeTier, scope, or endpoint override modifiedresource_type, old_value_hash, new_value_hash
flag_evaluationFeature flag evaluated at the edgeflag_name, result (on/off), user_tier

Writing Events

env.ANALYTICS_ENGINE?.writeDataPoint({
blobs: ['admin_action', actorId, action, resourceType],
doubles: [durationMs],
indexes: [requestId],
});

Querying (Workers Analytics Engine SQL API)

-- Admin actions in the last 24 hours
SELECT
blob1 AS event_type,
blob2 AS actor_id,
blob3 AS action,
COUNT(*) AS count
FROM bloqr_compiler_analytics
WHERE blob1 = 'admin_action'
AND timestamp > NOW() - INTERVAL '24' HOUR
GROUP BY blob1, blob2, blob3
ORDER BY count DESC;

Audit Log

The admin_audit_logs table in ADMIN_DB provides a persistent, append-only audit trail for all admin mutations.

What Gets Logged

Every state-changing admin operation creates an audit entry:

Action PatternResource TypeExample
role.createadmin_roleNew role definition created
role.updateadmin_roleRole permissions modified
role.assignadmin_role_assignmentRole assigned to user
role.revokeadmin_role_assignmentRole revoked from user
tier.updatetier_configTier rate limit changed
tier.deletetier_configTier removed
scope.updatescope_configScope configuration changed
scope.deletescope_configScope removed
endpoint.createendpoint_auth_overrideNew endpoint override
endpoint.updateendpoint_auth_overrideOverride modified
endpoint.deleteendpoint_auth_overrideOverride removed
flag.createfeature_flagNew feature flag
flag.updatefeature_flagFlag settings changed
flag.deletefeature_flagFlag removed
announcement.createadmin_announcementNew announcement
announcement.updateadmin_announcementAnnouncement modified
announcement.deleteadmin_announcementAnnouncement removed

Audit Entry Structure

Each entry captures the complete change context:

{
"id": 42,
"actor_id": "user_2abc123",
"actor_email": "admin@example.com",
"action": "tier.update",
"resource_type": "tier_config",
"resource_id": "pro",
"old_values": "{\"rate_limit\": 300}",
"new_values": "{\"rate_limit\": 500}",
"ip_address": "203.0.113.1",
"user_agent": "Mozilla/5.0...",
"status": "success",
"metadata": null,
"created_at": "2025-01-15T10:30:00"
}

Querying the Audit Log

Use GET /admin/system/audit with query parameters:

Terminal window
# All actions by a specific admin
curl "/admin/system/audit?actor_id=user_2abc123&limit=50" \
-H "Authorization: Bearer $JWT"
# All tier changes in the last week
curl "/admin/system/audit?resource_type=tier_config&since=2025-01-08T00:00:00Z" \
-H "Authorization: Bearer $JWT"
# Failed or denied actions
curl "/admin/system/audit?status=denied&limit=100" \
-H "Authorization: Bearer $JWT"
# Specific action type with date range
curl "/admin/system/audit?action=flag.update&since=2025-01-01T00:00:00Z&until=2025-01-31T23:59:59Z" \
-H "Authorization: Bearer $JWT"

Available Filters

ParameterTypeDescription
actor_idstringFilter by Clerk user ID
actionstringFilter by action (e.g. tier.update)
resource_typestringFilter by resource type (e.g. feature_flag)
resource_idstringFilter by specific resource
statusstringsuccess, failure, or denied
sincestringISO 8601 start date (inclusive)
untilstringISO 8601 end date (inclusive)
limitnumberResults per page (default: 50, max: 100)
offsetnumberPagination offset

Permission

Querying audit logs requires the audit:read permission. All three built-in roles (viewer, editor, super-admin) include this permission.

Observability Stack Summary

flowchart TD
    subgraph "Admin Request"
        A["Admin API Call"]
    end

    subgraph "Real-time"
        B["AdminLogger<br/>Structured JSON"]
        C["Workers Logs<br/>wrangler tail"]
    end

    subgraph "Analytics"
        D["Analytics Engine<br/>writeDataPoint()"]
        E["CF Dashboard<br/>SQL queries"]
    end

    subgraph "Persistent"
        F["admin_audit_logs<br/>ADMIN_DB D1"]
        G["GET /admin/system/audit<br/>Paginated API"]
    end

    A --> B --> C
    A --> D --> E
    A --> F --> G
LayerPurposeRetentionQuery Method
Structured LogsReal-time debugging and tracing~24 hours (Workers Logs)wrangler tail, Logpush
Analytics EngineDashboards, alerting, trends90 daysWorkers Analytics Engine SQL API
Audit LogCompliance, forensics, change historyIndefinite (D1)GET /admin/system/audit API