Skip to main content

Monitoring Metrics

The service exposes these health and metrics surfaces:

SurfaceCommand or endpointUse
HealthGET /healthLoad balancer and uptime checks
MetricsGET /metricsPrometheus-compatible scrape endpoint
Live dashboardjust status liveHuman operations dashboard

Metrics Endpoint​

GET /metrics returns Prometheus text format and does not require an API key. HTTP path labels use FastAPI route templates, with unmatched paths collapsed to __unmatched__ to avoid unbounded label cardinality. Celery workers write task duration and failure samples to Redis so the API metrics scrape can expose worker observations. It exports:

MetricPurpose
infoconnect_http_requests_totalRequest count by method, route-template path, and status
infoconnect_http_errors_total4xx/5xx error count by method, route-template path, and status
infoconnect_http_request_duration_secondsAPI latency histogram for p95/p99 queries
infoconnect_search_latency_secondsSearch latency histogram with reranked label
infoconnect_task_duration_secondsCelery task duration histogram
infoconnect_indexing_failures_totalIndexing failure count by task
infoconnect_queue_depthRedis queue depth for default, preprocessing, and embedding
infoconnect_component_healthAPI, Redis, and Qdrant health gauges (1 healthy, 0 unhealthy)
infoconnect_resource_usage_percentHost CPU, memory, and disk usage percent
infoconnect_metadata_discovery_readiness_clientsClients by readiness state (ready, backfilling, blocked), with no client identity
infoconnect_publication_transitions_totalPublication transitions by bounded transition and outcome
infoconnect_publication_waitaof_failures_totalPublication durability barriers that did not confirm one local AOF fsync
infoconnect_publication_invariant_violations_totalFail-closed publication invariant violations by bounded kind
infoconnect_publication_store_operations_totalPublication store operations by bounded store, operation, and outcome
infoconnect_publication_store_recordsCurrent record count in each publication store
infoconnect_publication_authority_stateOne-hot current publication authority state
infoconnect_publication_pending_acksCaptured publication worker acknowledgements still pending by kind
infoconnect_publication_runtimeBounded publication runtime diagnostics, including serving_ready, publishing_ready, AOF health, fence age, and Redis headroom

A client that has not finished its Metadata Backfill is a client capability state, not a service fault. blocked and backfilling counts never make infoconnect_component_health or /health fail. Content search is unaffected; only metadata and both requests require a ready Client.

The readiness gauge is emitted as infoconnect_metadata_discovery_readiness_clients{state=...} for the bounded ready, backfilling, and blocked states. It carries aggregate counts only; it never includes a client label.

Health Endpoint Semantics​

The Health API publication contract owns the response fields, component conditions, and HTTP status rules. Monitor serving availability separately from publishing readiness: a serving-safe fence pauses publication writes while keeping read traffic available.

The matching bounded metrics are:

  • infoconnect_publication_runtime{metric="serving_ready"} is 1 only when this process can safely serve publication-backed reads.
  • infoconnect_publication_runtime{metric="publishing_ready"} is 1 only when a new publication can enter the global fence immediately.

Keep infoconnect_publication_authority_state and fence_age_seconds on dashboards for diagnosis; do not use exact authority READY as a serving-availability proxy. Alert on dependency_health_changed transitions and dependency_timeout_cluster events rather than individual failed samples.

Dashboard Queries​

Use these PromQL expressions in Grafana or another Prometheus dashboard:

sum(rate(infoconnect_http_requests_total[5m]))
sum(rate(infoconnect_http_errors_total[5m])) / sum(rate(infoconnect_http_requests_total[5m]))
histogram_quantile(0.95, sum(rate(infoconnect_http_request_duration_seconds_bucket[5m])) by (le, path))
histogram_quantile(0.99, sum(rate(infoconnect_http_request_duration_seconds_bucket[5m])) by (le, path))
histogram_quantile(0.95, sum(rate(infoconnect_search_latency_seconds_bucket[5m])) by (le, reranked))
sum(infoconnect_queue_depth) by (queue)
min(infoconnect_component_health) by (component)
max(infoconnect_resource_usage_percent) by (resource)

