VictoriaLogs integration and a shared Integrations settings section #218

Closed
opened 2026-11-03 15:27:39 +01:00 by omp-deck · 0 comments
Collaborator

Problem

Agents can read Docker container logs through the Portainer integration. They have no way to query centralised logs. Add a read-only VictoriaLogs integration that copies the Portainer pattern:

  • one instance, configured in Settings, with a credential that can be written but is never returned;
  • a per-workspace opt-in;
  • read-only agent tools with typed result cards.

Settings currently gives every integration its own top-level section. Add an Integrations section that holds both the Portainer and VictoriaLogs panels. Forgejo keeps its own section.

Decided requirements:

  • Auth mode: none, bearer (Authorization: Bearer <token>) or basic (username plus password). Secrets can be written but are never returned.
  • Tenancy: optional AccountID / ProjectID. When set, they are sent as request headers on every call.
  • Tools: victorialogs_get_info, victorialogs_query, victorialogs_hits, victorialogs_field_names, victorialogs_field_values. All are read-only.
  • Query defaults: window is the last 1h; limit defaults to 200 and is capped at 5000. Large results are saved to local://victorialogs/…, with the last 200 lines returned inline.
  • Gating: a per-workspace victorialogsEnabled flag, off by default. Restricted (trigger) sessions treat these tools like the Portainer tools.

Plan

Context

The template is the Portainer integration:

  • apps/server/src/portainer/{config,client}.ts and tools/{shared,index,docker}.ts
  • apps/server/src/routes-portainer.ts
  • the workspace column from migration 021-workspace-portainer.sql
  • web: PortainerPanel.tsx, PortainerTool.tsx with tools/portainer/*View.tsx, preview-portainer.ts
  • protocol: portainer-tools.ts

Build a parallel victorialogs module, reusing the shared helpers rather than copying them. Then merge the two settings panels into one Integrations section.

Approach

Step 1: Protocol types (packages/protocol/src)

  • In index.ts (next to PortainerConfigResponse, around line 2073), add:
    export type VictoriaLogsAuthMode = "none" | "bearer" | "basic";
    export interface VictoriaLogsConfigResponse { url: string | null; authMode: VictoriaLogsAuthMode; username: string | null; hasSecret: boolean; accountId: string | null; projectId: string | null; configured: boolean }
    export interface UpdateVictoriaLogsConfigRequest { url: string; authMode: VictoriaLogsAuthMode; username?: string; /** token (bearer) or password (basic); omit to keep the stored one (only when url and authMode are unchanged) */ secret?: string; accountId?: string; projectId?: string }
    export interface VictoriaLogsTestResponse { ok: boolean; version?: string; error?: string }
    
    configured means: the URL is set, and either authMode === "none" or a secret is stored (for basic, the username must also be set).
  • Add victorialogsEnabled: boolean to Workspace (around line 60) and WorkspaceDetails (around line 96). Add victorialogsEnabled?: boolean to UpdateWorkspaceRequest (around line 120).
  • Create victorialogs-tools.ts with the result details types, and re-export it with export type * from "./victorialogs-tools"; next to the Portainer re-export (line 26):
    • VictoriaLogsInfoResult { version: string | null; url: string; tenant: string | null }
    • VictoriaLogsQueryResult { query: string; start: string; end: string; rows: number; limit: number; limited: boolean; bytes: number; snapshot: string | null; tail: string }. limited means rows === limit.
    • VictoriaLogsHitsResult { query: string; start: string; end: string; step: string; total: number; buckets: { time: string; hits: number }[] }
    • VictoriaLogsFieldsResult { query: string; field: string | null; values: { value: string; hits: number }[] }. Shared by field_names (field null) and field_values.

