Monitoring

searchcode enterprise publishes Prometheus metrics on a listener of its own, ships a Grafana dashboard and a set of alert rules built against those metrics, answers health probes on the API listener, and can send traces to your own OTLP collector. None of it needs an agent, a sidecar or a vendor account.

The metrics listener

/metrics is served on a third port, separate from the API and MCP listener (server.listen) and the UI listener (server.uiListen).

Setting Default Reload
observability.metrics.enabled true restart
observability.metrics.listen 0.0.0.0:9090 restart

It is a separate port rather than a route for one reason: repository names appear in metric labels, and they are often sensitive. A NetworkPolicy or firewall rule can say "this port, from the Prometheus namespace", but it cannot say "this path". Give Prometheus the port and nobody else.

The endpoint is unauthenticated, so restrict who can reach it at the network. Validation refuses an address that one of the other two listeners already uses, and the bound address is announced at startup on stderr as searchcode metrics listening on http://…/metrics. See Ports, names and what sits in front of each listener for where each listener should be reachable from.

Turning enabled off is a decision about scraping, not about exposure: the listener is already separate.

Per-repository series

indexing.metricsPerRepo controls whether the per-repository metrics carry one series per repository. Each repository costs about two series and a few hundred bytes of scrape, so set it to false above roughly 2,000 repositories.

When it is false the repo label is kept and every series collapses onto repo="all". The label set never changes with the setting, so a dashboard query that groups by repo keeps working either way. Lag collapses to the maximum (the stalest repository is the question); counts are summed.

The estate gauges are sampled every ten seconds off the request path, so a scrape reads a snapshot and never waits on a clone. /api/v1/status reports the age of that snapshot.

What is exported

The list below is the whole set. Nothing else is published: there are no go_* or process_* series.

Licence and metering

Metric Type Labels Meaning
searchcode_license_expiry_timestamp_seconds gauge — Unix time the licence expires. Zero when there is no expiry.
searchcode_licensed_users_active gauge — Distinct subjects seen in the last thirty days: the user meter's count.
searchcode_licensed_users_limit gauge — The licensed user ceiling. Zero means unlimited.
searchcode_user_limit_refusals_total counter — Sign-ins refused at the user limit. Somebody was turned away.
searchcode_licensed_repositories gauge — Repositories counted against the repository meter. Directory sources count as units, not mounts.
searchcode_licensed_repositories_limit gauge — The licensed repository ceiling. Zero means unlimited. This meter warns and never blocks.

See Licensing for what each meter does.

Freshness and ingestion

Metric Type Labels Meaning
searchcode_repo_index_lag_seconds gauge repo Seconds since the repository last ingested successfully. The freshness metric; alert on its p99.
searchcode_repo_index_failures_total counter repo, reason Failed clones and fetches.
searchcode_files_skipped gauge repo, reason Skip events from the most recent scan of each repository.
searchcode_indexing_duration_seconds histogram phase Seconds spent in one analysis pass, by phase. Not labelled by repository, deliberately.
searchcode_ingestion_paused gauge reason 1 while ingestion is paused: window, disk_full or license_expired. Serving is never affected.

searchcode_files_skipped is a gauge, not a file count, despite the name. For timeout, too_large, minified, binary, excluded and excluded_default it counts files. For gitignore, ignore_file and other it counts skip events, and a whole skipped directory is one event. Each scan replaces a repository's counts, so the value falls when a repository shrinks or an exclusion is narrowed. Alert on the value, never on increase(). A non-zero timeout or minified count is how you find out files are missing from search.

excluded is something you configured; excluded_default is the built-in exclusion set (dependency, build-output and cache directories). They have different remedies, which is why they are counted apart.

Capacity

Metric Type Labels Meaning
searchcode_disk_usage_ratio gauge path Fraction of the dataDir filesystem in use, 0.0 to 1.0.
searchcode_disk_available_bytes gauge path Bytes available on the dataDir filesystem.

Disk is sampled every thirty seconds, never per write. A check on every write puts a system call on the ingestion path, and a slow or failing filesystem is exactly where it would cost the most. /api/v1/status, /metrics and the admin page all read the same cached sample, so they cannot disagree. On a platform with no way to measure the filesystem the series are omitted rather than reported as zero, because a disk reported 0% full is a false all-clear.