Operations Workflow​

Use /metrics for automated scraping and just status live for hands-on triage. During UAT, capture both the Prometheus dashboard screenshot or query output and a just status docker --iterations 1 snapshot as go-live evidence.

Request-correlated logs​

The existing API middleware accepts X-Request-ID, generates one when absent, and echoes it in the response. API logs and request-started Celery tasks carry that request_id. Celery publishes it in task headers, then binds it with task_id, task_name, the upload job_id, client_id, document_id, version_id, and generation where known. Children and retries inherit the same request ID. Worker output uses the same structlog configuration as the API: JSON with DEBUG=false, console output with DEBUG=true. The lookup below requires JSON output. Search runs in the API; it does not enqueue a Celery task.

Capture the response header or send an ID you can search for:

curl -i -X POST "$API_URL/search" \
-H "X-API-Key: $API_KEY" \
-H "X-Request-ID: incident-20261013-01" \
-H "Content-Type: application/json" \
-d '{"query":"policy retention","scope":"content"}'

On the Docker host, use the deployment's project name and env files. This is read-only:

REQUEST_ID=incident-20261013-01
docker compose --env-file .env --env-file .runtime/prod.env \
-f docker-compose.yml -f docker-compose.prod.yml \
logs --no-color --no-log-prefix --since 24h \
api worker-preprocessing-1 worker-embedding-1 worker-embedding-2 2>&1 \
| jq -R --arg id "$REQUEST_ID" 'fromjson? | select(.request_id == $id)'

For one container, use docker logs --since 24h <container> 2>&1 with the same jq filter. With tracing enabled, logs emitted within an active trace also have trace_id and span_id; paste trace_id into the collector's trace search. These IDs are log fields, not Prometheus labels. Existing metrics and alert rules are unchanged.

Failed background tasks​

GET /metrics shows how many tasks failed per task name (infoconnect_indexing_failures_total), but not which task, document, or error. The Celery result backend stores each task's metadata by ID, and the celery Redis ACL user is denied SCAN/KEYS, so the celery-task-meta-* keyspace cannot be enumerated and direct store scans stay denied. The worker's task_failure signal therefore also appends one entry per failure to a bounded log at infoconnect:metrics:task_failure_log (app/utils/metrics.py), under the key pattern the production ACL already grants.

List recent failures with the operator lookup:

just failed-tasks                     # newest first, table
just failed-tasks --limit 50 --client <client-id>
just failed-tasks --json # machine-readable

Each row carries the failing time, the task id (the API job_id for client jobs), the Celery task name, the Client, the document/version, and the error message. The log keeps the most recent 200 failures (TASK_FAILURE_LOG_MAX_ENTRIES) on the broker connection (DB 1) and is read with a bounded LRANGE — no scan, no ACL change. Entries are written by the worker that observes the failure; run the lookup on the deployment host (or set --redis-url). GET /status/{job_id} remains the per-job, Client-scoped view; a failure that predates the deploy will not appear in the log.

Rotation and retention​

The production and Console overlays set Docker's json-file driver with max-size=100m and max-file=20, configurable through DOCKER_LOG_MAX_SIZE and DOCKER_LOG_MAX_FILE. This bounds logs to about 1.95 GiB per container. It covers 24 hours at up to about 23.7 KiB/s per container; size rotation is not a time guarantee. Measure each service's daily output, allow headroom for incident bursts, and raise the budget or ship off-host if it exceeds that rate. Seven services can consume about 13.7 GiB in total, apart from volumes and images.

Docker applies driver options when containers are created, not on restart. An approved deployment must recreate containers to apply the new policy. This change does not deploy or recreate anything in production. Do not delete log files under Docker's data directory; let Docker rotate them.

