Docs/Build & operate
Operator handbook

Operations

Operate Probara day to day: service lifecycle, health and readiness, Prometheus metrics, logs, migrations, retention, backup and restore, queue inspection, testing, and incident troubleshooting.

Service dependency map

ServiceHard dependenciesDegraded behavior
APIPostgreSQL; valid port/JWT configurationNATS publication/subscription is optional unless private-location authorization is enabled. Run-now checks and live status/alert updates degrade without NATS.
SchedulerPostgreSQL and NATSNo useful degraded mode. It schedules checks, ingests results, builds rollups, purges deleted monitors, enforces retention, and schedules mesh probes.
WorkerNATSCan execute checks without PostgreSQL. Database-free location workers intentionally omit DB; AI-RCA, alert-triage (relay and consumer) and notification consumers require DB.
AlerterPostgreSQLNATS is optional for synchronous evaluation/dispatch, but required for async notification publication and alert-event fan-out.
Status pagePostgreSQLNATS subscriber is optional; without it, cache invalidation waits for STATUS_PAGE_CACHE_TTL.
FrontendReachable API proxy targetStatic shell may load, but authenticated product workflows fail when API is unreachable.
Local AI serviceNone: no database, NATS or network egress; only its token and the bundled modelWhen it is down or saturated, API and worker report local AI unavailable, incident analyses retry and then fail with a retry action, new alert triages retry for a few minutes and are then recorded as failed (paging is unaffected), and dependency suggestions fail. Check execution, alerting and status pages are unaffected.
Collector agent (probara-collector)Reachable HTTPS API and valid API keyScrapes host metrics locally but cannot export while disconnected; there is no remote disable — uninstall is operator-run.

Start, stop, inspect, and restart

Local-process workflow
make start-all-local
make restart-all-local
make stop-all-local

tail -f /tmp/probara-*.log
tail -f /tmp/probara-ui.log
Docker-backed workflow
make start-all
make ps
make logs
make logs-api
make logs-scheduler
make logs-worker
make logs-status
make restart-all
make stop-all
Kubernetes workload inspection
kubectl -n probara get pods,deployments,statefulsets,jobs
kubectl -n probara get events --sort-by=.lastTimestamp
kubectl -n probara logs deployment/probara-api --all-containers --tail=200
kubectl -n probara logs deployment/probara-scheduler --all-containers --tail=200
kubectl -n probara rollout status deployment/probara-api
kubectl -n probara rollout status deployment/probara-worker

Health, readiness, and listener truth

/healthz answers liveness, /readyz checks required dependencies, and /metrics exposes Prometheus data. The port carrying those routes differs by service.

ServiceLocal/Compose URLLiveness semanticsReadiness semantics
APIhttp://localhost:8080Process HTTP handler is alive.PostgreSQL responds within five seconds. NATS is not part of normal API readiness.
Schedulerhttp://localhost:9091Process operational listener is alive.Both PostgreSQL and NATS respond within five seconds.
Workerhttp://localhost:9092Healthy while NATS is connected or reconnecting; unhealthy after permanent closure.NATS responds, plus PostgreSQL when the worker was configured with DB access.
Alerterhttp://localhost:9094Alive; nil/optional NATS is accepted, but a permanently closed configured connection is unhealthy.PostgreSQL responds; NATS also responds when configured.
Status pagehttp://localhost:9093Operational listener is alive.PostgreSQL responds within five seconds.
Local AIhttp://127.0.0.1:8090/healthz, unauthenticated: the process answers, and no batch has run past LOCAL_AI_MAX_INFERENCE_SECONDS (503 wedged otherwise, so Kubernetes restarts the pod)./readyz requires Authorization: Bearer <LOCAL_AI_TOKEN> (401 otherwise) and returns the model identity; 503 while the model loads, when LOCAL_AI_DISABLED=true, during shutdown, or while a batch is wedged.
NATShttp://localhost:8222/healthzBroker monitor endpoint.Use JetStream/consumer checks in addition to HTTP liveness.
Probe manually
curl -fsS http://localhost:8080/healthz
curl -fsS http://localhost:8080/readyz
curl -fsS http://localhost:9091/readyz
curl -fsS http://localhost:9092/readyz
curl -fsS http://localhost:9093/readyz
curl -fsS http://localhost:9094/readyz
curl -fsS -H "Authorization: Bearer $LOCAL_AI_TOKEN" http://127.0.0.1:8090/readyz

