Skip to main content

Logging

Control Zero Self-Managed produces two categories of logs: application logs (operational events from each service) and audit logs (governance decisions). This guide covers configuration, file locations, rotation, and debug mode.

Log Levels

Five log levels are available, from least to most verbose:

LevelDescriptionUse Case
errorErrors that require attention. Service failures, unhandled exceptions.Production (minimal output)
warnWarnings that do not stop operation but indicate potential issues.Production (default)
infoStandard operational events. Service startup, configuration loaded, policy bundle refreshed.Production (recommended)
debugDetailed operational events. Request/response metadata, policy evaluation details, timing.Troubleshooting
traceMaximum verbosity. Full request/response bodies, internal state dumps.Deep debugging only

Default level: info for all services.

Setting Log Levels

All services at once

czctl set-log-level <level>

For example, to set all services to debug:

czctl set-log-level debug

Per-service

czctl set-log-level <level> --service <service-name>

Via runtime configuration

Log levels can also be changed at runtime through the cz_config table. Updates take effect within 60 seconds without a service restart:

curl -s https://localhost:8080/api/v1/config \
-X PATCH \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"key": "gateway.log_level", "value": "debug"}'

Available runtime config keys:

Config KeyEquivalent Environment Variable
gateway.log_levelCZ_GATEWAY_LOG_LEVEL
api.log_levelCZ_API_LOG_LEVEL
engine.log_levelCZ_ENGINE_LOG_LEVEL
proxy.log_levelCZ_PROXY_LOG_LEVEL
scout.log_levelCZ_SCOUT_LOG_LEVEL

Via environment variables

Set the log level in config/controlzero.env or directly in docker-compose.yml:

ServiceEnvironment Variable
GatewayCZ_GATEWAY_LOG_LEVEL
APICZ_API_LOG_LEVEL
Policy EngineCZ_ENGINE_LOG_LEVEL
SSL ProxyCZ_PROXY_LOG_LEVEL
ScoutCZ_SCOUT_LOG_LEVEL

After changing environment variables, restart the affected service:

docker compose restart <service-name>

Log File Locations

Container logs (via Docker)

docker compose logs <service-name>
docker compose logs <service-name> --tail 100
docker compose logs <service-name> --follow

Persistent log files

If file-based logging is enabled (default for enterprise deployments), logs are written to:

ServiceFile Path
Gateway/var/log/controlzero/gateway.log
API/var/log/controlzero/api.log
Policy Engine/var/log/controlzero/engine.log
SSL Proxy/var/log/controlzero/proxy.log
Scout/var/log/controlzero/scout.log
Audit DB/var/log/controlzero/audit-db.log

These paths are inside the containers and mapped to the host via Docker volumes. The host path defaults to /opt/controlzero/logs/.

Log Rotation

Default rotation policy

SettingDefault Value
Maximum file size100 MB
Maximum file count10 (per service)
Compressiongzip after rotation

Configuring rotation

Log rotation is managed through the Docker logging driver. Configure it in docker-compose.yml under each service:

services:
gateway:
logging:
driver: json-file
options:
max-size: '100m'
max-file: '10'

Apply changes:

docker compose restart

External log rotation

If you prefer to manage rotation with an external tool (logrotate or similar), set the Docker logging driver to local or none and configure your external rotation tool to manage files in /opt/controlzero/logs/.

SIEM Compatibility

All Control Zero services write Docker JSON logs to the standard Docker log path:

/var/lib/docker/containers/<container-id>/<container-id>-json.log

These logs are compatible with any SIEM or log collector that supports the Docker JSON log format, including:

  • Splunk -- Use the Splunk Docker logging driver or forward from /var/lib/docker/containers/
  • Elasticsearch / OpenSearch -- Use Filebeat with the Docker input
  • Datadog -- Use the Datadog Agent with Docker log collection enabled
  • Fluentd / Fluent Bit -- Use the Fluentd Docker logging driver or tail the JSON log files
  • Syslog -- Use the Docker syslog logging driver for direct forwarding