Search debug trace​

Every /search request, for every API key, writes one bounded search_trace record while SEARCH_TRACE_ENABLED (the default) is on. The record goes only to the durable sink below: the ordinary application log keeps the redacted search_completed line above, with no query text and no scores. Set SEARCH_TRACE_ENABLED=false to roll back — no record is built or written anywhere and the behaviour matches the default path exactly.

The record carries the request request_id, the resolved client_id (never the API key), the scope and requested limit the search ran with, the original query, the corrected_query actually retrieved with after abbreviation expansion, known-typo correction and corpus fuzzy correction (query_rewritten reports whether they differ), the top results (results: the strongest content and metadata item per Document Version, with score, the page when the payload has one, and a text preview), and per-stage timings (stages: stage name, call count, total and max milliseconds). It is bounded in app/utils/search_trace.py: at most 10 results (results_limit reports that cap), 200 characters per query or preview, and 32 stage entries, so one record stays a few KiB.

Retention is 30 days in a durable sink. Compose mounts the API container at /var/log/infoconnect from the host directory SEARCH_TRACE_LOG_HOST_DIR, and the sink keeps <SEARCH_TRACE_LOG_PATH>/search-trace.jsonl there, rotated at UTC midnight and pruned to SEARCH_TRACE_RETENTION_DAYS (default 30) daily files. The host path is what makes the horizon survive container recreation and redeploys: Docker's json-file rotation alone dies with the container. Set SEARCH_TRACE_LOG_HOST_DIR to the host path that must keep the traces (the default is ./.runtime/search-trace); on a redeploy the API container is recreated and the sink keeps appending to the same host file. Size the host filesystem for search volume: one record is at most a few KiB and every search writes one.

Search trace permissions​

The API container runs as uid/gid 999. Before starting services, just prod-up (including prod-up rebuild and prod-rebuild) creates the mounted /var/log/infoconnect directory, assigns ownership to 999:999, grants owner read/write/execute permission, and probes a write as that user. Compose resolves the host mount, including SEARCH_TRACE_LOG_HOST_DIR set through .env. Preparation or probe failure stops the recipe before service startup.

For direct Compose startup, create the host directory beforehand and give it that ownership (mkdir -p "$SEARCH_TRACE_LOG_HOST_DIR" && chown 999:999 "$SEARCH_TRACE_LOG_HOST_DIR"); Docker otherwise creates it root-owned on Linux. The API opens the sink file at startup and, if the directory is not writable, logs search_trace_sink_unavailable and writes no traces.

# Trigger a search with any configured key
curl -s -X POST "$API_URL/search" -H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" -d '{"query":"attn jpm","scope":"both"}' >/dev/null

# Durable sink (host path, survives container recreation and redeploys)
jq -c 'select(.event == "search_trace")' "$SEARCH_TRACE_LOG_HOST_DIR"/search-trace.jsonl*
ls -1 "$SEARCH_TRACE_LOG_HOST_DIR"/search-trace.jsonl* # one file per retained UTC day

Traces hold every customer's raw query text, so treat the sink as sensitive production data: only operators already cleared to read customer data may read it, keep it on the host (never copy it into tickets or shared drives), and rely on the 30-day prune instead of manual deletion.

scripts/qa_stack.py provisions the sink for isolated stacks: the API container mounts the stack's own host directory .qa/<project>/search-trace, so a live QA check reads the traces from disk while the app log stays redacted.

No shipping service is enabled or deployed here. An operator can separately deploy Vector or Grafana Alloy on the host, decode the Docker JSON envelope and the inner structlog JSON, and send records over authenticated TLS to Loki or a hosted log store. Preserve timestamp, service/container, request_id, trace_id and task identity. Use service/environment as indexed labels; keep request/trace/task/document IDs as structured fields, not high-cardinality Loki labels. For example, after JSON decoding:

{service="infoconnect", environment="production"} | json | request_id="incident-20261013-01"