make healthcheck

Worker also serves /mesh/echo on the metrics listener. When its HTTP_PORT differs, it starts a second listener containing only mesh echo and /healthz, allowing mesh exposure without publishing /metrics.

Prometheus metrics

Custom metrics use namespace probara and a sanitized service subsystem, for example probara_scheduler_loops_total. Go runtime and process collectors are also registered.

AreaKey metricsWhat to alert on
Scheduler loopprobara_scheduler_loops_total, monitors_scheduled_total, loop_duration_seconds, monitors_in_batchNo loop increments, duration approaching schedule interval, or sustained full batches.
Scheduler failuresjobs_publish_errors_total, db_errors_total, mesh_publish_errors_totalAny sustained nonzero rate.
Result ingestresults_ingested_total, result_ingest_errors_total, result_ingest_duplicates_total, mesh_results_ingested_totalPublish success without ingest growth, ingest errors, or unexpectedly high duplicates.
Retention and rollupsretention_cleanup_runs_total, retention_cleanup_rows_total, rollup_runs_total, rollup_rows_total, rollup_errors_total, rollup_duration_seconds, rollup_cursor_unixStale completeness watermark, error growth, or runs exceeding the maintenance window.
Monitor purgemonitor_purge_runs_total, monitor_purge_rows_total, monitor_purge_monitors_total, monitor_purge_errors_total, monitor_purge_run_duration_secondsGrowing errors or deleted monitors never becoming fully purged.
Worker executionprobara_worker_jobs_total{status=…}, job_duration_seconds, http_request_duration_seconds, http_errors_total{reason=…}Failure-rate changes, high latency, timeouts, and policy blocks.
Worker NATS/resultsnats_ack_total, nats_nak_total, result_publish_errors_total, side_effect_redelivery_skipped_total{type=…}, side_effect_result_abandoned_total{type=…}, side_effect_result_interrupted_total{type=…}NAK or publish-error growth and falling ACK throughput; any skipped redelivery or abandoned result of a side-effecting check (a worker died or lost NATS mid-check). Interrupted results are side-effecting checks a worker shutdown cut short (expected around deploys; each is a missing sample).
SIP test callssip_linger_total{outcome=late_answer|late_bye_ok|late_bye_unconfirmed|late_cancel|quiet|flow_closed|aborted}, sip_linger_dropped_total, test_check_side_effect_rejected_total{type=…}, sip_call_teardown_unconfirmed_total{evidence=audio_after_bye|none}, sip_media_sessions_total{outcome=measured|truncated|refused|unusable_answer|not_held|interrupted}, sip_media_refused_total{reason=…}, sip_media_rx_dropped_total{reason=invalid|foreign|overflow}, sip_media_unreliable_totalAny sip_linger_dropped_total growth: every linger slot (32 per worker) was taken, so a late or forked answer to that call was neither ACKed nor BYE'd and may stay up, and billed, until the far end times it out. Sustained late_answer means the far end answers after the check gave up. late_bye_ok and late_bye_unconfirmed count the second BYE a linger sends when a call's hang-up BYE drew no response: late_bye_ok means the far end confirmed it (a 2xx, or 481 for a dialog it had already ended), and late_bye_unconfirmed means it went unanswered again or was refused, so the call may still be up, and billed, until the far end times it out; it outranks a late answer in the same linger. test_check_side_effect_rejected_total counts Test calls a worker refused because 16 were already running on it; scheduled checks are not affected. sip_call_teardown_unconfirmed_total{evidence=audio_after_bye} counts unconfirmed hang-ups after which the far end kept sending audio: those calls are likely still up and billed. For call quality, sip_media_refused_total by reason shows far ends whose media address the worker policy refuses (private_behind_public growth usually means a carrier leaks private SDP addresses, or HTTP_ALLOWED_CIDRS lacks an on-premises range); sip_media_rx_dropped_total{reason=foreign} growth means audio arriving from addresses the SDP did not name (asymmetric media); and any sustained sip_media_unreliable_total means the worker is too loaded to time media, so its quality thresholds are skipped: give it CPU or fewer concurrent checks.
Status live updatesprobara_status_page_sse_subscriber_connectedZero when low-latency status invalidation is expected.
Local AI service (port 8090, plain-text exposition)probara_ai_ready, probara_ai_admitted, probara_ai_completed_total, probara_ai_rejected_total, probara_ai_failed_total, probara_ai_inference_seconds_sum, probara_ai_inflight_seconds, probara_ai_wedgedready at 0, growth in rejected_total (429 saturation) or failed_total, admitted pinned at queue capacity + 1, and wedged at 1 (the executing batch, inflight_seconds old, has passed LOCAL_AI_MAX_INFERENCE_SECONDS). Divide inference_seconds_sum by completed_total for mean batch time.
Inspect metrics locally
curl -fsS http://localhost:8080/metrics | grep '^probara_'
curl -fsS http://localhost:9091/metrics | grep '^probara_scheduler_'
curl -fsS http://localhost:9092/metrics | grep '^probara_worker_'
curl -fsS http://localhost:9093/metrics | grep '^probara_status_page_'
curl -fsS http://127.0.0.1:8090/metrics

