VictoriaLogs integration and a shared Integrations settings section #218
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck#218
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
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:
none,bearer(Authorization: Bearer <token>) orbasic(username plus password). Secrets can be written but are never returned.AccountID/ProjectID. When set, they are sent as request headers on every call.victorialogs_get_info,victorialogs_query,victorialogs_hits,victorialogs_field_names,victorialogs_field_values. All are read-only.limitdefaults to 200 and is capped at 5000. Large results are saved tolocal://victorialogs/…, with the last 200 lines returned inline.victorialogsEnabledflag, 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}.tsandtools/{shared,index,docker}.tsapps/server/src/routes-portainer.ts021-workspace-portainer.sqlPortainerPanel.tsx,PortainerTool.tsxwithtools/portainer/*View.tsx,preview-portainer.tsportainer-tools.tsBuild a parallel
victorialogsmodule, 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)index.ts(next toPortainerConfigResponse, around line 2073), add:configuredmeans: the URL is set, and eitherauthMode === "none"or a secret is stored (forbasic, the username must also be set).victorialogsEnabled: booleantoWorkspace(around line 60) andWorkspaceDetails(around line 96). AddvictorialogsEnabled?: booleantoUpdateWorkspaceRequest(around line 120).victorialogs-tools.tswith the resultdetailstypes, and re-export it withexport 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 }.limitedmeans 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 byfield_names(fieldnull) andfield_values.Step 2: Migration and workspace flag
apps/server/src/db/migrations/023-workspace-victorialogs.sql:apps/server/src/db/workspaces.ts:victorialogs_enabled: numberto the row type andvictorialogsEnabled: r.victorialogs_enabled === 1to the mapper;, victorialogs_enabledtoWORKSPACE_COLUMNS;setWorkspaceVictoriaLogsEnabled(workspaceId, enabled), modelled onsetWorkspacePortainerEnabled(line 163).workspace-details.ts(line 66), addvictorialogsEnabled: ws.victorialogsEnabled.routes-workspaces.tsPATCH handler (lines 110–127):victorialogsEnabled(400"victorialogsEnabled must be a boolean"otherwise);"name, portainerEnabled or victorialogsEnabled is required";setWorkspaceVictoriaLogsEnabledwith the same 404 handling.apps/web/src/lib/api.ts, addsetWorkspaceVictoriaLogs(id, enabled), modelled onsetWorkspacePortainer.Step 3: Config and client (
apps/server/src/victorialogs/)config.ts, modelled onportainer/config.ts, using the sameapp_settingsgetters and setters.victorialogs.url,victorialogs.auth_mode,victorialogs.username,victorialogs.secret,victorialogs.account_id,victorialogs.project_id.normalizePortainerUrlby moving it unchanged to a sharedapps/server/src/integrations/url.tsasnormalizeIntegrationUrl. Update its importers (grep -rn normalizePortainerUrl apps).readVictoriaLogsConfig(): VictoriaLogsConfig | null, server-side only and including the secret;readVictoriaLogsConfigPublic(): VictoriaLogsConfigResponse;readStoredVictoriaLogs();resolveSaveVictoriaLogs(input, stored);saveVictoriaLogsConfig(...);clearVictoriaLogsConfig();VictoriaLogsConfigError.resolveSaveVictoriaLogs:authModemust be one of the three values;basicrequires a non-emptyusername;secretreuses 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");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 likePortainerClient.getJson<T>(path, {query, signal})andgetBytes(path, {query, maxBytes, signal}).ApiResult/BytesResultshapes (import those types from../portainer/client.ts) and usesreadBodyCapped/errorMessageFromBodyfrom../forgejo/client.ts.AccountID/ProjectIDwhen set.redact(text)scrubs the secret and the base64 basic credential.getVersion(signal): GET/metrics, then takeversion="…"from thevm_app_versionline. Return null if the request fails or there is no match. (The metric name isunverified — confirm firstagainst a real instance. If it is absent, look forvl_app_versiontoo.)/select/logsql/field_nameswithquery=*andstart=5m.{status: 401, error: "VictoriaLogs rejected the credentials (<status>): <msg>"}502 "could not reach <url>: <msg>"502 "unexpected response from <url>: <status> <msg>"Step 4: REST routes
apps/server/src/routes-victorialogs.tswithbuildVictoriaLogsRouter(), modelled onroutes-portainer.ts:GET /victorialogs/configPUT /victorialogs/config: verify, then save; 400/401/502 as in PortainerPOST /victorialogs/test:{ok:false,error:"VictoriaLogs is not configured"}when unconfigured; otherwise{ok:true, version?}DELETE /victorialogs/configroutes.tsafterbuildPortainerRouter()(line 291).apps/web/src/lib/victorialogs-api.ts, modelled onportainer-api.ts.Step 5: Tools (
apps/server/src/victorialogs/tools/)shared.ts:defineVictoriaLogsTool, modelled ondefinePortainerTool.victorialogs_;mutating: false; context{ client: VictoriaLogsClient }.client.redact.apiError/errorResult/textResult/withCap/MAX_OUTPUT_BYTESfromforgejo/tools/shared.ts, andparseTimefromportainer/tools/docker.ts.5mor2h; durations pass through as-is.logs.tsexportsVICTORIALOGS_TOOLS:victorialogs_get_info(no params): returnsVictoriaLogs <version|?> · <url> · tenant <a:p|default>.victorialogs_query{query: string (LogsQL), start?, end?, limit?: int}:start= now−1h,end= now.limitis clamped to 1–5000 (default 200)./select/logsql/querywithgetBytesandmaxBytes: SNAPSHOT_READ_BYTES. The body is JSON lines.<_time> <_stream> <_msg>; extra fields are rendered askey=value.portainer_container_logs(localRoot,writeFileAtomic, pathvictorialogs/<slug(query)>-<stamp>.log):MAX_OUTPUT_BYTES→ return inline;[limit reached: raise limit or narrow the time range].victorialogs_hits{query, start?, end?, step?: string (default "5m"), field?: string}:/select/logsql/hits.field: return the bucket counts (time hitslines) plus the total.field: return one group per field value.victorialogs_field_names{query? (default "*"), start?, end?}: GET/select/logsql/field_names. Returnname (hits)lines.victorialogs_field_values{field, query? (default "*"), start?, end?, limit?: int (default 100, max 1000)}: GET/select/logsql/field_values.Read-only..victorialogs_get_info's description says to call it first and points to the LogsQL docs URLhttps://docs.victoriametrics.com/victorialogs/logsql/.index.ts:VICTORIALOGS_TOOL_DEFS,VICTORIALOGS_TOOL_NAMES, andbuildVictoriaLogsTools(workspaceId), modelled onportainer/tools/index.ts: it returns no tools unless the workspace hasvictorialogsEnabledandreadVictoriaLogsConfig()is non-null, and it never throws.Step 6: Session wiring
bridge/in-process.ts, at both call sites (around lines 304 and 445):buildVictoriaLogsToolswith the same workspace id and spread the tools intocustomTools;restrictedToolOptionsto takeextraToolNames: readonly string[]instead ofportainer: boolean;[...(portainerTools.length ? PORTAINER_TOOL_NAMES : []), ...(vlTools.length ? VICTORIALOGS_TOOL_NAMES : [])].bridge/session-policy.tsdecidePolicy(line 630), change the Portainer"default"early return so it also returns"default"forVICTORIALOGS_TOOL_NAMES.Step 7: Settings — the Integrations section
SettingsView.tsxSECTIONS:portainerentry with{ id: "integrations", label: "Integrations", description: "Portainer, VictoriaLogs and other read-only data sources" };<IntegrationsPanel />for it;StubSectionexclusion union (replace"portainer"with"integrations");normalizeSection, map the legacy value"portainer"to"integrations"before the lookup, so old links still work.components/settings/IntegrationsPanel.tsx: a vertical stack of titled cards (<h2>Portainer +PortainerPanel,<h2>VictoriaLogs +VictoriaLogsPanel).PortainerPanelstays as it is.components/settings/VictoriaLogsPanel.tsx, modelled onPortainerPanel.tsx:hasSecret; Account ID and Project ID;WorkspacesPanel.tsx: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;victorialogsApi.getConfig()next to the Portainer config fetch (line 25).Step 8: Tool renderers
components/tools/VictoriaLogsTool.tsxandsummarizeVictoriaLogsArgs, modelled onPortainerTool.tsx. ReusePortainerShell/PortainerViewPropsfromtools/portainer/ui.tsx; no new shell.victorialogs_query→ a view modelled onportainer/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.components/tools/victorialogs/.field_values:<field> · <query>).ToolCallCard.tsx(line 306), addif (name.startsWith("victorialogs_")) return <VictoriaLogsTool {...props} />;and wire the summary next to the Portainer one.apps/web/src/preview-victorialogs.tswith one fixture per tool, typed with the protocol types. Register it inPreview.tsxas 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 thatvictorialogs_querycopies.apps/web/src/views/SettingsView.tsx:35-48, 104, 1390, 1438: the section list, the dispatch, the stub union, andnormalizeSection.Verification
apps/server/src/victorialogs/config.test.ts:resolveSaveVictoriaLogsrules (basic without username → error; missing secret after a URL change → error; switching tononeclears the secret; non-numeric accountId → error).client.test.ts: against aBun.servefake, modelled onportainer/client.test.ts.Authorization: Bearer t; basic mode sendsBasic base64(u:p).***.tools/logs.test.ts, modelled onportainer/tools/docker.test.ts.buildVictoriaLogsTools(ws.id)returns[]until the workspace flag is set and config is saved, then returns 5 tools./select/logsql/queryreturning 3 JSON lines gives 3 text rows.MAX_OUTPUT_BYTESwriteslocal://victorialogs/…and returns the last 200 lines.limit: 99999sendslimit=5000.routes-victorialogs.test.ts, modelled onroutes-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.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 typecheckat the repo root; if that script does not exist, use the rootpackage.jsonequivalent)./settings?section=portainer. It shows the Integrations section with both cards.docker run -p 9428:9428 victoriametrics/victoria-logs, auth none) → Save succeeds, and Test shows a version.victorialogs_get_infoandvictorialogs_querywithquery: "*". Both render cards.victorialogs_*tools./previewshows the "VictoriaLogs tools" fixtures.Assumptions & contingencies
/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 areunverified — confirm firstagainst the instance version in use. If a shape differs, adapt the parser inlogs.tsonly; the tool contracts above stay the same./metricsis not reachable (for example, blocked by the proxy),versionis null and the text shows?. That is not an error.