Choose off-host retention of at least 24 hours, persistent shipping checkpoints, bounded disk buffering, redaction rules, access controls and an alert for dropped logs. Existing DEBUG logs can contain search text, so review fields before shipping. Docker socket access grants host-level power; prefer read-only log-file ingestion with the minimum host permissions. Shipping and collector deployment need separate approval.

Optional OpenTelemetry tracing​

Tracing is off by default. The tracing extra in pyproject.toml owns the pinned SDK, OTLP HTTP/protobuf exporter and HTTPX/Redis instrumentation dependencies; uv.lock owns their resolved versions. No auto-instrumentation agent, gRPC exporter, OpenTelemetry log exporter or metrics exporter is installed. Default builds omit the extra entirely.

Disabled startup never imports or initializes OpenTelemetry and creates no exporter, instrumentation threads or collector traffic. It omits service wrappers and the trace log processor. Install the optional extra before enabling; setting the flag without installing it fails startup. See the configuration reference for environment variables and defaults.

Spans cover query parsing/spelling, dense and lexical query preparation, Metadata dense preparation, lane fusion, rerank, publication begin/candidate/final checks, Qdrant calls, HTTPX and synchronous/asynchronous Redis commands. A hybrid Qdrant request runs dense, sparse prefetch and RRF on the server in one call; its server-side sub-times cannot be separated by client spans. Embedding and rerank are local FastEmbed inference, not remote HTTP services; embedding.dense, embedding.sparse and rerank.predict measure that work. Stages that the request does not use produce no synthetic spans.

API requests accept W3C traceparent/tracestate. Celery task headers propagate those fields from the API and between child tasks. Each task creates a consumer span. Prefork workers initialize their exporters after fork; shutdown flushes the batch queue. Trace export keeps timing, topology and bounded method/status/operation attributes, not request bodies, HTTP URLs, Redis statements, exception messages or stack traces. Audit logs still use their existing sanitizers; other application logs are not filtered by the trace exporter and need the shipping review described above.

Local collector example​

Use a separate Compose project and non-conflicting API/Redis/Qdrant ports for local testing. Do not run these commands against a shared or production stack. A standalone local Jaeger instance can receive OTLP HTTP and expose its UI:

docker run --rm --name ifc-local-traces \
-p 127.0.0.1:16687:16686 -p 127.0.0.1:14318:4318 \
jaegertracing/jaeger:2.6.0

For local Python API and worker processes:

uv sync --frozen --extra dev --extra tracing
export TRACING_ENABLED=true
export OTEL_SERVICE_NAME=infoconnect-local
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:14318/v1/traces
export TRACING_SAMPLE_RATIO=1.0

Start the API and both worker pools with these variables and the isolated stack's URLs. The flag is a startup setting: restart API and workers after changing it. Browser clients may send traceparent/tracestate through CORS and read the exposed X-Request-ID. Send a search and an upload, then open http://127.0.0.1:16687 and look up their logged trace IDs. The upload's trace continues in Celery after the HTTP response.

For containerized API/workers, build with TRACING_EXTRA=1 and set TRACING_ENABLED=true. The collector endpoint must be reachable inside every enabled container: localhost there means that container, not the host. Put a separately managed collector on the isolated network, or configure a host-gateway route in a local override. Rebuild and recreate only that isolated project. Empty TRACING_EXTRA and TRACING_ENABLED=false restore the default runtime.

Production collector requirements​

A later approved deployment must provide an OTLP HTTP/protobuf receiver reachable from all API and worker replicas, TLS and authentication, restricted network ingress, persistent trace storage and an explicit retention/access policy. Configure sampling for the expected volume, collector memory limits, bounded queues, durable buffering, and export/drop/error monitoring. API and worker service names must be searchable together. Mount any custom CA and set the optional exporter headers through secret management. Exporter timeout is 5 seconds and export runs on a bounded batch queue; a collector outage must not change publication or search correctness, but can lose trace evidence. No collector, shipping agent, production flag change or deployment is part of this change.