The local AI service runs one batch at a time and admits at most LOCAL_AI_QUEUE_CAPACITY more. A batch beyond that, or one that waits longer than LOCAL_AI_QUEUE_TIMEOUT_SECONDS (default 60 s, a few full batches) for the executing slot, gets HTTP 429 with Retry-After: 5. The worker treats that as transient: an incident analysis or alert triage is redelivered 1 minute and then 2 minutes later (or after the service’s Retry-After, at most 2 minutes), and the third failed delivery records it as failed; a failed analysis carries the action to check readiness and retry. Dependency suggestions, which the API computes on request, answer 503 unavailable with Retry-After instead of an error. A batch still running after LOCAL_AI_MAX_INFERENCE_SECONDS (default 120 s) is wedged: /healthz answers 503 and probara_ai_wedged is 1 until it finishes, so the Helm liveness probe restarts the pod; under Compose the container turns unhealthy and needs docker compose restart local-ai. Bodies over 512 KiB get 413 and evidence outside the allowlist 422. The chart does not annotate this pod for scraping; see local AI values.

Logs and diagnostic context

  • Use LOG_LEVEL=debug temporarily for connection, scheduling, and readiness diagnosis; restore info after the incident.
  • Correlate monitor ID, tenant ID, location ID, alert ID, NATS stream/subject, and timestamps across services.
  • Never paste location-authenticated NATS URLs, API keys, SMTP passwords, webhook secrets, OIDC secrets, JWT secrets, or encrypted configuration keys into tickets or chat.
  • A warning that status-update publishing failed means core CRUD may work while public live invalidation is degraded.
  • A scheduler warning that RESULT_INGEST_ENABLED=false means workers can run checks but results will not enter PostgreSQL.
  • A plaintext/NoOp encryption warning requires immediate review before storing or updating sensitive configurations.
Focused Compose logs
docker compose logs --since=15m api scheduler worker alerter status-page
docker compose logs -f --tail=200 scheduler worker

Database migrations and administrator bootstrap

Run migrations
make migrate

# Direct local migration command
POSTGRES_URL='postgres://probara:probara@localhost:5432/probara?sslmode=disable' \
MIGRATIONS_PATH='./shared/db/migrations' \
go run ./cmd/migrate