Step 2: Migration and workspace flag

  • Create apps/server/src/db/migrations/023-workspace-victorialogs.sql:
    -- Per-workspace switch for registering VictoriaLogs tools in sessions; off by default.
    ALTER TABLE workspaces ADD COLUMN victorialogs_enabled INTEGER NOT NULL DEFAULT 0;
    
  • In apps/server/src/db/workspaces.ts:
    • add victorialogs_enabled: number to the row type and victorialogsEnabled: r.victorialogs_enabled === 1 to the mapper;
    • append , victorialogs_enabled to WORKSPACE_COLUMNS;
    • add setWorkspaceVictoriaLogsEnabled(workspaceId, enabled), modelled on setWorkspacePortainerEnabled (line 163).
  • In workspace-details.ts (line 66), add victorialogsEnabled: ws.victorialogsEnabled.
  • In the routes-workspaces.ts PATCH handler (lines 110–127):
    • accept a boolean victorialogsEnabled (400 "victorialogsEnabled must be a boolean" otherwise);
    • change the empty-body check to "name, portainerEnabled or victorialogsEnabled is required";
    • call setWorkspaceVictoriaLogsEnabled with the same 404 handling.
  • In apps/web/src/lib/api.ts, add setWorkspaceVictoriaLogs(id, enabled), modelled on setWorkspacePortainer.

Step 3: Config and client (apps/server/src/victorialogs/)

  • config.ts, modelled on portainer/config.ts, using the same app_settings getters and setters.
    • Keys: victorialogs.url, victorialogs.auth_mode, victorialogs.username, victorialogs.secret, victorialogs.account_id, victorialogs.project_id.
    • URL validation: reuse normalizePortainerUrl by moving it unchanged to a shared apps/server/src/integrations/url.ts as normalizeIntegrationUrl. Update its importers (grep -rn normalizePortainerUrl apps).
    • Exports:
      • readVictoriaLogsConfig(): VictoriaLogsConfig | null, server-side only and including the secret;
      • readVictoriaLogsConfigPublic(): VictoriaLogsConfigResponse;
      • readStoredVictoriaLogs();
      • resolveSaveVictoriaLogs(input, stored);
      • saveVictoriaLogsConfig(...);
      • clearVictoriaLogsConfig();
      • VictoriaLogsConfigError.
    • Rules in resolveSaveVictoriaLogs:
      • authMode must be one of the three values;
      • basic requires a non-empty username;
      • a missing secret reuses the stored secret only when url and authMode are unchanged; otherwise it is an error ("a token/password is required when the URL or auth mode changes");
      • for none, any stored secret and username are deleted on save;
      • accountId / projectId, when present, must match /^\d+$/; an empty string clears the value.
  • client.ts: VictoriaLogsClient, read-only like PortainerClient.
    • It has only GET methods: getJson<T>(path, {query, signal}) and getBytes(path, {query, maxBytes, signal}).
    • It returns the same ApiResult / BytesResult shapes (import those types from ../portainer/client.ts) and uses readBodyCapped / errorMessageFromBody from ../forgejo/client.ts.
    • Default timeout is 20s.
    • Headers: the auth header for the mode; AccountID / ProjectID when set.
    • redact(text) scrubs the secret and the base64 basic credential.
    • getVersion(signal): GET /metrics, then take version="…" from the vm_app_version line. Return null if the request fails or there is no match. (The metric name is unverified — confirm first against a real instance. If it is absent, look for vl_app_version too.)
  • Verification call: used by both save and test. GET /select/logsql/field_names with query=* and start=5m.
    • 401/403 → {status: 401, error: "VictoriaLogs rejected the credentials (<status>): <msg>"}
    • status 0 → 502 "could not reach <url>: <msg>"
    • any other failure → 502 "unexpected response from <url>: <status> <msg>"

Step 4: REST routes

  • Create apps/server/src/routes-victorialogs.ts with buildVictoriaLogsRouter(), modelled on routes-portainer.ts:
    • GET /victorialogs/config
    • PUT /victorialogs/config: verify, then save; 400/401/502 as in Portainer
    • POST /victorialogs/test: {ok:false,error:"VictoriaLogs is not configured"} when unconfigured; otherwise {ok:true, version?}
    • DELETE /victorialogs/config
  • Register it in routes.ts after buildPortainerRouter() (line 291).
  • Add apps/web/src/lib/victorialogs-api.ts, modelled on portainer-api.ts.