Alert Rules​

Load the go-live Prometheus rules from monitoring/prometheus-alerts.yml. These rules assume the API scrape job is named infoconnect-api; worker-down detection uses the task metrics and queue-depth gauges exported by the API /metrics endpoint rather than per-worker scrape targets. Keep the same labels in UAT and production so alert routing can separate environments by the Prometheus environment external label.

InfoConnectWorkerDown assumes the preprocessing and embedding queues are operated as distinct worker pools. In a single-pool deployment, successful work on one queue can hide a stalled queue because the task-throughput signal is shared. Treat single-pool sign-off as conditional unless you add per-queue or per-task-type throughput evidence to the release packet.

InfoConnectHighErrorRate is scoped to 5xx availability failures. It intentionally does not cover 4xx/auth-pressure detection; if credential probing, scraping, or rate-limit pressure is a go-live criterion, add a dedicated 401/403/429 alert or include audit-log evidence in the release packet.

AlertConditionThresholdFirst response
InfoConnectAPIDownAPI downup{job="infoconnect-api"} == 0 for 2 minutesCheck GET /health, container status, and API logs.
InfoConnectRedisDownRedis downinfoconnect_component_health{component="redis"} == 0 for 2 minutesCheck Redis container health and broker connectivity.
InfoConnectQdrantDownQdrant downinfoconnect_component_health{component="qdrant"} == 0 for 2 minutesCheck Qdrant container health and collection availability.
InfoConnectWorkerDownworker downQueue backlog exists while task throughput is zero for 10 minutes, firing after 5 minutesCheck Celery worker containers, queue bindings, and recent task logs.
InfoConnectHighErrorRatehigh error rate5-minute HTTP 5xx ratio above 5% for 10 minutesInspect recent 5xx responses and route-specific logs.
InfoConnectHighLatencyhigh latencyRoute p95 latency above 2 seconds for 10 minutesCheck slow endpoints, Qdrant latency, rerank usage, and host pressure.
InfoConnectIndexingFailureSpikeindexing failure spike3 or more indexing failures in 15 minutesInspect failed Celery task logs and recent uploaded documents.
InfoConnectQueueBacklogGrowinggrowing queue backlogAny Redis queue above 100 jobs with a positive 15-minute slopeCheck worker availability and embedding throughput before scaling.
InfoConnectHighDiskUsagehigh disk usageHost disk usage above 85% for 15 minutesClear safe temp files or expand storage before Qdrant/Redis writes fail.
InfoConnectHighMemoryUsagehigh memory usageHost memory usage above 85% for 15 minutesCheck API and worker memory; scale replicas before raising embedding concurrency.
InfoConnectAPIContainerMemoryWarningAPI container memory highcAdvisor usage above 85% of its limit for 5 minutesCheck cgroup current, anon, swap, and pressure for each API replica.
InfoConnectAPIContainerMemoryCriticalAPI container memory criticalcAdvisor usage above 95% of its limit for 1 minuteInspect API cgroup and process memory; do not raise the limit as a substitute for bounding work.
InfoConnectAPIContainerSwapGrowingAPI container swap growingAny positive 5-minute swap delta (immediate)Compare swap, anon, reclaim, and PSI; preserve cgroup evidence.
InfoConnectAPIContainerOOMKillAPI container OOM killAny cAdvisor OOM event (immediate)Preserve kernel and cgroup OOM evidence; check for a container restart.
InfoConnectAPIContainerRestartAPI container restartAny Docker container start event in 5 minutes, including initial starts (immediate)Confirm whether the restart was an approved deployment; correlate Docker and kernel logs.
InfoConnectMemoryPressureWarningAPI container memory pressure warningmemory_pressure_warning with outcome=warning in Loki (immediate)Inspect container-scoped memory and swap before the limit is reached.
InfoConnectPublicationAuthorityUnavailablepublication serving unavailableinfoconnect_publication_runtime{metric="serving_ready"} < 1 for 30 secondsRead serving is unsafe; keep search fail-closed and follow the publication recovery runbook.
InfoConnectPublicationPublishingDegradedpublication publishing degradedpublishing_ready == 0 and ignoring(metric) serving_ready == 1 for 1 minuteReads remain available; inspect fence overlap, owners, and pending acknowledgements before restarting anything.
InfoConnectPublicationAOFUnhealthypublication AOF unhealthyinfoconnect_publication_runtime{metric="aof_healthy"} < 1 for 30 secondsCheck Redis AOF and publication persistence configuration.
InfoConnectPublicationLowRedisHeadroompublication Redis headroom lowinfoconnect_publication_runtime{metric="redis_headroom_bytes"} < 134217728 for 5 minutesStop publishers and restore Redis capacity before authority writes fail.
InfoConnectPublicationDurabilityFailurepublication durability barrier failedAny increase in infoconnect_publication_waitaof_failures_total (immediate)Keep affected clients fenced and follow lost-result recovery.
InfoConnectPublicationInvariantViolationpublication invariant violationAny increase in infoconnect_publication_invariant_violations_total (immediate)Keep the authority fenced; inspect invariant diagnostics.
InfoConnectPublicationFenceStuckpublication fence stuckinfoconnect_publication_runtime{metric="fence_age_seconds"} > 30 for 1 minuteCheck leases and worker fence/convergence acknowledgements.
InfoConnectPublicationWorkerACKMissingpublication worker acknowledgement missinginfoconnect_publication_pending_acks > 0 for 30 secondsCheck captured workers; activation requires every ACK.
InfoConnectPublicationJournalGrowthpublication journal growthinfoconnect_publication_runtime{metric="journal_entries"} > 100000 for 15 minutesApply the documented backup-aware retention procedure.