The migration CLI requires POSTGRES_URL, defaults MIGRATIONS_PATH to ./migrations, and retries the initial database connection up to 30 times at one-second intervals. API startup also runs the shared migration set. The Helm migration Job is a post-install/post-upgrade hook.

Create or update the initial administrator
POSTGRES_URL='postgres://probara:probara@localhost:5432/probara?sslmode=disable' \
ADMIN_USERNAME='admin' \
ADMIN_PASSWORD='use-a-password-manager-generated-value' \
ADMIN_BCRYPT_COST='12' \
go run ./cmd/admin

The command always leaves a platform superadministrator behind, with no tenant memberships (a superadministrator reaches every tenant), matching the first-user form. Unlike that form it also works on a populated install: run against an existing username, it resets the password, re-enables a disabled account and promotes it to superadministrator, which makes it the break-glass recovery path for an operator with database access.

Backup and restore

Compose PostgreSQL backup and restore
make db-backup       # writes ./backup.sql

# Inspect and copy the backup away from the workstation before maintenance.
ls -lh backup.sql

# Restore into the configured Compose database.
make db-restore
  1. Quiesce writes or capture a database-consistent snapshot appropriate to your PostgreSQL topology.
  2. Back up PostgreSQL, including schema migration state, tenants, monitor configuration, alert state, users, API keys, audit data, and encrypted ciphertext.
  3. Back up the complete encryption keyring separately. A database backup without the historical keys may be unrecoverable.
  4. Protect JetStream state or accept that queued checks/results/notifications may be replayed or lost after recovery.
  5. Preserve browser artifacts separately if they are part of your incident evidence policy.
  6. Restore into an isolated environment, run readiness and data-integrity checks, then perform a documented cutover.

The bundled PostgreSQL and NATS StatefulSets use PVCs but provide no scheduled backup controller, point-in-time recovery, replication, or cross-zone failover. Production recovery objectives require managed services, an operator, or external backup automation.

Retention, rollups, and deletion

Tenant telemetry retention
Raw check results are kept 30 days for every tenant, including tenants set to 0; ranges of 7 days and longer, status-page history, and latency-anomaly baselines read the hourly and daily rollups, which are kept 400 days. Cleanup removes whole hours only, starting one hour below the 30-day horizon, and keeps any hour still waiting for its rollup rebuild until the rebuild runs; a result that arrives for an hour already past the horizon is stored but not folded into rollups. Each tenant’s data_retention_days controls mesh results and can tighten (never extend) raw agent metric-sample retention below METRIC_RAW_RETENTION_DAYS. A value of 0 preserves mesh results; the public validator otherwise accepts 30–3650 days. It is configured in tenant settings.
State history and SLA reports
Monitor state intervals (the timeline behind time-based availability and SLA reports) are not pruned by time; they are removed only with a monitor’s history or when a deleted monitor is purged. Issued SLA reports are kept until their SLA is deleted and survive monitor purges.
Alert triage
Each alert_triage row lives as long as its alert: it is deleted with the alert (monitor purge or history deletion) or with its workspace, and is not pruned by time or by data_retention_days. Labels on a triage outlive it, like other AI labels.
AI training labels
Operator labels on AI analyses, alert triages and dependency suggestions are not pruned by time or by data_retention_days, and survive monitor purges and incident deletion; they are removed only with their workspace.
Audit retention
AUDIT_RETENTION_DAYS is a separate compliance control, defaults to 365 days, and uses 0 to retain forever.
Monitor purge
Soft-deleted monitors are later purged with dependent rows in bounded batches.
Rollups
Result ingestion marks each affected hour in a dirty-bucket ledger within the same transaction, and scheduler maintenance rebuilds marked buckets wholesale from raw results — late or redelivered rows can never be permanently skipped. A failed batch leaves its marks for the next run.

