---
title: Troubleshooting
path: troubleshooting
status: published
---

# Troubleshooting

This page maps common symptoms to their cause and fix. Each subsection is one
symptom you can recognize from a log line, an exception, or a counter. Every
behaviour here is a deliberate design choice — ScaiLog fails safe (never leaking
PI, never silently dropping) rather than failing convenient, so most of these are
signals telling you what to fix.

## Logs appear on stderr as JSON instead of reaching the server

**Symptom.** Your service prints ScaiLog entries as newline-delimited JSON to
stderr (landing in container or journald logs), and PI field values show up as
`{"type": "...", "redacted": true}` instead of their real value.

**Cause.** The SDK could not reach the local agent socket, or its in-process
queue was full. Rather than drop the entry silently, the SDK falls back to stderr
in the same NDJSON shape — and it **redacts PI values first**, keeping the field
name and PI type so the drop is auditable but no plaintext leaks into stderr.

**Fix.** Make sure the agent (or a standalone server's embedded agent) is running
and that `SCAILOG_SOCKET` points at the socket it is listening on (default
`/run/scailog/agent.sock`). Confirm file permissions let your service connect.
Once the socket is reachable, entries flow over it again. A rising
`dropped_to_stderr` count is your signal that the agent is unavailable or your
emit rate is exceeding the queue.

## `SUBJECT_REQUIRED`

**Symptom.** An emit call raises `ValueError: SUBJECT_REQUIRED: entry has PI
markers but no subject=` (Python), the entry is rejected with a `SUBJECT_REQUIRED`
diagnostic (TS/Go/.NET), or an ingested batch entry is nacked with
`SUBJECT_REQUIRED`.

**Cause.** The entry carries one or more `pi()` fields but no `subject`. Any PI
requires a data subject to attach it to — that's what makes cryptographic erasure
possible. The rule is enforced in the SDK before the entry leaves the process,
and again at the agent and server.

**Fix.** Pass the data-subject id on the emit call (`subject=...`), or set it on
the ambient context (e.g. `withContext`/`WithSubject`/`LogContext.Push`) so it
propagates automatically. If the field isn't actually about a natural person,
don't mark it `pi()`.

## `invalid call_site_id`

**Symptom.** An emit call raises
`ValueError: invalid call_site_id: '...' (pattern ^[A-Z0-9]+(-[A-Z0-9]+){2,}$)`,
or `scailog-ci check` fails the build on a call site.

**Cause.** Every entry needs a developer-assigned `call_site_id`, and it must
match `^[A-Z0-9]+(-[A-Z0-9]+){2,}$` — at least three uppercase-alphanumeric
segments joined by hyphens, e.g. `SCAI-CHECKOUT-PAID-001`. Call-site provenance
is first-class in ScaiLog (GDPR remediation is "fix one call site"), so the id
can't be blank, lowercase, or too few segments. The `AUTO-` and `SCAILOG-`
prefixes are reserved for the system.

**Fix.** Give the call site a constant, unique, well-formed id. Run `scailog-ci
check` in CI to catch bad or interpolated ids before they ship — it also enforces
that the id is a constant (not built from a variable) and that `pi()` is
accompanied by a subject.

## Entries turn into `[PANIC_DROPPED:...]` (SCAILOG-AGENT-PANIC-DROP-001)

**Symptom.** A PI field is stored as `[PANIC_DROPPED:<pi_type>]` in the entry
metadata, the agent's `panic_drops` counter rises, and the agent emits a warning
under call site `SCAILOG-AGENT-PANIC-DROP-001`: "refused to cache plaintext PI:
server unreachable and no DEK available".

**Cause.** The agent needs a per-subject DEK to encrypt PI, and it couldn't get
one — the server was unreachable (so enrollment or DEK fetch failed), or the
subject was erased (the server returns HTTP 410, the key is gone). Rather than
write plaintext PI to its local cache, the agent enters panic mode and drops the
value, loudly.

**Fix.** Restore connectivity to the server and confirm the agent's API key is
valid, then new entries encrypt and ship normally. If the drops are for an
**erased** subject, that is correct behaviour — the key was destroyed by design
and cannot come back. Never reach for `--fail-open` in production to make the
drops stop: it caches plaintext PI and exists only for dev.

## A console script prints `pip install 'scailog[...]'` and exits

**Symptom.** Running `scailog`, `scailog-server`, `scailog-agent`, or
`scailog-audit` exits immediately with, e.g.:

```text
scailog-server needs the 'server' extra, which isn't installed.
    pip install 'scailog[server]'
(missing dependency: fastapi)
```

**Cause.** The base install is the pure-stdlib SDK, which carries none of the
server/agent/CLI/audit dependencies. The console scripts are wrapped so a missing
extra becomes a one-line install hint instead of a raw `ModuleNotFoundError`
traceback.

**Fix.** Install the extra named in the hint, e.g. `pip install 'scailog[server]'`
(or `agent`, `cli`, `audit`, or `all`). Note `scailog-ci` is the exception — it is
zero-dependency and runs on the base install. (If the missing dependency is a
`scailog.*` submodule rather than a third-party package, that's a genuine bug and
is re-raised untouched — it won't be masked by an install hint.)

## `RESERVED_FIELD` rejection

**Symptom.** An emit call raises `ValueError: reserved field name: '...'`
(Python) or the entry is rejected/nacked with `RESERVED_FIELD`.

**Cause.** A field name collides with a reserved top-level entry key. These names
are reserved: `event_id`, `tenant`, `service`, `env`, `site`, `level`, `ts`,
`msg`, `fields`, `pi`, `subject`, `trace_id`, `request_id`. Allowing them as
user fields would let a field shadow a protocol key.

**Fix.** Rename the field to something outside that set (e.g. use `user_service`
instead of `service`). The check runs on both plain fields and `pi()` fields.

## PI never gets encrypted — it's kept or dropped instead

**Symptom.** A field you marked `pi()` shows up in plaintext `fields` (or is
gone) rather than encrypted, and the entry metadata records an action of `keep`,
`redact`, or `drop`.

**Cause.** The policy engine decides the action per PI type. Not every type
defaults to `encrypt`: `pseudonymous` defaults to `keep`, and `credentials` and
`special_category` default to `drop` (credentials and special-category data are
dropped, not stored encrypted). Your policy may also override the default for a
service or environment.

**Fix.** Check the effective policy for the service/env/PI-type combination
(`scailog explain` / the policies view). If a type should be encrypted, set an
`encrypt` action for it in your policy. If you marked the wrong PI type, fix the
marker at the call site.