Step 5: Tools (apps/server/src/victorialogs/tools/)

  • shared.ts: defineVictoriaLogsTool, modelled on definePortainerTool.
    • Prefix victorialogs_; mutating: false; context { client: VictoriaLogsClient }.
    • Errors thrown by a tool are redacted with client.redact.
    • Reuse apiError / errorResult / textResult / withCap / MAX_OUTPUT_BYTES from forgejo/tools/shared.ts, and parseTime from portainer/tools/docker.ts.
    • Time params accept Unix seconds, ISO-8601, or a VictoriaLogs relative duration such as 5m or 2h; durations pass through as-is.
  • logs.ts exports VICTORIALOGS_TOOLS:
    • victorialogs_get_info (no params): returns VictoriaLogs <version|?> · <url> · tenant <a:p|default>.
    • victorialogs_query {query: string (LogsQL), start?, end?, limit?: int}:
      • Defaults: start = now−1h, end = now. limit is clamped to 1–5000 (default 200).
      • Request: GET /select/logsql/query with getBytes and maxBytes: SNAPSHOT_READ_BYTES. The body is JSON lines.
      • Output text: one line per row, <_time> <_stream> <_msg>; extra fields are rendered as key=value.
      • Spill rule, copied from portainer_container_logs (localRoot, writeFileAtomic, path victorialogs/<slug(query)>-<stamp>.log):
        • output ≤ MAX_OUTPUT_BYTES → return inline;
        • otherwise → write the snapshot and return a header line plus the last 200 lines.
      • When rows === limit, append [limit reached: raise limit or narrow the time range].
    • victorialogs_hits {query, start?, end?, step?: string (default "5m"), field?: string}:
      • GET /select/logsql/hits.
      • Without field: return the bucket counts (time hits lines) plus the total.
      • With field: return one group per field value.
    • victorialogs_field_names {query? (default "*"), start?, end?}: GET /select/logsql/field_names. Return name (hits) lines.
    • victorialogs_field_values {field, query? (default "*"), start?, end?, limit?: int (default 100, max 1000)}: GET /select/logsql/field_values.
    • Every description ends with Read-only.. victorialogs_get_info's description says to call it first and points to the LogsQL docs URL https://docs.victoriametrics.com/victorialogs/logsql/.
  • index.ts: VICTORIALOGS_TOOL_DEFS, VICTORIALOGS_TOOL_NAMES, and buildVictoriaLogsTools(workspaceId), modelled on portainer/tools/index.ts: it returns no tools unless the workspace has victorialogsEnabled and readVictoriaLogsConfig() is non-null, and it never throws.

Step 6: Session wiring

  • In bridge/in-process.ts, at both call sites (around lines 304 and 445):
    • call buildVictoriaLogsTools with the same workspace id and spread the tools into customTools;
    • change restrictedToolOptions to take extraToolNames: readonly string[] instead of portainer: boolean;
    • at both call sites, pass [...(portainerTools.length ? PORTAINER_TOOL_NAMES : []), ...(vlTools.length ? VICTORIALOGS_TOOL_NAMES : [])].
  • In bridge/session-policy.ts decidePolicy (line 630), change the Portainer "default" early return so it also returns "default" for VICTORIALOGS_TOOL_NAMES.

Step 7: Settings — the Integrations section

  • In SettingsView.tsx SECTIONS:
    • replace the portainer entry with { id: "integrations", label: "Integrations", description: "Portainer, VictoriaLogs and other read-only data sources" };
    • render <IntegrationsPanel /> for it;
    • update the StubSection exclusion union (replace "portainer" with "integrations");
    • in normalizeSection, map the legacy value "portainer" to "integrations" before the lookup, so old links still work.
  • Create components/settings/IntegrationsPanel.tsx: a vertical stack of titled cards (<h2> Portainer + PortainerPanel, <h2> VictoriaLogs + VictoriaLogsPanel). PortainerPanel stays as it is.
  • Create components/settings/VictoriaLogsPanel.tsx, modelled on PortainerPanel.tsx:
    • fields: URL; auth mode select; username (only for basic); a secret field labelled "Token" or "Password" with an empty placeholder "stored, leave blank to keep" when hasSecret; Account ID and Project ID;
    • buttons: Save / Test / Disconnect, with the same notices.
  • In WorkspacesPanel.tsx:
    • add a VictoriaLogs <dt>/<dd> row after Portainer (line 258) with the checkbox "Give sessions VictoriaLogs tools", a "VictoriaLogs not configured" hint, and the same "applies to…" note;
    • load victorialogsApi.getConfig() next to the Portainer config fetch (line 25).

