Structured Logging
JSON tracing output by default, with a redaction layer that scrubs known-sensitive field names.
Both binaries log via tracing + tracing-subscriber. The default
output is JSON, one line per event, suitable for shipping to any
log collector.
Format
# server.toml
log_format = "json" # default
# log_format = "compact" # human-readable for local devA typical line:
{
"timestamp": "2026-05-06T12:01:30Z",
"level": "INFO",
"event": "rule.activated",
"client_name": "edge-01",
"rule_id": 1,
"request_id": "01HM…"
}Filtering
Use the standard tracing-subscriber env-filter:
RUST_LOG=info portunus-server serve
RUST_LOG=portunus_server=debug,portunus_client=info portunus-server serve
# Per-target filtering
RUST_LOG=tls_sni=trace portunus-client
RUST_LOG=portunus_client::forwarder::udp=debug portunus-clientrequest_id propagation
request_id (ULID) is propagated end-to-end through RuleUpdate /
RuleStatus / audit events. Correlate operator request → server audit
→ client rule activation by the same request_id.
Redaction layer
A defense-in-depth RedactionLayer inspects every event and span for
field names containing token, secret, or private_key
(case-insensitive substring match). It does not rewrite the
offending value — it is a runtime tripwire: on a match it increments a
violation counter and emits a separate audit.redaction_violation
event to stderr so the leak is visible even if a misconfigured
formatter swallowed the original line.
Values are redacted at the point of recording (call sites write
token = "<redacted>" rather than the raw value); the layer exists to
fail loudly if a future patch forgets.
The audit code path in particular never sees raw bearer tokens —
the auth middleware verifies the bearer and hands the post-verify
OperatorIdentity to the audit emitter, so even an operator
implementation bug cannot leak a token through structured logging.
Common events to alert on
These are real event= field values. Alert on them by matching the
event field, not free-text.
| Event | Severity | Cause |
|---|---|---|
tls.cert_regenerated | WARN | Operator certificate regenerated (e.g. expired / SAN change) |
tls.operator_cert_in_use | INFO | Existing operator certificate reused on startup |
auth.failure | WARN | Client bearer rejected (revoked / unknown / signature mismatch) |
operator.deny | WARN | RBAC rejection on the operator API |
client.disconnected | WARN/INFO | Client lost the gRPC stream (transient or revoke) |
client.transport_error | WARN | gRPC transport-level error on a client stream |
store.pool_exhausted | WARN | SQLite connection pool exhausted under load |
rule.failed | WARN | Rule activation failed (e.g. port_in_use) |
Some fatal startup conditions abort before the logging pipeline is
useful and surface as the process error message rather than an event
field: startup.unsupported_filesystem (--data-dir on NFS / tmpfs /
ramfs), store_in_use (two server processes sharing --data-dir), and
schema_version_too_new (an older binary against a newer DB schema).
Local development tips
# Compact, tagged-by-target format for tailing
RUST_LOG=info portunus-server --log-format compact serve
# Or via TOML:
log_format = "compact"Mix compact format with RUST_LOG env-filter and you get readable
local output without sacrificing the JSON shipping path on production
hosts.