Container Memory Alerts​

The Prometheus rules in monitoring/prometheus-alerts.yml use cAdvisor job infoconnect-cadvisor and the Docker Compose label container_label_com_docker_compose_service="api". Scrape every API replica and retain that label. The rules compare container_memory_usage_bytes with container_spec_memory_limit_bytes; swap growth and OOM events use container_memory_swap and container_oom_events_total. The alert file defines rules only; it does not configure or deploy cAdvisor.

The warning fires above 85% of the container limit for 5 minutes. Critical fires above 95% for 1 minute or when swap grows over 5 minutes. OOM alerts fire without a pending period; restart detection uses the Docker event stream below. For diagnosis, capture cgroup current/max, memory.stat anon, swap current/max, memory.events max/oom/oom_kill, /proc/1/status RSS, and memory.pressure PSI at 15–30 second intervals from an external collector.

monitoring/loki-alerts.yml routes the structured memory_pressure_warning event with outcome=warning to Alertmanager as service=api, severity=warning. Load it in Loki Ruler and keep the existing service=api Alertmanager route to infoconnect-ops-primary. This rule requires the existing log shipping and Loki Ruler path; this repository change does not deploy a collector, load production rules, or test a notification.

InfoConnectAPIContainerRestart lives in monitoring/loki-alerts.yml. A later approved deployment must continuously ship Docker Engine events from every Docker host to Loki, with stream labels job="infoconnect-docker-events", compose_service from Actor.Attributes["com.docker.compose.service"], instance for the host, and container from Actor.ID. Preserve the JSON event body and its event timestamp. The source is Docker's event stream, for example docker events --filter type=container --format '{{json .}}'. Container stdout shipping alone does not include these events. The shipper needs Docker Engine read access; persist events and monitor collection gaps, because Docker only retains its last 256 events for replay.

