Log codes
Every log line this product emits carries an eight-character unique code that identifies the single line of source that wrote it.
{"ts":"2026-08-06T09:14:22.118Z","level":"WARN","msg":"clone failed",
"component":"ingest","repository":"acme/billing","uniqueCode":"cl01d4e5",
"error":"x509: certificate signed by unknown authority"}
cl01d4e5 is not a category, a severity or an error class. It is that line.
Alert on codes, not on message substrings
The code is the contract; the message is human text and will be reworded.
# good
uniqueCode = "cl01d4e5"
# fragile
msg =~ "clone failed"
This is the same thing Oracle has with ORA-, Postgres with SQLSTATE and
Windows with event IDs, for the same reason: an operator needs something stable
to build automation on, and English prose is not it.
Two properties make the code worth building on:
- It is unique across the whole product. Two lines cannot share one, and the build fails if they do.
- It is never reused. A retired code is recorded as retired and cannot come back meaning something else — otherwise historical log analysis silently misattributes.
Codes appear in logs, not in error responses
No unique code ever appears in an HTTP or MCP error body, and this is deliberate rather than an omission.
On authentication paths a code is an enumeration oracle: distinct codes for "no such user", "wrong password" and "stale groups" hand an attacker exactly the distinction you spent the design avoiding. And you do not need it in the response — this is a self-hosted product, so you already have the logs, and the log line carries the code plus the trace identifier plus the context that never belonged in a response body.
What an API error carries instead:
{"error": "no repository named acme/billing is available to this principal",
"error_code": "repo_not_found",
"suggestion": "list what you can reach at /api/v1/repos"}
error_code is the semantic class — stable, documented, and what a client should
branch on. The response also carries an X-Request-Id header, and that is
the join: the log lines for the same request carry the same identifier, and one
of them carries the unique code.
The one exception is the authenticated web UI error page, whose viewer genuinely cannot reach the logs.
From a request id to a trace
A request id is not a trace id. With observability.tracing.enabled on, a
sampled request also carries an X-Trace-Id response header and a
trace_id_w3c field beside trace_id on its log lines — that is the
thirty-two-character value a collector's exact-trace-id lookup takes, so paste it
rather than the request id:
curl -s http://your-collector:16686/api/traces/$(
curl -sI http://searchcode:8080/api/v1/repos | tr -d '\r' |
awk 'tolower($1)=="x-trace-id:"{print $2}')
The two differ because OTLP requires a sixteen-byte trace id and the request
identifier is eight, so the exporter widens it; X-Request-Id alone returns
404 trace not found. A log line or a response with no trace_id_w3c was
never traced — tracing off, or the request sampled out — so there is nothing to
look up, and that absence is the answer rather than a missing field.
Resolving a code
codes.json ships with every release and maps every code this build can emit to
its file, line, level and message:
{
"version": "v0.1.0",
"codes": {
"cl01d4e5": [
{"path": "pkg/enterprise/ingest/scheduler.go", "line": 412,
"level": "warn", "message": "clone failed"}
]
}
}
jq '.codes["cl01d4e5"]' codes.json
Keep the codes.json for the build you are running. That is the whole point
of the file: a support ticket from a customer on v1.2.3 is unresolvable once the
line has moved in v1.4.0 — which is exactly when the ticket arrives. It is
per-build, it is small, and it is the one release artefact worth archiving
alongside the binary.
It is in the tarball at the top level, and in the release assets. To generate it from a source checkout:
make codes-json
The support template
When something goes wrong, two things resolve almost everything:
- The log line, verbatim — it carries the code, the request identifier and the context.
searchcode doctor bundle— see searchcode doctor.
That is strictly more useful than an error message, and it is available to every customer because every customer is self-hosting.
The log stream itself
stdout is pure JSON, one object per line, and that is the schema to write SIEM rules against:
| Field | |
|---|---|
ts |
RFC 3339 with milliseconds |
level |
DEBUG / INFO / WARN / ERROR |
msg |
Human text. Do not match on it. |
component |
Which part of the product |
uniqueCode |
The emission site |
Anything else on the line is context for that particular code.
stderr carries the human lines: the startup banner naming the addresses
bound, and — on a startup failure — one readable FATAL: line naming the path
and the probable cause, printed before any structured output. That ordering is
deliberate: in a merged view like docker compose up, the actionable sentence is
the first thing you see.
logging.level is reloadable — a level change takes effect without a restart.
Audit records are a different stream
Application logs are ours; audit records are your compliance team's.
Different destination (audit.output), different retention
(audit.retentionDays, default 400), different stability guarantee — additive
changes only, with a version field that makes that promise checkable.
An audit record carries three identifiers that answer three different questions and must not be collapsed:
| Field | Answers |
|---|---|
action |
What semantically happened. A small, documented, closed set — the contract with compliance. |
uniqueCode |
Which line decided it. Thousands of values — the contract with support. |
trace_id |
Which request. Ephemeral. |
auth.proxy_rejected, for example, is emitted from five different places
carrying five different codes: an untrusted peer, a wrong shared secret and a
duplicated identity header need different answers, and the action alone cannot
tell them apart.