To forward logs via the Docker daemon, configure the logging driver globally in /etc/docker/daemon.json or per-service in docker-compose.yml.

Audit event export: CEF and Syslog

The methods above forward the raw Docker JSON application logs. For the governance audit trail specifically (every allow/deny/warn decision), Control Zero emits structured ArcSight CEF and RFC 5424 syslog so the records land cleanly in Splunk, IBM QRadar, and Micro Focus ArcSight without a custom parser. There are two delivery modes; both are available on self-managed and air-gapped installs.

1. Pull (export endpoint). Add ?format=cef or ?format=syslog to the audit export endpoint. json (the default) is unchanged and byte-identical to prior releases.

# CEF lines (one event per line)
curl -H "Authorization: Bearer $CZ_TOKEN" \
"https://controlzero.example/api/projects/$PROJECT_ID/export?format=cef&start=$START_MS&end=$END_MS"

# RFC 5424 syslog frames wrapping CEF
curl -H "Authorization: Bearer $CZ_TOKEN" \
"https://controlzero.example/api/projects/$PROJECT_ID/export?format=syslog"

The export respects the same RBAC (org membership), time-range, and feature gating (audit_export, Solo+ tier) as the JSON export. An unwired audit store returns 501; a non-member returns 403; an unrecognized format returns 400.

2. Push (outbound forwarder). Set SIEM_SYSLOG_ADDR to stream each durably-stored decision to a collector over UDP, TCP, or TLS. Configure it via environment (operator-only; never tenant-controlled):

SIEM_SYSLOG_ADDR      host:port of the collector (presence enables forwarding)
SIEM_SYSLOG_PROTO udp | tcp | tcp+tls (default: tcp)
SIEM_SYSLOG_FORMAT cef | json (default: cef)
SIEM_SYSLOG_APP_NAME RFC5424 APP-NAME (default: control-zero)
SIEM_SYSLOG_TLS_CA PEM CA bundle path (tcp+tls only)
SIEM_SYSLOG_TLS_CERT client cert path for mutual TLS (optional)
SIEM_SYSLOG_TLS_KEY client key path for mutual TLS (optional)

The forwarder is fire-and-forget: it never blocks or fails audit ingest. A slow or unreachable collector causes rows to be dropped (the durable audit store remains the system of record), counted by the Prometheus metric controlzero_siem_forward_failed_total (labels: buffer_full, dial, write). On an air-gapped install the collector sits on the customer's private network; the forwarder intentionally permits private destinations because the target is operator-configured, not a tenant-supplied URL.

CEF field mapping. Each audit row renders as:

CEF:0|ControlZero|control-zero|<ver>|<decision>|<tool>|<severity>|rt=<epoch_ms> src=<client_ip> suser=<user_email> shost=<hostname> act=<decision> outcome=<status> cs1=<policy_id> cs1Label=PolicyID cs2=<reason_code> cs2Label=ReasonCode cs3=<source> cs3Label=Source cs4=<org_id> cs4Label=OrgID cs5=<project_id> cs5Label=ProjectID cs6=<sdk_version> cs6Label=SDKVersion deviceProcessName=<agent_id> externalId=<id>

Severity follows the decision: deny = 8, warn = 5, allow = 3. The RFC 5424 frame computes PRI = local0(16) * 8 + severity, where the syslog severity is deny = Error(3), warn = Warning(4), allow = Informational(6). All header and extension values are escaped per the CEF spec, so a crafted tool name or reason cannot break the line or inject a second event.

Audit Logs vs Application Logs

Application logs

Operational events from each service. Used for troubleshooting and monitoring service health. Contain information like startup messages, configuration changes, error stack traces, and request timing.

Audit logs

Governance decision records stored in the immutable audit trail. Every policy evaluation produces an audit record with:

  • Timestamp
  • Agent identity
  • Action evaluated
  • Resource
  • Decision (allow or deny)
  • Policy ID that matched
  • Token usage (if applicable)
  • Request metadata

