Admin Logs

Operator-facing Logs tab in the GENIE.AI admin UI — VictoriaLogs-backed log search, summaries, and the MELT seam.

The Logs tab in the admin dashboard (reachable at https://<NGINX_PUBLIC_DOMAIN>/admin → “Logs”) is the operator-facing surface for searching container logs across all GENIE.AI services. It is backed by VictoriaLogs via the backend’s MELT seam (MELT_PROVIDER=victorialogs, an abstraction layer in components/shared/lib/melt/ named for Metrics / Events / Logs / Traces — a future-proof seam that lets the storage backend swapped without changing call sites), with graceful degradation when VL is unreachable.

This page covers what the Logs tab does, the five endpoints behind it, the env vars that govern behaviour, and how to debug common VL failures.

Architecture

flowchart LR
  subgraph services[Services with logging: *fluent-logging]
    S1[backend / document-repository<br/>+ all OPEA services]
  end
  S1 -- stdout/stderr<br/>tag=genie.<container_name> --> FC[OTel Collector<br/>:24224 fluent_forward]
  FC -- OTLP HTTP<br/>logs pipeline --> VL[VictoriaLogs<br/>:9428]
  subgraph backend[Node.js backend]
    API[/api/admin/logs/*/]
    LogsService --> MELT[shared/lib/melt<br/>victorialogs-client.js]
    MELT -- /select/logsql/{query,hits} --> VL
  end
  API -- GET search/summary --> LogsService
  VL -. VlUnavailableError .-> API

The Logs tab never talks to VL directly — every read goes through the backend’s MELT seam (components/shared/lib/melt/victorialogs-client.js). Ingestion also goes through the OTel Collector (no direct jsonline POST from the app code). When VL is unreachable the read path throws a typed VlUnavailableError that surfaces to the API as HTTP 503 — see Failure modes.

Prerequisites

  • Admin role in Keycloak (the JWT must contain realm_access.roles including admin; see Configuration → Keycloak admin guide).
  • Observability stack on (or VL reachable on VICTORIALOGS_URL). When the observability profile is off, VL is still started as a core service (see the env file’s Section 12D: ADMIN LOGS MIGRATION) — but Grafana / dashboards are not provisioned.
  • For non-admin operators: see the read-only Service Logs Grafana dashboard under General → Service Logs in Observability → Dashboards.

What the Logs tab is for

The Logs tab gives an operator a single search box for the structured Winston JSON envelopes emitted by every Node.js and Python service, plus the OTel Collector (services with logging: *fluent-logging in docker-compose.yaml). Three concrete workflows:

  1. “Did my last re-ingest finish cleanly?” — pick dataprep-arango-service in the service dropdown (the Logs tab shows the actual VL service name — genie.<stack>_dataprep-arango-service_<n> for a Compose deployment), choose a date range, look for failed entries.
  2. “Why did the login fail for user X?” — pick backend in the service dropdown (VL name: genie.<stack>_backend_<n>), search for the user’s Keycloak sub claim.
  3. “Was VictoriaLogs itself healthy during yesterday’s outage?” — pick victorialogs in the service dropdown and use the “Yesterday” preset. Note: the OTel Collector container does not use the fluentd logging driver and does not export its own logs via OTLP, so its records do not land in VictoriaLogs.

Where service names come from: every container that runs with logging: *fluent-logging ships stdout/stderr to the OTel Collector under the tag genie.<container_name> (Compose container name format: <project>_<service>_<index>, e.g. genieai_backend_1). The Collector forwards to VictoriaLogs, which stores the tag as the service.name resource attribute. The Logs tab’s service dropdown is populated from those _stream.service values.

Workflow decision tree

Map your question to the right dropdown + filter combination:

flowchart TD
  Q{What do you need?} -->|Did ingest finish?| A[service=dataprep-arango-service<br/>dateRange=today or yesterday<br/>term=failed]
  Q -->|Why did login fail?| B[service=backend<br/>dateRange=today<br/>term=Keycloak sub claim]
  Q -->|Was VL healthy?| C[service=victorialogs<br/>dateRange=yesterday<br/>no term filter]
  Q -->|Generic error hunt?| D[level=ERROR<br/>dateRange=week<br/>leave service=all]
  Q -->|Specific term across all services| E[term=substring<br/>dateRange=week<br/>leave service=all]

Endpoints

All five endpoints live under /api/admin/logs and require a Keycloak JWT with the admin realm role.

MethodPathPurpose
GET/api/admin/logsPaginated list of recent logs. Query params: limit (default 100, max 10000), level (TRACE | DEBUG | INFO | WARN | ERROR | FATAL), service (any _stream.service value present in VL — typically the fluentd-derived name genie.<container_name>).
GET/api/admin/logs/summaryError / Warning counts grouped by service.
GET/api/admin/logs/searchFull-text search with date-range presets. Query params: term (substring match on the log message body — _msg field only, not level/service), level, service, dateRange (today | yesterday | week | month | custom), startDate, endDate.
GET/api/admin/logs/debug-yesterdayYesterday-only diagnostic dump (used by the Logs tab’s “Yesterday” shortcut).
POST/api/admin/logs/rolloverDeprecated. Returns 410 Gone for cron User-Agents (cron, curl, wget, httpie, python-requests, python-urllib, go-http-client) and 200 {"deprecated":true} for admin browser clients. See the deprecation note below.

Example: search for errors in the backend over the last week

ADMIN_TOKEN=...  # Get via the Keycloak master admin recipe

# `service=backend` matches the VL service.name `genie.<stack>_backend_<n>`
# (the Logs tab translates the dropdown label back to that exact value).
# Use `level=error` (lowercase is fine — the API uppercases via _normalizeLevelFilter).
curl -sk -H "Authorization: Bearer $ADMIN_TOKEN" \
  "https://<NGINX_PUBLIC_DOMAIN>/api/admin/logs/search?level=error&service=backend&dateRange=week&term=ECONNREFUSED" \
  | jq '.logs[] | {ts: .timestamp, level: .level, service: .service, msg: .message}'

Wire-level contract (VictoriaLogs HTTP API)

The backend’s shared/lib/melt/victorialogs-client.js is the read-side client for the Logs tab. It speaks VL’s /select/logsql/query and /select/logsql/hits endpoints. Ingestion goes through the OTel Collector (via logger.emit(...) in components/shared/lib/victorialogs-transport.js), not via a direct jsonline POST.

  • VL base URL — VICTORIALOGS_URL (Ansible default http://victorialogs:9428).
  • VL image — victoriametrics/victoria-logs:v1.50.0 (docker-compose.yaml:1834). The 1.50+ matters because that release introduced the canonical AccountID + ProjectID tenant headers; older VL builds ignore the multi-tenant header.
  • Tenant header — VICTORIALOGS_TENANT_ID (default 0:0; format AccountID:ProjectID per VL 1.50+ canonical tenant header).
  • Query timeout — VL_QUERY_TIMEOUT_MS (default 30000 ms; raised for 7-day security scans that aggregate more rows).
  • Time field — _time. Every log line carries an ISO-8601 timestamp.
  • Envelope parsing gotcha — for Node.js services, VL’s _msg field is the raw Winston JSON envelope. VictoriaLogsAdapter._normalizeRows (shared/lib/melt/victorialogs-client.js:237) projects _msg into the row-level message field. The Logs tab reads message, not _msg, so the TYPE column shows the human-readable text — never the literal JSON envelope {"level":"info","message":...}.

VictoriaLogs field reference

The fields below are VL-internal — VictoriaLogsAdapter rewrites most of them before the row reaches the Logs tab, so deployers usually do not see the raw names. They appear here for grep/LogSQL queries run directly against VL (e.g. from inside the VL container):

FieldSourceMeaning
_msgRaw log line as receivedFor Node services, this is the Winston JSON envelope (see gotcha above). For OTel-ingested lines, the rendered log body.
_stream.serviceContainer tag (genie.<container_name>)The service the row belongs to. The Logs tab’s service dropdown is populated from this field.
_stream.levelWinston level or OTel severityinfo, warn, error, debug. May be empty for some sources — see the level filter failure-mode row.
_timeIngest timestampISO-8601 nanosecond timestamp; every log line carries one.
LogSQLVL query languageThe pipe-filter SQL-like language used by /select/logsql/query and /select/logsql/hits. query=* selects everything; query=service:backend AND level:error filters. See VL docs.
AccountID:ProjectIDMultitenancy header (VICTORIALOGS_TENANT_ID)Required from VL 1.50+; sends the tenant header on every HTTP call. Format AccountID:ProjectID; default 0:0.

Configuration

VictoriaLogs has no environment opt-out — it is always-on because the fluentd driver ships container logs to it unconditionally and the admin logs UI queries it for backend endpoints. The in-app OTel SDK init is controlled separately by ENABLE_OBSERVABILITY (see Observability → Configuration).

Env varDefaultPurpose
MELT_PROVIDERvictorialogs (hardcoded constant in components/shared/lib/melt/index.js)Future-proof seam selector. Whitelisted by tests/config-validator/ so a deployer can override the env file; the runtime constant is the actual current value. Today there is only one backend implementation.
VICTORIALOGS_URLhttp://victorialogs:9428 (set by Ansible env.j2:241)VL HTTP base URL. There is no in-code default — the VictoriaLogsClient constructor must receive baseURL (it gets undefined if you bypass Ansible and forget to inject this env var).
VICTORIALOGS_TENANT_ID0:0 (read by shared/lib/melt/victorialogs-client.js:105)Tenant header for /select/logsql/* (AccountID:ProjectID).
VL_QUERY_TIMEOUT_MS30000 (hardcoded docker-compose.yaml:580,648)axios timeout for VL query and hits methods. Raise for long scans. Lower for fast-fail dashboards.
ENABLE_OBSERVABILITY0 (docker-compose.yaml:592)Gate for the OTel SDK self-telemetry. The Logs tab itself stays functional regardless — VL + OTel Collector + LogsService do not depend on this flag.

Tunables worth knowing (not env vars, but operator-facing knobs in components/gov-chat-backend/services/logs-service.js):

  • VL health probe is lazy on first call: 3 retries × 5 s backoff (HEALTH_PROBE_ATTEMPTS, HEALTH_PROBE_BACKOFF_MS in shared/lib/melt/victorialogs-client.js). Constructor does not block on VL reachability.
  • VL-unreachable warning logging is rate-limited to 1 per minute per host (VL_UNREACHABLE_LOG_COOLDOWN_MS = 60_000, persisted to /tmp/vl-unreachable-ts so backend restarts do not reset the cadence).
  • VL mode has no date-range cap (LogSQL is unbounded — restrict the date range explicitly if a query times out).

Failure modes and debugging

SymptomCauseFix
All /api/admin/logs/* calls return 503 {error: 'vl_unreachable', message}VictoriaLogs is unreachable — LogsService._vlOrThrow re-throws connection-class errors (ECONNREFUSED, ENOTFOUND, ETIMEDOUT, ECONNABORTED, VictoriaLogsHealthError, 5xx upstream) as a typed VlUnavailableError carrying statusCode: 503 + the documented body. The global error middleware reads err.statusCode + err.body and renders them verbatim.Check docker service logs <stack>_victorialogs --since 5m (Swarm) or docker compose logs victorialogs --since 5m (Compose). VL disk full? → check the storage dashboard (VICTORIALOGS_RETENTION may be too generous). The first failure per minute is logged with [opName] VictoriaLogs unreachable: … — subsequent ones are rate-limited.
/api/admin/logs/summary returns all-zero counts even though services are loggingVL is healthy but the service logs aren’t reaching itCheck service.name in the Service Logs dashboard for the same window — if empty, the affected service may be missing the logging: *fluent-logging directive in docker-compose.yaml. Out of 37 services in docker-compose.yaml, 35 use the fluentd logging driver; the rest use default Docker logging and do not land in VL.
/api/admin/logs/search returns rows but level filter does nothingThe query param is one of the six allowed levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL) but the rows’ _stream.level is empty or non-canonicalVL _msg is the raw Winston JSON envelope. VictoriaLogsAdapter._normalizeRows projects _msg into message and pulls level from fields.level (falling back to _stream.level, then INFO). Inspect raw rows with curl -sk "http://victorialogs:9428/select/logsql/query?query=*&limit=1".
Backend cannot reach VICTORIALOGS_URL from the container (no baseURL configured, or DNS failure)The first VL request triggers the health-probe retries, then _vlOrThrow re-throws as VlUnavailableError (HTTP 503)From inside the backend container: docker exec <stack>_backend wget -qO- "http://victorialogs:9428/health". If that succeeds but the Logs tab still fails, check the in-container env: docker exec <stack>_backend printenv VICTORIALOGS_URL — if empty, your .env is missing the var (Ansible normally injects it).
Cron still hitting /api/admin/logs/rollover and getting 410Old crontab entry not removedRemove the cron job — log rollover is no longer required (logs are written directly to VL). The User-Agent regex that triggers the 410 is /\b(?:cron|curl|wget|httpie|python-requests|python-urllib|go-http-client)\b/.
Backend OOM during wide scansVL query returning huge result setsLower VL_QUERY_TIMEOUT_MS to fail fast, or restrict the dateRange preset (today / yesterday / week are bounded; month may be too wide for noisy services; custom requires explicit startDate + endDate).

What happens when VL is unreachable

LogsService._vlOrThrow (static method) classifies connection-class failures into a typed VlUnavailableError. Connection errors caught:

  • VictoriaLogsHealthError (raised by the client when the health-probe retries are exhausted)
  • ECONNREFUSED, ENOTFOUND, ETIMEDOUT, ECONNABORTED (Node network errors)
  • 5xx upstream responses from VL

The thrown VlUnavailableError carries:

err.statusCode === 503
err.body      === { error: 'vl_unreachable', message: '<human-readable>' }

The global error middleware in components/gov-chat-backend/index.js reads err.statusCode + err.body and renders them verbatim, so the API contract is 503 + {error: 'vl_unreachable', message}.

Validation / programmer errors (bad query params, etc.) propagate unchanged so the route layer keeps its existing 400 / 500 semantics.

Carve-out for /api/admin/system-health. The AdminDashboardService.getSystemHealth() path deliberately does not call _vlOrThrow for the VL hits query. The error-rate tile is one derived metric of a larger health payload, and the dashboard stays well-formed (errorRate: 0, a warn log line) on a VL outage rather than 503-ing the whole health response. All other VL read paths in LogsService and AdminDashboardService.debugYesterdayLogs funnel through _vlOrThrow.

Why VL is always-on. The fluentd logging driver is configured on every container regardless of the observability profile, so container logs reach VL unconditionally. Removing VL would break log shipping entirely — the only configurable opt-out is the in-app SDK (ENABLE_OBSERVABILITY).

Verify it worked

Before trusting the Logs tab in production, run through this checklist once on a fresh deployment:

  1. VL is reachable from the backend container

    docker exec <stack>_backend wget -qO- "http://victorialogs:9428/health"
    # → "OK"
    
  2. VL is receiving logs

    # Wait ~30s for the OTel Collector to flush the first batch, then:
    curl -sk "http://victorialogs:9428/select/logsql/query?query=*&limit=5"
    # → a non-empty JSON array; each row has _stream.service, _time, _msg
    
  3. The Logs tab returns rows, not a vl_unreachable error

    Open https://<NGINX_PUBLIC_DOMAIN>/admin → Logs tab, run a search with the “Yesterday” preset. You should see rows; the TYPE column should show human-readable text (e.g. error — Login failed), never the literal JSON envelope {"level":"info","message":...}.

  4. The admin role is enforced

    # Without an admin token: 401/403
    curl -sk -w "%{http_code}\n" \
      "https://<NGINX_PUBLIC_DOMAIN>/api/admin/logs?limit=1"
    
  5. The rollover deprecation behaves as expected

    # Cron-style caller gets 410
    curl -sk -o /dev/null -w "%{http_code}\n" \
      -X POST -H "User-Agent: curl/8.0.0" \
      "https://<NGINX_PUBLIC_DOMAIN>/api/admin/logs/rollover"
    # → 410
    
    # Browser / admin client (no cron-style User-Agent) gets 200 + deprecated:true
    curl -sk -X POST \
      -H "Authorization: Bearer $ADMIN_TOKEN" \
      -H "Content-Type: application/json" \
      -H "User-Agent: Mozilla/5.0 (admin-ui)" \
      "https://<NGINX_PUBLIC_DOMAIN>/api/admin/logs/rollover"
    # → {"deprecated":true}
    

If any of these fails, cross-reference with Failure modes and debugging.

Deprecation of rollover

POST /api/admin/logs/rollover is deprecated and intentionally returns 410 Gone for cron callers. The user-agent check (regex against cron|curl|wget|httpie|python-requests|python-urllib|go-http-client) exists so that:

  • Browser / admin clients see 200 {"deprecated":true} so the UI can display a friendly “no longer needed” message without breaking.
  • Old cron jobs see 410 Gone so they fail loudly and are removed (rather than silently succeeding forever against a no-op endpoint).

If you still have a crontab entry calling /api/admin/logs/rollover, remove it. Logs are written to VL directly — no rollover step is required.