Access and credentials

Metric Type Labels Meaning
searchcode_authz_denials_total counter surface Authorisation denials. A spike is an attack or a policy edit somebody regrets.
searchcode_auth_mode_open gauge — 1 when auth.mode is open: nobody is authenticated and every repository is readable by anyone who reaches the port.
searchcode_credential_anomaly_total counter — Times an agent credential exceeded api.apiKeys.rateLimit.anomalyMultiplier times its own rolling baseline. Each is also an apikey.anomaly audit record naming the key.
searchcode_credential_refused_total counter — Requests refused by the per-credential ceiling api.apiKeys.rateLimit.requestsPerMinute. The credential was valid and sending too fast.
searchcode_credential_last_used_dropped_total counter — Last-used observations discarded because the write buffer was full. Deliberate, but a rising rate means the dormant-key report reads older timestamps.

Code-host connections

Metric Type Labels Meaning
searchcode_connection_repositories gauge connection Repositories a connection currently contributes.
searchcode_connection_last_success_timestamp_seconds gauge connection Unix time of the last sync that finished with no gaps. Zero if never, so guard an age alert with > 0.
searchcode_connection_state gauge connection, phase 1 on the current phase: never_synced, syncing, ok, partial, paused or failed.
searchcode_connection_syncs_total counter connection, result Syncs by outcome: ok, partial, paused, failed.
searchcode_connection_repositories_retired gauge connection Repositories retired and not yet reclaimed: out of search, still on disk for the retention window.

partial is the phase worth alerting on. A failure is loud everywhere else; a connection quietly missing one organisation of three is not.

Search index

Metric Type Labels Meaning
searchcode_search_index_repositories gauge state Repositories by index state: indexed, below_minimum, building, not_applicable, failed. The five sum to the repository count.
searchcode_search_index_rebuilding gauge — Repositories serving an index while its replacement builds. A subset of indexed.
searchcode_search_index_documents gauge — Documents across the indexes being served.
searchcode_search_index_bytes gauge — Resident bytes of search index. This is what governs an estate's memory.
searchcode_search_index_builds_total counter outcome Build attempts: built, skipped, failed. A rising failed is search falling back to the slower path.
searchcode_search_index_build_seconds_total counter — Total seconds in build attempts. Divide by the attempt count for the mean build cost.
searchcode_search_index_warming gauge — 1 while a build is queued or running. Alert if it stays 1 longer than a warm should take.

A repository without an index is still searched, correctly, by reading its files. The index makes search faster; it never decides what matches.

Analysis

Metric Type Labels Meaning
searchcode_analysis_repositories gauge state Repositories by deep-analysis state: analysed or pending. The same reading as analysis.coverage on /api/v1/status.
searchcode_analysis_jobs gauge pool, state Jobs by pool (inventory, deep) and state (active, queued). Queued falling to zero is a re-analysis finishing.
searchcode_analysis_workers gauge pool Configured concurrency of each pool. Active equal to this is a saturated pool.

HTTP and usage

Metric Type Labels Meaning
searchcode_http_requests_total counter listener, route, method, status Requests by listener, route pattern, method and status class.
searchcode_http_request_duration_seconds histogram listener, route, method Seconds to serve one request, measured outside authentication so a slow proxy or password hash is visible.
searchcode_searches_total counter surface, result Searches by surface and outcome. A count, not a query: it keeps working with audit.logSearchQueries: false.
searchcode_zero_result_searches_total counter — Searches that matched nothing and were not denied. Each is a coverage gap or a ranking gap.
searchcode_agent_instruction_coverage_ratio gauge — Fraction of indexed repositories whose agent-instruction file mentions searchcode.
searchcode_mcp_calls_total counter tool, result MCP tool calls by tool and outcome.

route is the registered route pattern, never the request path. Paths carry repository names and file paths, so a per-path label would be cardinality driven by your estate.

Advisory data

Metric Type Labels Meaning
searchcode_advisory_database_age_days gauge — Age in days of the offline advisory data. Zero when none is loaded.
searchcode_advisory_database_stale gauge — 1 when the advisory data is absent, is the small compiled-in seed, or is a side-loaded bundle more than 30 days old.