Scheduler checks once per minute whether the configured UTC cleanup hour is due, runs at most once per UTC date, uses a PostgreSQL advisory lock, and applies a 30-minute cleanup timeout. Batch size and maximum rows per run bound database pressure.

ControlDefaultOperator effect
RETENTION_CLEANUP_ENABLEDtrueEnable daily tenant telemetry cleanup.
RETENTION_CLEANUP_HOUR_UTC2Daily target hour, 0–23 UTC.
RETENTION_CLEANUP_BATCH_SIZE5000Delete batch size.
RETENTION_CLEANUP_MAX_ROWS_PER_RUN200000Per-run cap; backlog can require multiple days/runs.
AUDIT_RETENTION_DAYS365Separate hourly API audit pruner; 0 disables pruning.
METRIC_RAW_RETENTION_DAYS30Raw OTLP metric samples (daily partitions); a tenant's data_retention_days can tighten it. Hourly metric rollups are kept 400 days.
OTLP_MAX_SERIES_PER_MONITOR2000Metric-series cardinality cap per agent monitor; overflow points are rejected via OTLP partial_success.
OTLP_MONITOR_RATE_PER_MIN60OTLP ingest requests per minute per monitor; excess returns 429 with Retry-After.
MONITOR_PURGE_INTERVAL_SECONDS30Soft-delete purge cadence.
MONITOR_PURGE_BATCH_SIZE5000Child-row purge batch size.
MONITOR_PURGE_MAX_ROWS_PER_RUN200000Purge safety cap.

NATS and JetStream operations

ContractCode defaultPurpose
CHECK_JOBS / check.jobsWork-queue stream/subjectScheduler and API publish; workers consume.
CHECK_RESULTS / check.resultsWork-queue stream/subjectWorkers publish; scheduler persists.
ALERTS / alertsAlert event stream/subjectAlerter publishes; API live subscriber consumes.
NOTIFICATIONS / alerts.dispatch.>Optional work queueAsync alerter-to-worker dispatch.
AI_RCA / ai.rca.jobsAI work queueAPI publishes tenant RCA work; DB-connected workers consume, restart the consumer with capped backoff (re-creating the durable if it disappears), and fail analyses pending for over 20 minutes (longer than the retries for a temporarily unavailable local AI).
AI_TRIAGE / ai.triage.jobsAlert triage work queue (1-hour max age, 30-minute duplicate window)Worker-internal; triage never changes paging (the alerter may send an opt-in suspected-cause follow-up from ready triages). The triage relay in DB-connected platform workers claims alerts every 15 seconds and publishes one job per claim, using the triage ID as Nats-Msg-Id. It republishes a job no worker has started up to twice, 3 minutes apart (duplicates are dropped), and fails triages still pending 30 minutes after a worker started them or 70 minutes after the claim when none did. The durable ai-triage-workers consumer restarts like AI_RCA. One relay tick runs at a time across replicas (a PostgreSQL advisory lock).
Core NATS statuspage.updatesNon-JetStream fan-outCache/status update invalidation.
Inspect with the NATS CLI
nats --server "$NATS_URL" server check connection
nats --server "$NATS_URL" stream ls
nats --server "$NATS_URL" stream info CHECK_JOBS
nats --server "$NATS_URL" consumer ls CHECK_JOBS
nats --server "$NATS_URL" stream info CHECK_RESULTS
nats --server "$NATS_URL" consumer info CHECK_RESULTS result-ingest
  • Compare the actual stream subjects with every publisher and consumer environment before deleting or recreating streams.
  • Watch pending, redelivered, and unacknowledged counts; queue depth without scheduler/worker progress indicates a contract or connectivity failure.
  • CHECK_JOB_LEGACY_CONSUMERS causes scheduler startup to delete named filterless consumers. Review the list before renaming durable consumers.
  • JetStream PVC persistence does not replace a broker backup/replication strategy.
  • Core NATS status updates are ephemeral by design; status cache TTL is the fallback.