Step 8: Tool renderers

  • Create components/tools/VictoriaLogsTool.tsx and summarizeVictoriaLogsArgs, modelled on PortainerTool.tsx. Reuse PortainerShell / PortainerViewProps from tools/portainer/ui.tsx; no new shell.
    • victorialogs_query → a view modelled on portainer/LogsView.tsx: a header showing the rows/limit/snapshot path, then the tail in a pre block.
    • victorialogs_hits → a bucket table.
    • field_names / field_values → a value/hits table.
    • get_info → a key/value list.
    • Put the views in components/tools/victorialogs/.
    • The summary is the query, truncated to 80 chars (for field_values: <field> · <query>).
  • In ToolCallCard.tsx (line 306), add if (name.startsWith("victorialogs_")) return <VictoriaLogsTool {...props} />; and wire the summary next to the Portainer one.
  • Add apps/web/src/preview-victorialogs.ts with one fixture per tool, typed with the protocol types. Register it in Preview.tsx as a "VictoriaLogs tools" section, modelled on lines 250 and 373.

Critical files & anchors

  • apps/server/src/bridge/in-process.ts: restrictedToolOptions (line 164) and its two callers. Without these changes trigger sessions never see the new tools.
  • apps/server/src/bridge/session-policy.ts:630: the unattended policy short-circuit.
  • apps/server/src/portainer/tools/docker.ts:225-289: the spill/snapshot pattern that victorialogs_query copies.
  • apps/web/src/views/SettingsView.tsx:35-48, 104, 1390, 1438: the section list, the dispatch, the stub union, and normalizeSection.

Verification

  • Unit tests (bun):
    • apps/server/src/victorialogs/config.test.ts: resolveSaveVictoriaLogs rules (basic without username → error; missing secret after a URL change → error; switching to none clears the secret; non-numeric accountId → error).
    • client.test.ts: against a Bun.serve fake, modelled on portainer/client.test.ts.
      • Bearer mode sends Authorization: Bearer t; basic mode sends Basic base64(u:p).
      • Tenant headers are present when configured.
      • An error body that echoes the secret comes back as ***.
    • tools/logs.test.ts, modelled on portainer/tools/docker.test.ts.
      • buildVictoriaLogsTools(ws.id) returns [] until the workspace flag is set and config is saved, then returns 5 tools.
      • A fake /select/logsql/query returning 3 JSON lines gives 3 text rows.
      • A response bigger than MAX_OUTPUT_BYTES writes local://victorialogs/… and returns the last 200 lines.
      • limit: 99999 sends limit=5000.
    • routes-victorialogs.test.ts, modelled on routes-portainer.test.ts: PUT with a rejected credential → 401 and nothing stored; a good one → 200, and the GET body contains neither the secret nor the password.
  • Run: bun test apps/server/src/victorialogs apps/server/src/routes-victorialogs.test.ts apps/server/src/portainer apps/server/src/routes-portainer.test.ts, then the repo typecheck (bun run typecheck at the repo root; if that script does not exist, use the root package.json equivalent).
  • Manual:
    • Start the deck, then open /settings?section=portainer. It shows the Integrations section with both cards.
    • Configure a VictoriaLogs instance (e.g. docker run -p 9428:9428 victoriametrics/victoria-logs, auth none) → Save succeeds, and Test shows a version.
    • Enable it on one workspace, then start a session there and ask the agent to run victorialogs_get_info and victorialogs_query with query: "*". Both render cards.
    • A session in a workspace without the flag has no victorialogs_* tools.
    • /preview shows the "VictoriaLogs tools" fixtures.

Assumptions & contingencies

  • VictoriaLogs API paths and response shapes (/select/logsql/{query,hits,field_names,field_values}, JSON lines for query, {"values":[{"value","hits"}]} for fields, {"hits":[{"fields","timestamps","values","total"}]} for hits) are taken from the upstream docs and are unverified — confirm first against the instance version in use. If a shape differs, adapt the parser in logs.ts only; the tool contracts above stay the same.
  • If /metrics is not reachable (for example, blocked by the proxy), version is null and the text shows ?. That is not an error.
