Files
LocalAI/docs/content/features/diagnostics.md
T
mudler-agentandEttore Di Giacinto 0e0e91f95c feat(diagnostics): add opt-in profiling (#12610)
* feat(diagnostics): add private opt-in profiler

Keep profiling on a dedicated loopback listener rather than the public API.
Validate settings and bind synchronously so startup reports configuration
and socket failures. Own sampling and shutdown for the run process.

Cover private handlers, blocked cmdline access, sampling cleanup, and
serving failures with focused Ginkgo tests.

Assisted-by: nib:gpt-6-astra

* feat(diagnostics): add sanitized phase timing

Carry generated request and reload identities through private contexts.
Keep timing disabled without allocations and emit only validated fields
at Info level when enabled.

Cover identity isolation, nested durations, inheritance, and sanitized
logging with focused race-tested Ginkgo specs.

Assisted-by: nib:gpt-6-astra

* fix(diagnostics): retain request observations

Reuse an existing request observation across recorders to avoid splitting
correlation. Keep the original context, identity, and sink without new
allocations. Preserve nil-recorder behavior and fresh reload identities.

Verify the regression fails before the fix and passes with the full
Diagnostics race suite afterward.

Assisted-by: nib:gpt-6-astra

* feat(cli): own opt-in diagnostics lifecycle

Expose profiling and phase timing flags without enabling them for debug.
Bind the private profiler before application initialization and propagate
serving failures through the run lifecycle.

Bound signal cleanup and preserve preload-only backend startup. Cover
flag parsing, preload isolation, cancellation, and once-only shutdown.
Document initialization and shutdown cancellation limitations.

Assisted-by: nib:gpt-6-astra

* feat(config): measure filter and reload phases

Separate request lock waits from filtering and held work. Aggregate reload
filesystem and parsing durations without logging inside the config lock.

Preserve caller option execution under the lock after metadata collection.
Extract only the internal diagnostics option before waiting for the lock.
Keep direct callers uninstrumented unless they supply diagnostics options.

Verify focused Diagnostics examples with the race detector, including
option ordering, gated contention, errors, panic propagation, and parity.

Assisted-by: nib:gpt-6-astra

* fix(config): separate early reload observation

Opaque callbacks cannot select timing before their execution point.
Carry the startup recorder explicitly through loader construction.
Apply late options once under the lock and honor composed last writes.
Document early and late phase boundaries and observation identities.

Report malformed gallery syntax as an internal parse failure without
changing strict or non-strict loader returns.

Assisted-by: nib:gpt-6-astra

* fix(config): preserve per-model option callbacks

The resolved snapshot suppresses existing callbacks during model defaults,
even when diagnostics are disabled. Pass original options through the
existing defaults pathway without adding diagnostics-driven replay.

Test stateful options with two models in enabled and disabled modes.
Preserve three calls and thread defaults of two and three.

Assisted-by: nib:gpt-6-astra

* feat(model): observe discovery phases

Carry request diagnostics through config and loose-file discovery to
separate directory enumeration from in-memory filtering and fallback.
Keep existing APIs as background-context wrappers and preserve discovery
policies, ordering, duplicates, errors, and filesystem operation counts.

Add focused parity, privacy, phase, and disabled-allocation tests.
The focused diagnostics race gate passes for model and galleryop.

Assisted-by: nib:gpt-6-astra

* feat(http): observe extraction phases

Generate diagnostic identity before request discovery, independently of
client correlation headers. End each extraction segment before inference
so downstream latency does not inflate discovery measurements.

Carry observations through discovery helpers and rebuilt request contexts
without changing cancellation or model selection. Cover disabled parity,
error outcomes, privacy, concurrent identity isolation and route contexts.

Assisted-by: nib:gpt-6-astra

* fix(http): retain diagnostics across failover

Failover restores its entry request before retrying. Initialize the
observation before that capture so direct extraction retains one ID.
Keep extraction spans per attempt and end them before downstream work.

Exercise two real targets without default middleware, including replay,
shared identity, phase boundaries, and timing-off response parity.

Assisted-by: nib:gpt-6-astra

* feat(model): observe initialization and routing

Mark loader entry before cache and admission checks. Measure only the
ModelRouter callback, without attributing transport or worker latency.

Test inherited identity and sanitized events with fake backends. Exercise
the actual Echo router to keep profiles private across debug and timing
options. Cover capture shutdown, forced close, and lifecycle failures.

Assisted-by: nib:gpt-6-astra

* docs(diagnostics): guide bounded investigations

Document private profiling, sampling ownership, and phase boundaries so
operators can interpret captures without assuming request attribution.
Add Ginkgo drift checks for flags, defaults, phases, links, and capture
warnings. These checks do not certify the operator prose.

The focused documentation race test passes after four expected failures.
The consolidated Diagnostics race gate passes across all eight packages.
This gate does not validate the full suite, auth, or production UI.

Assisted-by: nib:gpt-6-astra

* refactor(cli): restore inline run lifecycle

Remove diagnostic startup hooks so application startup stays readable
in Run. Keep the profiler as an optional sidecar with bounded cleanup
and an error watcher, without a general lifecycle runner.

Replace hook simulations with real CLI startup checks and component
capture tests. Document that application cleanup can exceed the
private listener budget.

The eight-package Diagnostics race gate passes. A cleanup mutation
fails the real CLI listener-release regression as expected.

Assisted-by: nib:gpt-6-astra

* fix(cli): retain concurrent startup errors

Cancellation must not hide a public API bind failure. Join the public
serve result, cancellation cause, and HTTP shutdown errors inline.
Ignore ErrServerClosed only after intentional shutdown.

Exercise real startup with an occupied API port and a failed private
listener. Isolate the Linux socket fixture in a subprocess and wait
for cancellation acknowledgment without timing sleeps.

Assisted-by: nib:gpt-6-astra

* refactor(http): make request context primary

Move context preparation into the request extractor. Diagnostics remain
an optional addition, not the entry point for request processing.
Preserve context initialization before failover captures the request.

Assisted-by: nib:gpt-6-astra

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-10-10 20:27:28 +02:00

13 KiB
Raw Blame History

+++ disableToc = false title = "Profiling and request diagnostics" weight = 30 +++

LocalAI provides two independently opt-in diagnostics: a private Go profiling listener and structured phase timing events. Both default to disabled. Debug logging alone enables neither feature.

Scope and configuration

These options apply to local-ai run, including a distributed frontend. They do not enable diagnostics in separately launched workers or provide backend-native llama.cpp profiling. Diagnostics has no MCP, admin, or public API surface and no UI controls. State is per-instance, not shared between replicas. Profiles describe the frontend Go process, not the whole cluster.

Parameter Default Description Environment Variable
--pprof false Enable the private loopback profiling listener $LOCALAI_PPROF
--pprof-address 127.0.0.1:6060 Numeric loopback IP with an explicit port $LOCALAI_PPROF_ADDRESS
--pprof-mutex-profile-fraction 0 Nonnegative mutex sampling fraction; zero disables sampling $LOCALAI_PPROF_MUTEX_PROFILE_FRACTION
--pprof-block-profile-rate 0 Nonnegative block sampling rate in nanoseconds; zero disables sampling $LOCALAI_PPROF_BLOCK_PROFILE_RATE
--request-phase-timing false Emit sanitized phase events at Info, independently of profiling $LOCALAI_REQUEST_PHASE_TIMING

The address accepts numeric IPv4 loopback or bracketed IPv6 loopback, such as 127.0.0.1:6060 or [::1]:6060. Ports must be decimal values from 1–65535. Validation rejects hostnames, including localhost, wildcard addresses, empty hosts, LAN addresses, public addresses, and Unix sockets. Validation applies even when profiling is disabled. Negative sampling settings fail validation; nonzero sampling settings require --pprof. --preload-backend-only validates the options but does not start profiling or sampling.

For mutex sampling, a value of N records approximately one in N contention events; 1 records every event. Block sampling targets one blocking event per configured number of nanoseconds spent blocked; 1 records every blocking event. These settings are not percentages or capture durations. Sampling adds process-wide overhead, especially at 1.

One diagnostics server owns process-global runtime sampling in each run process. It applies both configured rates once after binding successfully and resets both rates to zero on shutdown or fatal serving failure. It does not restore previous rates; Go provides no getter for the previous block rate. Do not combine competing sampling owners in the same process. There is no dynamic toggle, continuous capture, or automatic profile storage. Configuration changes require a separately approved restart or rollout.

The listener binds before normal request serving. Validation or bind errors fail startup; subsequent fatal serving errors propagate through the run lifecycle. The private listener gets five seconds to drain before profiling connections are force-closed, which can interrupt an active capture. Its cleanup runs on startup failure, normal return, and through the existing signal handler (which exits without running deferred cleanup). A fatal profiler serving error cancels the run context and is returned after application cleanup. Application initialization or shutdown work that ignores cancellation can delay that return; the private-listener budget is not a deadline for the entire application. With profiling disabled, diagnostics opens no socket, starts no monitoring goroutine, and changes no runtime sampling settings. Disabled phase timing allocates no diagnostic identities or events and adds no diagnostic filesystem operations.

Access and data sensitivity

{{% notice warning %}} The profiling listener is unauthenticated. Loopback reachability is its authorization boundary, not the public API's authentication policy. Other processes in the same network namespace can access it, including containers sharing a pod's namespace. Profiles can contain sensitive application data. Heap data and goroutine output can expose secrets. Protect captures, review them before sharing, and delete them when the investigation ends. {{% /notice %}}

The separate listener uses a private mux, not the public API router or the default Go mux. It provides the profiling index, CPU, trace, symbol, and named runtime profiles, including heap, goroutine, mutex, and block. /debug/pprof/cmdline is unavailable, including query and subtree variants, because command-line arguments can contain credentials. The standard index can still mention that endpoint.

Timing events omit credentials, bearer candidates, model names, prompts, bodies, raw headers, filenames, paths, and raw errors. This sanitation applies only to diagnostic timing payloads. It does not redact unrelated logs or profiles. Events use the constant message diagnostic_phase at Info. Operator-selected warn/error filtering suppresses these events, even when phase timing is enabled.

Bounded Kubernetes investigation

A rollout or build configuration change is a separate, explicit, owner-approved action. Select one frontend and a defined observation window. Approve profiling, timing, and any nonzero sampling settings before the window. Do not change replicas, clocks, scheduling, or storage mounts to force a result. The examples below capture data only; they do not mutate a live deployment.

Your cluster runtime must support port-forwarding to the pod's loopback listener. Do not add a Service, ingress, or NodePort for profiling. Forward directly to the selected frontend pod, not a load-balanced Service. The local port must be free. Keep kubectl's default local-only binding; do not add a wildcard --address.

  1. Start the direct pod port-forward in a separate terminal, replacing both placeholders:

    kubectl -n <namespace> port-forward pod/<selected-frontend-pod> 6060:6060
    
  2. Protect newly created files in your capture terminal:

    umask 077
    
  3. Capture ten seconds of CPU activity during the approved window:

    curl --fail --output cpu.pprof 'http://127.0.0.1:6060/debug/pprof/profile?seconds=10'
    go tool pprof cpu.pprof
    

CPU profiles show execution, not time blocked on I/O or locks. Capture naturally slow requests and matched control requests against the same frontend. Record the observation window and generated diagnostic IDs alongside protected captures. Profiles are process-wide; they do not automatically attribute samples to a request ID.

Mutex and block profiles require their respective nonzero sampling settings before capture. The following ten-second delta captures bound collection time, not the duration of process-global sampling. Sequential commands cover different windows; compare them accordingly.

curl --fail --max-time 20 --output mutex.pprof 'http://127.0.0.1:6060/debug/pprof/mutex?seconds=10'
curl --fail --max-time 20 --output block.pprof 'http://127.0.0.1:6060/debug/pprof/block?seconds=10'
go tool pprof mutex.pprof
go tool pprof block.pprof

Mutex profiles locate contention attributed to lock holders; block profiles locate sampled blocking operations. Neither substitutes for the request-owned lock and filesystem timings below. A goroutine snapshot can show I/O or lock waits that CPU profiles miss. It describes one instant, not a measured duration. Its text can be large and sensitive; the client timeout bounds waiting, not file size.

curl --fail --max-time 10 --output goroutines.txt 'http://127.0.0.1:6060/debug/pprof/goroutine?debug=2'

Stop the port-forward after capture. Disable diagnostics and sampling afterward through the separately approved rollout process. Protect, review, and delete the capture files under your data-handling policy. If latency does not reproduce, report non-reproduction. Do not infer a predetermined filesystem, lock, or routing root cause.

Event identity and phase boundaries

Each event contains id, kind, phase, state, outcome, elapsed, and count. The server generates diagnostic UUIDs; it does not trust or reuse client X-Correlation-ID headers as diagnostic identities. Requests use kind request; config reload operations use kind config_reload with fresh operation IDs. Request identity survives the instrumented internal context reconstructions. Background callers remain unobserved unless explicitly enabled.

States are start, end, and mark; outcomes are ok, error, canceled, and skipped. Durations use monotonic elapsed time, not cross-host clock subtraction. Counts describe aggregate work, not filenames or model labels. Lock-protected helpers emit buffered aggregates after unlocking, so log order alone does not establish execution order.

Nested timings overlap and are not additive. Repeated extraction segments share one request ID; they are not duplicate whole-request measurements. Extraction ends before downstream inference. It is not socket arrival or full request latency. The existing trace start follows global authentication and body reading; it is not socket arrival either.

Phase Measured boundary
extraction Each extractor-owned entry/exit segment, including repeated extraction or retries, before downstream inference.
bearer_lookup Existing bearer-derived model existence lookup, without recording the candidate.
default_listing Existing default-model candidate listing within extraction.
body_lookup Existing body-derived model existence lookup, without recording body data.
config_load_defaults Existing config loading and default application within extraction.
alias_resolution Existing model alias resolution within extraction.
config_lock_wait Time waiting to acquire the config loader mutex for filtering.
config_lock_hold Time holding that mutex for the config listing/filter operation. Includes nested filter work.
config_filter The in-memory config filter pass under that lock, not filesystem latency.
fs_enumeration The existing os.ReadDir call for loose model discovery only.
loose_filter Existing filename and in-memory loose-file filtering passes, not directory enumeration.
existence_fallback The existing ExistsInModelPath fallback when discovery requires it. No extra diagnostic filesystem calls.
model_init Entry mark immediately after ModelLoader.Load constructs options, before admission, cache, or load logic. Not backend-native execution.
model_router_callback Only the existing ModelRouter callback invocation. Not transport publish or worker receive latency.
reload_lock_wait Time waiting for the mutex of the loader performing the reload.
reload_lock_hold Time holding that loader's mutex during reload, including nested enumeration, metadata, and later parsing work.
reload_enumeration The reload's existing os.ReadDir call.
reload_metadata Aggregate existing DirEntry.Info calls, separate from enumeration.
reload_yaml_read Aggregate YAML file reads, including configured-gallery YAML inspection where applicable.
reload_parse_defaults Aggregate YAML parsing/default application, including configured-gallery YAML classification where applicable. Excludes the measured file read.

Reload observer boundaries

Reload timing measures ModelConfigLoader.loadModelConfigsFromPath, not the unrelated dynamic JSON watcher. Distributed resync constructs a temporary authoritative loader with its own mutex. That loader's reload events do not prove contention on the shared loader lock or ownership of another request's wait. No event establishes cross-host timing or worker execution.

The immutable constructor option WithReloadDiagnostics controls early lock, enumeration, and metadata timing. Ordinary LoadOptionDiagnostics callbacks run at their original position after enumeration and metadata, under the loader lock. Their resolved recorder controls later YAML-read and parse/default timing. An option-only caller cannot observe earlier phases. A late nil recorder disables subsequent instrumented work, not constructor-enabled early measurements.

Unchanged recorder identity preserves the reload operation ID across early and later phases. A different non-nil late recorder receives a separate reload operation ID; these are separate observations, not one correlated span. Callbacks retain ordering, composition, and last-write semantics. They retain every preexisting invocation, including each parsed model's ModelConfig.SetDefaults; diagnostics does not add a replay or snapshot functional options.

See the [CLI reference]({{% relref "reference/cli-reference" %}}) for other server options. The [troubleshooting guide]({{% relref "getting-started/troubleshooting" %}}) covers broader operational checks.