Synthetic-browser artifacts

  • Workers write failure screenshots beneath SYNTHETIC_BROWSER_ARTIFACTS_DIR; API reads them for authenticated retrieval. Trace and HAR requests currently emit warnings and do not create artifacts.
  • Compose mounts a shared named volume into API and worker.
  • Local-process services share the same host directory.
  • The current Helm workloads do not mount shared artifact storage, so cross-pod and cross-node retrieval is unreliable.
  • Set retention and access controls appropriate for screenshots because they can capture page content, tokens, personal data, or internal application state.

Verification and tests

Main Go module
make test
make lint

# make fmt is a check, not a formatter.
gofmt -w path/to/changed.go
make fmt
make vet
Frontend and chart
(cd web && npm run lint && npm run build)
(cd website && npm run typecheck && npm run build)

helm lint ./helm/monitoring-platform \
  --set secrets.adminJwtSecret='replace-with-32-plus-char-secret-value' \
  --set api.publicBaseURL='https://probara.example.com'
  • make test runs the Go module with race detection and writes coverage.out; it excludes script packages. There is no separate agent module or test suite — the collector is a prebuilt OpenTelemetry distribution (make build-collector-static).
  • Database integration tests use Testcontainers and require a working Docker daemon capable of starting PostgreSQL 16.
  • Run make test-cover to open the HTML coverage report.
  • Frontend changes require both lint and a production build; documentation site changes should at least run npm run typecheck and npm run build from website/.
  • Helm lint requires the chart’s mandatory JWT and public base URL values.
  • For a release candidate, verify scheduled checks, on-demand checks, all touched monitor types, result ingest, alerts, notification delivery, status pages, OIDC, imports, private locations, and backup restore.

Troubleshooting guide

