# Structured Logging (https://portunus.bybee.dev/en/docs/observability/logging)



Both binaries log via `tracing` + `tracing-subscriber`. The default
output is **JSON, one line per event**, suitable for shipping to any
log collector.

## Format [#format]

```toml
# server.toml
log_format = "json"     # default
# log_format = "compact"  # human-readable for local dev
```

A typical line:

```json
{
  "timestamp": "2026-05-06T12:01:30Z",
  "level": "INFO",
  "event": "rule.activated",
  "client_name": "edge-01",
  "rule_id": 1,
  "request_id": "01HM…"
}
```

## Filtering [#filtering]

Use the standard `tracing-subscriber` env-filter:

```sh
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-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 [#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 [#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 [#local-development-tips]

```sh
# 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.