Audit logs are queried through the dashboard or the management API. They are stored separately from application logs and have their own retention policy.

Field reference

Every audit row includes the fields below. The dashboard renders a subset by default; the rest are accessible by expanding a row, exporting CSV, or querying the management API.

FieldTypeDescription
idUUIDStable identifier for the row. SDK-supplied to make retries idempotent.
timestampISO 8601When the host runtime fired the event (UTC).
org_idUUIDOwning organisation. Server-enforced, not client-supplied.
project_idUUIDProject the API key belongs to.
agent_idstringLogical agent name (e.g. code-assistant, support-bot). Free-form; set at SDK init.
tool_namestringCanonical tool name the policy engine evaluated. See the canonical tool names reference for the full list of host-tool mappings.
method_namestringSub-method on the tool (e.g. SELECT on database). * when not extracted.
extracted_methodstringMethod the SDK hook extractor resolved from arguments. Empty when no extractor ran.
decisionenumPolicy outcome: allow, deny, warn.
statusenumWire status: success, denied, error.
policy_idstringUUID of the policy rule that matched, if any.
policy_matchedstringHuman-readable name of the matched rule.
latency_msuintEnd-to-end evaluation time, in milliseconds.
providerstringLLM provider for token-bearing calls (e.g. openai, anthropic).
model_idstringLLM model identifier (e.g. claude-sonnet-4).
input_tokensuintPrompt tokens for LLM calls. Zero for non-LLM tool calls.
output_tokensuintCompletion tokens for LLM calls. Zero for non-LLM tool calls.
estimated_cost_usdfloatBest-effort cost estimate based on the provider price card.
client_ipstringSource IP the audit event arrived from.
hostnamestringWorkstation hostname the SDK saw at runtime.
user_emailstringDeveloper email captured by enrolled SDKs (fleet attribution).
machine_idUUIDStable machine identifier from enrolment.
client_namestringFree-form client label the SDK supplies (e.g. claude-code, cursor). Pre-dates the canonical source field; both still flow.
client_versionstringVersion of the host CLI / agent runtime.
sdk_versionstringControl Zero SDK version that emitted the row (e.g. python-1.3.0, gateway-0.2.0).
sourceenumCanonical host runtime that emitted this event. See the table below.
tagsobjectFree-form key/value metadata the SDK attaches.

source field — allowed values

source answers the question "which thing fired this event": the user's coding CLI, an MCP server, a direct SDK integration, or the proxy gateway. Values are lowercase, snake_case, and constrained to the set below. The dashboard renders an unset or unknown source as -- rather than the literal word Unknown.

ValueEmitted by
claude_codeClaude Code via the Control Zero Python SDK PreToolUse hook.
gemini_cliGemini CLI via the Control Zero Node SDK hook.
codex_cliCodex CLI sandbox tap.
kiro_cliKiro CLI (kiro-cli) via the Control Zero SDK hook.
kiro_ideKiro IDE (AWS agentic IDE) via the Control Zero SDK hook.
antigravityAntigravity (Google "agy") via the Control Zero SDK hook.
cursor_ideCursor IDE via the Control Zero SDK hook.
cursor_cliCursor CLI (cursor-agent) via the Control Zero SDK hook.
mcp_serverAn MCP server using @controlzero/mcp-server.
python_sdkDirect Python SDK integration (no host CLI -- application code calling guard).
node_sdkDirect Node SDK integration.
go_sdkDirect Go SDK integration.
gatewayThe Control Zero proxy gateway (LLM-API-shaped traffic).
unknownFallback when the SDK did not supply a value and nothing in the request can be inferred. The dashboard renders this as --.

Resolving source server-side

When the SDK does not set source explicitly, the API derives one with the following ladder:

  1. Use the wire source field if it is in the allowed set.
  2. Else map the SDK's client_name (e.g. claude-code -> claude_code).
  3. Else infer from sdk_version (e.g. gateway-0.2.0 -> gateway, python-1.3.0 -> python_sdk).
  4. Else default to literal lowercase unknown.