SymptomLikely causeChecks and resolution
Configuration exits immediatelyMissing ports/JWT, short or placeholder JWT, invalid bool/int/CIDR, incomplete OIDC, or location ID/credential mismatchRead the first fatal line; compare the process environment with the configuration reference.
Compose command fails before containers startRequired ${ADMIN_JWT_SECRET:?…} or ${PUBLIC_BASE_URL:?…} interpolation is missingPopulate .env before any Compose-backed Make target.
PostgreSQL authentication fails after old local experimentsExisting volume was initialized with different credentialsBack up first. If disposable, docker compose down -v reinitializes all repo-scoped volumes and destroys local data.
Scheduled checks work, Run now does notAPI publishes on a different CHECK_JOB_SUBJECT than the workers consume (a custom override applied to only some services)Align CHECK_JOB_SUBJECT and stream configuration on API, scheduler, and worker; the shipped Compose file and Helm chart set all three identically. Inspect actual JetStream subjects.
Checks execute but no history appearsResult-ingest disabled, scheduler unavailable, result subject mismatch, DB failure, or ingest backlogCheck scheduler readiness/logs and results_ingested_total / result_ingest_errors_total; inspect CHECK_RESULTS consumer state.
Private location does not startMissing credential, invalid UUID, unsafe public NATS URL, broker authentication/TLS issue, or no external broker routeRegenerate deployment info; require both location ID and credential; test the TLS/WSS endpoint from that network.
Encrypted monitor fails only in schedulerScheduler lacks the current/historical encryption keyringProvide the identical PROBARA_SECRETS_KEY* set to scheduler. The shipped Compose file and Helm chart wire the base key; add rotation keys (_V2+) through the chart's top-level extraEnv so every service receives the same keyring.
Browser screenshot is 404/missingAPI and worker use different filesystems/pods or artifact expiredVerify identical artifact directory and shared storage. Helm does not currently provide it.
Email fails on port 587SMTP_USE_TLS=true selects implicit TLS rather than STARTTLSConfirm provider protocol. Use the correct implicit-TLS port or a supported non-TLS/private relay setting.
OIDC redirects or callback failsIssuer/client secret/callback mismatch, bad public base URL, or JIT tenant/role errorCompare the exact HTTPS callback with the IdP registration and inspect OIDC discovery.
SSO user loses roles (or gains none) after loginOIDC group mappings exist but the ID token carries no groups claim — groups scope not requested, wrong OIDC_GROUPS_CLAIM, or the IdP does not embed groups in the ID tokenCheck API logs for the missing-groups warning, add groups to OIDC_SCOPES, and verify the group-mapping requirements. Deleting all mappings restores manual role management.
Status page updates slowlyNATS status subscriber disconnected or update subject differsCheck sse_subscriber_connected, subscriber logs, shared subject, and cache TTL.
Frontend links point to old originNEXT_PUBLIC_* changed only at runtimeRebuild the frontend client bundle with the desired public values or use stable relative routing.
Settings → AI says “not configured correctly”LOCAL_AI_TOKEN missing or shorter than 16 characters on the API, or an invalid LOCAL_AI_URLCompare the API and worker environment with the service’s. A worker with the same problem fails incident analyses with “Local AI is not configured correctly for this installation”, claims no alerts for automatic triage (so the hourly cap is not spent on triages that would fail), and logs a warning at startup. In Helm, an existing Secret without local_ai_token leaves API and worker without a token by design; add the key or set secrets.localAiTokenExistingSecret.
Settings → AI says “unreachable”The service is not running, its image was never built or pulled, the URL points elsewhere, or a NetworkPolicy blocks the callerCheck docker compose ps local-ai / kubectl get pods -l app.kubernetes.io/component=local-ai and its logs. A local image build fetches the pinned base model from Hugging Face and fails on purpose for a checkpoint that is not release-approved; to run without local AI, set LOCAL_AI_DISABLED=true with the Make targets (start-all, start-all-local, up), which then skip local-ai, or localAI.enabled=false (Helm). Plain docker compose up still pulls or builds local-ai whatever LOCAL_AI_DISABLED says: name the other services instead, or set LOCAL_AI_IMAGE to an image you already have. Only the API and platform worker pods are admitted by the chart policy.
Settings → AI says “not ready”Model still loading, LOCAL_AI_DISABLED=true on the service only, a token mismatch (401), or a model whose catalogue version differs from the API’sProbe /readyz with the token. A mismatch needs the same token on API, worker and service and a restart of all three. The readiness result is cached for about 30 seconds per API replica; the Settings test probes afresh.
Local AI pod in CreateContainerConfigError after upgradesecrets.existingSecret lacks the local_ai_token keyAdd a token of at least 16 characters (openssl rand -hex 32) under that key, set secrets.localAiTokenExistingSecret, or set localAI.enabled=false.
Incident analyses fail during loadInference saturation: 429 responses exhausted the worker’s three deliveriesWatch probara_ai_rejected_total and probara_ai_admitted. Raise localAI.queueCapacity or localAI.queueTimeoutSeconds (it must stay below localAI.timeoutSeconds), give the pod more CPU up to 4 threads, or retry the analysis later. If probara_ai_wedged is 1, one batch is holding the slot: the liveness probe restarts the pod, or restart it yourself.
Prometheus cannot scrape API on 9090API has no separate metrics listenerScrape /metrics on API HTTP port 8080 or fix the workload/service topology.

Incident response checklist

  1. Identify the affected tenant, monitor/location, time window, and whether the failure is control plane, check execution, result persistence, alerting, or presentation.
  2. Check liveness and readiness on the real service listeners; preserve logs and relevant metrics before restarting.
  3. Inspect PostgreSQL availability and NATS stream/consumer state without deleting consumers or messages.
  4. Compare deployed queue variables and encryption-key versions across all replicas.
  5. Reduce blast radius with reversible actions: pause a broken monitor/policy, scale a healthy worker fleet, or disable optional async paths.
  6. Restore service, verify end-to-end from schedule through status/alert delivery, then reconcile delayed or duplicate jobs.
  7. Record the operator action in incident notes and retain the audit/log/artifact evidence according to policy.