## Problem Agents can read Docker container logs through the Portainer integration. They have no way to query centralised logs. Add a read-only **VictoriaLogs** integration that copies the Portainer pattern: - one instance, configured in Settings, with a credential that can be written but is never returned; - a per-workspace opt-in; - read-only agent tools with typed result cards. Settings currently gives every integration its own top-level section. Add an **Integrations** section that holds both the Portainer and VictoriaLogs panels. Forgejo keeps its own section. Decided requirements: - **Auth mode:** `none`, `bearer` (`Authorization: Bearer <token>`) or `basic` (username plus password). Secrets can be written but are never returned. - **Tenancy:** optional `AccountID` / `ProjectID`. When set, they are sent as request headers on every call. - **Tools:** `victorialogs_get_info`, `victorialogs_query`, `victorialogs_hits`, `victorialogs_field_names`, `victorialogs_field_values`. All are read-only. - **Query defaults:** window is the last 1h; `limit` defaults to 200 and is capped at 5000. Large results are saved to `local://victorialogs/…`, with the last 200 lines returned inline. - **Gating:** a per-workspace `victorialogsEnabled` flag, off by default. Restricted (trigger) sessions treat these tools like the Portainer tools. ## Plan ### Context The template is the Portainer integration: - `apps/server/src/portainer/{config,client}.ts` and `tools/{shared,index,docker}.ts` - `apps/server/src/routes-portainer.ts` - the workspace column from migration `021-workspace-portainer.sql` - web: `PortainerPanel.tsx`, `PortainerTool.tsx` with `tools/portainer/*View.tsx`, `preview-portainer.ts` - protocol: `portainer-tools.ts` Build a parallel `victorialogs` module, reusing the shared helpers rather than copying them. Then merge the two settings panels into one Integrations section. ### Approach **Step 1: Protocol types** (`packages/protocol/src`) - In `index.ts` (next to `PortainerConfigResponse`, around line 2073), add: ```ts export type VictoriaLogsAuthMode = "none" | "bearer" | "basic"; export interface VictoriaLogsConfigResponse { url: string | null; authMode: VictoriaLogsAuthMode; username: string | null; hasSecret: boolean; accountId: string | null; projectId: string | null; configured: boolean } export interface UpdateVictoriaLogsConfigRequest { url: string; authMode: VictoriaLogsAuthMode; username?: string; /** token (bearer) or password (basic); omit to keep the stored one (only when url and authMode are unchanged) */ secret?: string; accountId?: string; projectId?: string } export interface VictoriaLogsTestResponse { ok: boolean; version?: string; error?: string } ``` `configured` means: the URL is set, and either `authMode === "none"` or a secret is stored (for `basic`, the username must also be set). - Add `victorialogsEnabled: boolean` to `Workspace` (around line 60) and `WorkspaceDetails` (around line 96). Add `victorialogsEnabled?: boolean` to `UpdateWorkspaceRequest` (around line 120). - Create `victorialogs-tools.ts` with the result `details` types, and re-export it with `export type * from "./victorialogs-tools";` next to the Portainer re-export (line 26): - `VictoriaLogsInfoResult { version: string | null; url: string; tenant: string | null }` - `VictoriaLogsQueryResult { query: string; start: string; end: string; rows: number; limit: number; limited: boolean; bytes: number; snapshot: string | null; tail: string }`. `limited` means rows === limit. - `VictoriaLogsHitsResult { query: string; start: string; end: string; step: string; total: number; buckets: { time: string; hits: number }[] }` - `VictoriaLogsFieldsResult { query: string; field: string | null; values: { value: string; hits: number }[] }`. Shared by `field_names` (`field` null) and `field_values`. **Step 2: Migration and workspace flag** - Create `apps/server/src/db/migrations/023-workspace-victorialogs.sql`: ```sql -- Per-workspace switch for registering VictoriaLogs tools in sessions; off by default. ALTER TABLE workspaces ADD COLUMN victorialogs_enabled INTEGER NOT NULL DEFAULT 0; ``` - In `apps/server/src/db/workspaces.ts`: - add `victorialogs_enabled: number` to the row type and `victorialogsEnabled: r.victorialogs_enabled === 1` to the mapper; - append `, victorialogs_enabled` to `WORKSPACE_COLUMNS`; - add `setWorkspaceVictoriaLogsEnabled(workspaceId, enabled)`, modelled on `setWorkspacePortainerEnabled` (line 163). - In `workspace-details.ts` (line 66), add `victorialogsEnabled: ws.victorialogsEnabled`. - In the `routes-workspaces.ts` PATCH handler (lines 110–127): - accept a boolean `victorialogsEnabled` (400 `"victorialogsEnabled must be a boolean"` otherwise); - change the empty-body check to `"name, portainerEnabled or victorialogsEnabled is required"`; - call `setWorkspaceVictoriaLogsEnabled` with the same 404 handling. - In `apps/web/src/lib/api.ts`, add `setWorkspaceVictoriaLogs(id, enabled)`, modelled on `setWorkspacePortainer`. **Step 3: Config and client** (`apps/server/src/victorialogs/`) - **`config.ts`**, modelled on `portainer/config.ts`, using the same `app_settings` getters and setters. - Keys: `victorialogs.url`, `victorialogs.auth_mode`, `victorialogs.username`, `victorialogs.secret`, `victorialogs.account_id`, `victorialogs.project_id`. - URL validation: reuse `normalizePortainerUrl` by moving it unchanged to a shared `apps/server/src/integrations/url.ts` as `normalizeIntegrationUrl`. Update its importers (`grep -rn normalizePortainerUrl apps`). - Exports: - `readVictoriaLogsConfig(): VictoriaLogsConfig | null`, server-side only and including the secret; - `readVictoriaLogsConfigPublic(): VictoriaLogsConfigResponse`; - `readStoredVictoriaLogs()`; - `resolveSaveVictoriaLogs(input, stored)`; - `saveVictoriaLogsConfig(...)`; - `clearVictoriaLogsConfig()`; - `VictoriaLogsConfigError`. - Rules in `resolveSaveVictoriaLogs`: - `authMode` must be one of the three values; - `basic` requires a non-empty `username`; - a missing `secret` reuses the stored secret only when url and authMode are unchanged; otherwise it is an error (`"a token/password is required when the URL or auth mode changes"`); - for `none`, any stored secret and username are deleted on save; - `accountId` / `projectId`, when present, must match `/^\d+$/`; an empty string clears the value. - **`client.ts`**: `VictoriaLogsClient`, read-only like `PortainerClient`. - It has only GET methods: `getJson<T>(path, {query, signal})` and `getBytes(path, {query, maxBytes, signal})`. - It returns the same `ApiResult` / `BytesResult` shapes (import those types from `../portainer/client.ts`) and uses `readBodyCapped` / `errorMessageFromBody` from `../forgejo/client.ts`. - Default timeout is 20s. - Headers: the auth header for the mode; `AccountID` / `ProjectID` when set. - `redact(text)` scrubs the secret and the base64 basic credential. - `getVersion(signal)`: GET `/metrics`, then take `version="…"` from the `vm_app_version` line. Return null if the request fails or there is no match. (The metric name is `unverified — confirm first` against a real instance. If it is absent, look for `vl_app_version` too.) - **Verification call:** used by both save and test. GET `/select/logsql/field_names` with `query=*` and `start=5m`. - 401/403 → `{status: 401, error: "VictoriaLogs rejected the credentials (<status>): <msg>"}` - status 0 → `502 "could not reach <url>: <msg>"` - any other failure → `502 "unexpected response from <url>: <status> <msg>"` **Step 4: REST routes** - Create `apps/server/src/routes-victorialogs.ts` with `buildVictoriaLogsRouter()`, modelled on `routes-portainer.ts`: - `GET /victorialogs/config` - `PUT /victorialogs/config`: verify, then save; 400/401/502 as in Portainer - `POST /victorialogs/test`: `{ok:false,error:"VictoriaLogs is not configured"}` when unconfigured; otherwise `{ok:true, version?}` - `DELETE /victorialogs/config` - Register it in `routes.ts` after `buildPortainerRouter()` (line 291). - Add `apps/web/src/lib/victorialogs-api.ts`, modelled on `portainer-api.ts`. **Step 5: Tools** (`apps/server/src/victorialogs/tools/`) - **`shared.ts`**: `defineVictoriaLogsTool`, modelled on `definePortainerTool`. - Prefix `victorialogs_`; `mutating: false`; context `{ client: VictoriaLogsClient }`. - Errors thrown by a tool are redacted with `client.redact`. - Reuse `apiError` / `errorResult` / `textResult` / `withCap` / `MAX_OUTPUT_BYTES` from `forgejo/tools/shared.ts`, and `parseTime` from `portainer/tools/docker.ts`. - Time params accept Unix seconds, ISO-8601, or a VictoriaLogs relative duration such as `5m` or `2h`; durations pass through as-is. - **`logs.ts`** exports `VICTORIALOGS_TOOLS`: - **`victorialogs_get_info`** (no params): returns `VictoriaLogs <version|?> · <url> · tenant <a:p|default>`. - **`victorialogs_query`** `{query: string (LogsQL), start?, end?, limit?: int}`: - Defaults: `start` = now−1h, `end` = now. `limit` is clamped to 1–5000 (default 200). - Request: GET `/select/logsql/query` with `getBytes` and `maxBytes: SNAPSHOT_READ_BYTES`. The body is JSON lines. - Output text: one line per row, `<_time> <_stream> <_msg>`; extra fields are rendered as ` key=value`. - Spill rule, copied from `portainer_container_logs` (`localRoot`, `writeFileAtomic`, path `victorialogs/<slug(query)>-<stamp>.log`): - output ≤ `MAX_OUTPUT_BYTES` → return inline; - otherwise → write the snapshot and return a header line plus the last 200 lines. - When rows === limit, append `[limit reached: raise limit or narrow the time range]`. - **`victorialogs_hits`** `{query, start?, end?, step?: string (default "5m"), field?: string}`: - GET `/select/logsql/hits`. - Without `field`: return the bucket counts (`time hits` lines) plus the total. - With `field`: return one group per field value. - **`victorialogs_field_names`** `{query? (default "*"), start?, end?}`: GET `/select/logsql/field_names`. Return `name (hits)` lines. - **`victorialogs_field_values`** `{field, query? (default "*"), start?, end?, limit?: int (default 100, max 1000)}`: GET `/select/logsql/field_values`. - Every description ends with `Read-only.`. `victorialogs_get_info`'s description says to call it first and points to the LogsQL docs URL `https://docs.victoriametrics.com/victorialogs/logsql/`. - **`index.ts`**: `VICTORIALOGS_TOOL_DEFS`, `VICTORIALOGS_TOOL_NAMES`, and `buildVictoriaLogsTools(workspaceId)`, modelled on `portainer/tools/index.ts`: it returns no tools unless the workspace has `victorialogsEnabled` and `readVictoriaLogsConfig()` is non-null, and it never throws. **Step 6: Session wiring** - In `bridge/in-process.ts`, at both call sites (around lines 304 and 445): - call `buildVictoriaLogsTools` with the same workspace id and spread the tools into `customTools`; - change `restrictedToolOptions` to take `extraToolNames: readonly string[]` instead of `portainer: boolean`; - at both call sites, pass `[...(portainerTools.length ? PORTAINER_TOOL_NAMES : []), ...(vlTools.length ? VICTORIALOGS_TOOL_NAMES : [])]`. - In `bridge/session-policy.ts` `decidePolicy` (line 630), change the Portainer `"default"` early return so it also returns `"default"` for `VICTORIALOGS_TOOL_NAMES`. **Step 7: Settings — the Integrations section** - In `SettingsView.tsx` `SECTIONS`: - replace the `portainer` entry with `{ id: "integrations", label: "Integrations", description: "Portainer, VictoriaLogs and other read-only data sources" }`; - render `<IntegrationsPanel />` for it; - update the `StubSection` exclusion union (replace `"portainer"` with `"integrations"`); - in `normalizeSection`, map the legacy value `"portainer"` to `"integrations"` before the lookup, so old links still work. - Create `components/settings/IntegrationsPanel.tsx`: a vertical stack of titled cards (`<h2>` Portainer + `PortainerPanel`, `<h2>` VictoriaLogs + `VictoriaLogsPanel`). `PortainerPanel` stays as it is. - Create `components/settings/VictoriaLogsPanel.tsx`, modelled on `PortainerPanel.tsx`: - fields: URL; auth mode select; username (only for basic); a secret field labelled "Token" or "Password" with an empty placeholder "stored, leave blank to keep" when `hasSecret`; Account ID and Project ID; - buttons: Save / Test / Disconnect, with the same notices. - In `WorkspacesPanel.tsx`: - add a `VictoriaLogs` `<dt>/<dd>` row after Portainer (line 258) with the checkbox "Give sessions VictoriaLogs tools", a "VictoriaLogs not configured" hint, and the same "applies to…" note; - load `victorialogsApi.getConfig()` next to the Portainer config fetch (line 25). **Step 8: Tool renderers** - Create `components/tools/VictoriaLogsTool.tsx` and `summarizeVictoriaLogsArgs`, modelled on `PortainerTool.tsx`. Reuse `PortainerShell` / `PortainerViewProps` from `tools/portainer/ui.tsx`; no new shell. - `victorialogs_query` → a view modelled on `portainer/LogsView.tsx`: a header showing the rows/limit/snapshot path, then the tail in a pre block. - `victorialogs_hits` → a bucket table. - `field_names` / `field_values` → a value/hits table. - `get_info` → a key/value list. - Put the views in `components/tools/victorialogs/`. - The summary is the query, truncated to 80 chars (for `field_values`: `<field> · <query>`). - In `ToolCallCard.tsx` (line 306), add `if (name.startsWith("victorialogs_")) return <VictoriaLogsTool {...props} />;` and wire the summary next to the Portainer one. - Add `apps/web/src/preview-victorialogs.ts` with one fixture per tool, typed with the protocol types. Register it in `Preview.tsx` as a "VictoriaLogs tools" section, modelled on lines 250 and 373. ### Critical files & anchors - `apps/server/src/bridge/in-process.ts`: `restrictedToolOptions` (line 164) and its two callers. Without these changes trigger sessions never see the new tools. - `apps/server/src/bridge/session-policy.ts:630`: the unattended policy short-circuit. - `apps/server/src/portainer/tools/docker.ts:225-289`: the spill/snapshot pattern that `victorialogs_query` copies. - `apps/web/src/views/SettingsView.tsx:35-48, 104, 1390, 1438`: the section list, the dispatch, the stub union, and `normalizeSection`. ### Verification - **Unit tests (bun):** - `apps/server/src/victorialogs/config.test.ts`: `resolveSaveVictoriaLogs` rules (basic without username → error; missing secret after a URL change → error; switching to `none` clears the secret; non-numeric accountId → error). - `client.test.ts`: against a `Bun.serve` fake, modelled on `portainer/client.test.ts`. - Bearer mode sends `Authorization: Bearer t`; basic mode sends `Basic base64(u:p)`. - Tenant headers are present when configured. - An error body that echoes the secret comes back as `***`. - `tools/logs.test.ts`, modelled on `portainer/tools/docker.test.ts`. - `buildVictoriaLogsTools(ws.id)` returns `[]` until the workspace flag is set and config is saved, then returns 5 tools. - A fake `/select/logsql/query` returning 3 JSON lines gives 3 text rows. - A response bigger than `MAX_OUTPUT_BYTES` writes `local://victorialogs/…` and returns the last 200 lines. - `limit: 99999` sends `limit=5000`. - `routes-victorialogs.test.ts`, modelled on `routes-portainer.test.ts`: PUT with a rejected credential → 401 and nothing stored; a good one → 200, and the GET body contains neither the secret nor the password. - **Run:** `bun test apps/server/src/victorialogs apps/server/src/routes-victorialogs.test.ts apps/server/src/portainer apps/server/src/routes-portainer.test.ts`, then the repo typecheck (`bun run typecheck` at the repo root; if that script does not exist, use the root `package.json` equivalent). - **Manual:** - Start the deck, then open `/settings?section=portainer`. It shows the Integrations section with both cards. - Configure a VictoriaLogs instance (e.g. `docker run -p 9428:9428 victoriametrics/victoria-logs`, auth none) → Save succeeds, and Test shows a version. - Enable it on one workspace, then start a session there and ask the agent to run `victorialogs_get_info` and `victorialogs_query` with `query: "*"`. Both render cards. - A session in a workspace without the flag has no `victorialogs_*` tools. - `/preview` shows the "VictoriaLogs tools" fixtures. ### Assumptions & contingencies - VictoriaLogs API paths and response shapes (`/select/logsql/{query,hits,field_names,field_values}`, JSON lines for query, `{"values":[{"value","hits"}]}` for fields, `{"hits":[{"fields","timestamps","values","total"}]}` for hits) are taken from the upstream docs and are `unverified — confirm first` against the instance version in use. If a shape differs, adapt the parser in `logs.ts` only; the tool contracts above stay the same. - If `/metrics` is not reachable (for example, blocked by the proxy), `version` is null and the text shows `?`. That is not an error.
lucas closed this issue 2026-11-03 16:24:50 +01:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/omp-deck#218
No description provided.