This means rows from older SDK versions that still emit client_name continue to populate the SOURCE column correctly, even before customers upgrade.

DLP findings in audit logs

When the gateway detects PII in a request or response, the audit log entry includes a dlp_findings array with details about each detection:

{
"timestamp": "2026-04-05T14:22:10.456Z",
"level": "info",
"service": "gateway",
"message": "Policy evaluation completed",
"fields": {
"agent_id": "agent-prod-01",
"action": "llm:generate",
"decision": "allow",
"latency_ms": 2.1,
"request_id": "req_abc123def456",
"dlp_findings": [
{
"direction": "response",
"type": "credit_card",
"locale": "default",
"action_taken": "mask",
"count": 1
}
]
}
}

Each finding includes the scan direction (request or response), the pattern type, the locale that matched, the action taken (detect, mask, or block), and the number of occurrences.

Correlation IDs in audit logs

Every audit log entry includes a request_id field containing the correlation ID for that request. This is the same value returned in the X-Request-ID response header.

Use correlation IDs to join audit log entries with your application logs and upstream provider logs for end-to-end request tracing.

# Find all audit entries for a specific request
cat /opt/controlzero/logs/gateway.log | jq 'select(.fields.request_id == "req_abc123def456")'

Prometheus metrics reference

The gateway exposes seven metric families on the /metrics endpoint in Prometheus exposition format. These metrics complement the structured audit logs with real-time operational telemetry.

MetricTypeLabels
cz_gateway_requests_totalCounterprovider, model, status_code
cz_gateway_request_duration_secondsHistogramprovider, model
cz_gateway_policy_evaluations_totalCounterdecision
cz_gateway_pii_detections_totalCountertype, direction
cz_gateway_rate_limit_hits_totalCounterscope
cz_gateway_upstream_errors_totalCounterprovider, status_code
cz_gateway_active_connectionsGauge--

Scrape the /metrics endpoint with Prometheus and build Grafana dashboards from the counters and histograms above.

Audit log retention

Configure retention in config/audit.yml:

audit:
retention_days: 90
partition_by: month

Debug Mode

Enable debug mode when troubleshooting specific issues. Debug mode increases log verbosity for all services simultaneously.

Enable debug mode

czctl set-log-level debug

Enable trace mode

Use trace mode only when debug output is insufficient. Trace mode logs full request and response bodies, which can generate large volumes of output and may include sensitive data.

czctl set-log-level trace

Disable debug mode

Restore the default log level after troubleshooting:

czctl set-log-level info

Structured JSON Log Format

All services output logs in structured JSON format for compatibility with log aggregation systems (Splunk, Elasticsearch, Datadog, and similar):

{
"timestamp": "2026-04-03T10:15:30.123Z",
"level": "info",
"service": "gateway",
"message": "Policy evaluation completed",
"fields": {
"agent_id": "agent-prod-01",
"action": "database:query",
"decision": "allow",
"latency_ms": 1.2,
"policy_id": "pol_abc123"
}
}

Parsing with common tools

# Filter errors from gateway logs
cat /opt/controlzero/logs/gateway.log | jq 'select(.level == "error")'

# Count decisions by result
cat /opt/controlzero/logs/gateway.log | jq 'select(.message == "Policy evaluation completed") | .fields.decision' | sort | uniq -c

Audit log argument redaction

Argument values for each governed tool call can be stored in three modes: key-only (the default), encrypted at rest with admin-only unmask, or plaintext for admin-only viewing. By default, both hosted and self-managed deployments run in key-only mode -- values are dropped at the SDK boundary and never reach the audit store. Self-managed operators who need argument-level visibility for incident response can opt in to store_full (database is single-tenant trusted) or redact_with_admin_unmask (encrypted-at-rest with audit-on-decrypt chain). See the customer-facing reference at docs/sdk/policies/audit-redaction.md for the trade-offs and the opt-in flow.