The rule fires without a pending period on Type="container", Action="start". This detects automatic restarts and manual stop/start sequences even when the container ID and creation time stay unchanged. Initial starts and deployment recreates also alert; correlate them with the approved change window. cAdvisor's container_start_time_seconds is Docker's creation time and cannot detect a same-container restart. Detection latency includes shipping and Ruler evaluation; the event remains eligible for 5 minutes. No Docker event collector is installed or enabled by this repository change. Until this prerequisite and the Loki Ruler rule are configured, restart alerting is unavailable.

Container OOM And Restart Diagnosis​

docker inspect cannot show OOM history: State.ExitCode and State.OOMKilled reset on every container start, so a running container that was OOM-killed an hour ago reports exit=0 oom=false. The durable evidence lives in the kernel log (Memory cgroup out of memory: Killed process ...) and in dockerd's restarting container ... exitCode=137 journal lines.

Run the host-side check on the Docker host to summarize both surfaces:

uv run python scripts/check_container_oom.py --since "24 hours ago"
uv run python scripts/check_container_oom.py --syslog /var/log/syslog --json

The script exits non-zero when it finds a kernel OOM kill or an exitCode=137 restart in the window, so it can also run from cron or an external health checker as a poor-man's OOM alert. Treat exitCode=137 as an unexpected-kill signal and correlate it with the kernel OOM lines — a forced SIGKILL produces 137 too. exitCode=3 restarts are uvicorn startup failures (dependency not ready), a different failure class.

When a container OOMs, compare the victim's anon-rss against its Compose memory limit (PROD_API_MEMORY, PROD_QDRANT_MEMORY, ...) from .runtime/prod.env, then regenerate the plan with just prod-runtime-plan rather than hand-editing the generated file.

Alert Recipients And Escalation​

Primary on-call: Alertmanager receiver infoconnect-ops-primary. Secondary escalation: Alertmanager receiver infoconnect-platform-secondary. Incident commander: Alertmanager receiver infoconnect-incident-command. Route critical alerts to infoconnect-ops-primary immediately; page infoconnect-platform-secondary if the alert remains firing for 15 minutes or if client isolation, indexing, Redis, Qdrant, or publication availability is affected; add infoconnect-incident-command for unresolved critical alerts after 30 minutes or any client-impacting outage.

Before go-live, bind those receiver names to the active UAT or production rota in Alertmanager and paste the rota link into the release evidence packet. Confirm that service=api routes warning and critical alerts to infoconnect-ops-primary, and that service=worker, service=redis, service=qdrant, service=publication, and service=host reach the same primary receiver.

Test-Fire Evidence​

Test-fire every Prometheus and Loki alert before UAT sign-off by lowering its threshold in a temporary rule copy or by using Alertmanager's silence-safe test receiver. Capture the firing rule state, Alertmanager notification, and operator acknowledgement. Store each evidence artifact with the release packet using this naming pattern: go-live-alert-<alert-name>-<environment>-<date>.md.

Record these checks for all alerts: InfoConnectAPIDown, InfoConnectRedisDown, InfoConnectQdrantDown, InfoConnectWorkerDown, InfoConnectHighErrorRate, InfoConnectHighLatency, InfoConnectIndexingFailureSpike, InfoConnectQueueBacklogGrowing, InfoConnectHighDiskUsage, InfoConnectHighMemoryUsage, InfoConnectAPIContainerMemoryWarning, InfoConnectAPIContainerMemoryCritical, InfoConnectAPIContainerSwapGrowing, InfoConnectAPIContainerOOMKill, InfoConnectAPIContainerRestart, InfoConnectMemoryPressureWarning, InfoConnectPublicationAuthorityUnavailable, InfoConnectPublicationPublishingDegraded, InfoConnectPublicationAOFUnhealthy, InfoConnectPublicationLowRedisHeadroom, InfoConnectPublicationDurabilityFailure, InfoConnectPublicationInvariantViolation, InfoConnectPublicationFenceStuck, InfoConnectPublicationWorkerACKMissing, and InfoConnectPublicationJournalGrowth.