Portunus
Observability

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 dev

A 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-client

request_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.

EventSeverityCause
tls.cert_regeneratedWARNOperator certificate regenerated (e.g. expired / SAN change)
tls.operator_cert_in_useINFOExisting operator certificate reused on startup
auth.failureWARNClient bearer rejected (revoked / unknown / signature mismatch)
operator.denyWARNRBAC rejection on the operator API
client.disconnectedWARN/INFOClient lost the gRPC stream (transient or revoke)
client.transport_errorWARNgRPC transport-level error on a client stream
store.pool_exhaustedWARNSQLite connection pool exhausted under load
rule.failedWARNRule 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.

On this page