An instance that has side-loaded no advisory bundle reads 1 here, which is correct: the compiled-in seed is a handful of entries and is not coverage.

The shipped dashboard and alerts

Two files are shipped with every release and built against the list above:

The Helm chart carries the same two files and renders them when asked: serviceMonitor.enabled, prometheusRule.enabled and grafanaDashboard.enabled (a ConfigMap labelled grafana_dashboard for the Grafana sidecar). All three are off by default. See Install with Helm.

The alerts

Alert Severity Fires when
SearchcodeLicenceExpiringSoon warning The licence expires in under 30 days.
SearchcodeLicenceExpired critical The licence has expired. The instance keeps serving; indexing pauses once grace ends.
SearchcodeUserLimitRefusals critical A sign-in was refused at the user limit in the last hour.
SearchcodeRepositoryLimitExceeded warning Over the licensed repository count for a day. Nothing is blocked.
SearchcodeIndexLagHigh warning p99 index lag is over six hours for 30 minutes.
SearchcodeRepoIndexFailing warning One repository failed to ingest more than three times in an hour.
SearchcodeFilesSkipped warning More than 100 files are skipped for timeout, too_large or minified.
SearchcodeIngestionPaused warning Ingestion has been paused for an hour. window is usually your schedule; disk_full and license_expired are not.
SearchcodeDiskHigh warning dataDir is over 80% full for 30 minutes.
SearchcodeDiskCritical critical dataDir is over 95% full. Ingestion is about to pause, or has.
SearchcodeAuthModeOpen warning auth.mode is open for an hour. Usually a config meant to be changed before going live.
SearchcodeAuthzDenialSpike warning More than one denial a second on a surface for 15 minutes.
SearchcodeAgentInstructionCoverageLow info Under half of indexed repositories tell their agents about searchcode, for a week.
SearchcodeZeroResultSearchesHigh info Over 40% of searches return nothing, for six hours.

The two licence alerts guard on > 0: a community instance and a licence with no expiry both report zero, and without the guard both would page as long expired.

Every alert about ingestion says the same thing in its description, and it is the property to remember when one fires: serving is unaffected. A failed fetch, a paused scheduler or a full disk stops the estate growing, never answering.

Health endpoints

All of these are on the API listener (server.listen) and need no credential, because a probe runs before any credential exists.

Endpoint Answers Use it for
/livez 200 ok whenever the process can answer. Touches no storage. Liveness.
/readyz 200 once the server is accepting traffic; not ready while starting or draining. Readiness.
/startupz 200 once the process has booted, and never flips back. Startup probe.
/version The build identity. Support tickets.
/api/status This process: build, readiness, start time, auth mode, and which configuration file it loaded. Nothing about the estate. Quick checks and searchcode doctor.

Two properties look like oversights and are not:

The estate itself — per-repository state, analysis coverage, index coverage, the licence and the meters — is on /api/v1/status, which authenticates.

Tracing

OTLP tracing is off by default. It is one of the three things that can make the process open an outbound connection, and it points at your own collector, never at a vendor.

Setting Default Reload
observability.tracing.enabled false restart
observability.tracing.endpoint — restart
observability.tracing.sampleRate 1.0 reloads

Traces are sent as OTLP over HTTP with a JSON payload, posted to /v1/traces on the endpoint (appended when the endpoint names only a host). Every span carries the same request identifier as the audit record and the log lines for that request, so one request can be followed across all three.

enabled and endpoint are restart-only on purpose: opening an outbound connection is not something an edited file should do a second later without the deployment noticing. sampleRate adjusts how much of an already-permitted stream is sent, so it reloads. A disabled tracer installs nothing at all; there is no half-armed state where a collector is dialled to find out it was not wanted.

Trying it locally

deploy/observability/ is a separate Docker Compose project that brings up an instance with Prometheus, Grafana and Jaeger already wired to it:

cd deploy/observability
docker compose up -d --build

Grafana is on http://localhost:13000 with the shipped dashboard loaded, Prometheus on http://localhost:19091 with the shipped alert rules, Jaeger on http://localhost:16686, and the instance's UI on http://localhost:18081. The shipped dashboard and rules are mounted rather than copied, so what renders is exactly what you get. It uses its own ports so it does not collide with an instance already running on the usual ones. It is for looking, not for production.

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