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:

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:

  1. The log line, verbatim — it carries the code, the request identifier and the context.
  2. 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.

Generated from the documentation that ships with searchcode enterprise. Questions: ben@boyter.org