On this page
Xodiac Agent — Plugin SDK
How collectors work, the contracts they must honor, and the checklist for
adding a new one. Read pkg/plugins/plugins.go first — it is the router.
Architecture
platform config (plugins[]) ──poll──▶ agent daemon
│ │
│ enabled plugin list + params │ conf.d/<name>.json creds
▼ ▼
RunAll() → VirtEntity[] CollectMetrics() → Metric[]
│ │
└──────── report payload ───────────┘
- Entities (
RunAll/RunOne) →virt_entitiestable → Virt page, topology tree,virt-subject incidents. - Metrics (
CollectMetrics) →metric_samples/metric_buckets→ per-device graphs +device_metricalert rules. - A plugin may emit both (docker/podman/snmptrap/snmp do).
Every plugin runs sandboxed: RunAll wraps each call in recover() + a
30 s context.WithTimeout. One bad plugin never blocks the report —
return an error and it is logged-and-skipped.
The two contracts
Entity collector
func runMyPlugin(ctx context.Context, creds json.RawMessage,
params map[string]any) ([]VirtEntity, error)
creds= contents ofconf.d/<name>.jsonon the host (may benilfor creds-less plugins likedocker,smartmon). Secrets live only here — never inparams(params come from the platform and are tenant-editable).params= the plugin'sparamsobject from platform config.- Return
[]VirtEntity;error→ logged + skipped.
type VirtEntity struct {
Kind string // vm|container|link|service|device|cluster|backup|security|disk|network …
ExtID string // stable unique id, e.g. "proxmox/node/pve1" — dedup key on platform
Name string // display name
ParentExt string // optional parent ExtID → topology edges
State string // running|stopped|degraded|stale|up|down… (free-form)
Meta map[string]any // JSON blob shown in the UI; numbers stay numeric
}
Metrics collector
Registered by name inside CollectMetrics (pkg/plugins/scrape.go) —
same creds/params, returns []collect.Metric:
type Metric struct {
Name string // dotted, sanitized to [a-zA-Z0-9_.] — "link.eth0.bytes_in"
Value float64
TS string // optional RFC3339; empty = server receipt time
}
Rates/counters: keep delta state in a package-level var guarded by a
mutex (see snmpmetrics.go) — the agent is a long-running daemon, so
in-memory baselines survive across report cycles. First sample after a
restart should emit nothing rather than a fake spike.
The declarative escape hatch — no Go required
Most HTTP JSON APIs need zero code:
custom_http— paramsurl/path/name_field/id_field/state_field/parent_field/meta_fieldsmap any JSON response to entities. Auth only viaconf.d/custom_http[.<cred_key>].json.metrics_scrape— params/conf.dendpoints[]scrape Prometheus text or flat JSON → metrics.custom_metrics— drop executables or.promfiles inconf.d/metrics.d/→ metrics.script— executables inconf.d/scripts/→ entities/metrics.
Only write Go when the API needs sessions, multi-step flows, binary protocols, or local system access.
The generic spec table
pkg/plugins/generic.go specs map covers ~200 services with ~4 lines
each — prefer extending it over a new file:
type spec struct {
kind, name string // entity kind + display name
exec []string // argv; first element must be on PATH
http string // JSON endpoint; "{{url}}" = params.url
httpAuth string // "basic" | "bearer" | "header:<Name>" | "query:<param>"
basicParse bool // parse k:v / k=v output lines into meta
tcpCmd string // tcp:// specs: line sent before reading stats
}
http may also be tcp://{{url}} for stats-over-TCP services
(memcached stats, ZooKeeper mntr, beanstalkd stats).
Credentials boundary (security contract — do not weaken)
- Secrets only from
conf.d/<name>.json. Params are platform-side and tenant-editable. credsURL()(plugins.go): when creds carry aurl, that url is the fetch target and auth attaches only to it —params.urlcan never redirect credentials. A creds file with secrets but nourlsends nothing to aparams.urltarget.custom_httpcred_keygives each authed endpoint its own conf.d file (custom_http.<key>.json).- Never log creds; never put them in
VirtEntity.Meta(mask as"***"). plugin_runremote action can only name the plugin — params always come from stored platform config.
Adding a collector — checklist
- Agent: implement
run<Name>(or aspecrow) inpkg/plugins/. - Register: add a
caseinrun()(plugins.go) — or rely onrunGenericfor spec-table names. - Metrics? add a branch in
CollectMetrics(scrape.go). - Platform catalog: add a
KNOWNentry inapp/Controllers/Api/Plugins.php(label, detect hints, params defaults, blurb) — without it the UI can't enable the plugin. - Docs: FEATURES.md plugin list + cookbook entry if it needs conf.d setup.
- Test:
xodiac-agent -test-pluginsruns every conf.d cred file locally and dumps entities/metrics — the cred-verification path. Unit-test pure parsing/delta logic (seesnmpmetrics_test.go,trap_test.go).
Conventions
- Context: honor
ctxfor network/exec (exec.CommandContext,http.NewRequestWithContext); the 30 s plugin timeout is the hard cap. - Bounded reads:
io.LimitReaderon HTTP bodies (1 MB) and exec output. - ExtID stability: derive from immutable ids (vmid, serial, ifIndex) — never from names that can be renamed.
- Meta values: strings/numbers/bools only (it round-trips as JSON).
- State:
runningfor healthy services;degradedfor partial faults (non-ok sensor, stale backup);stalefor old data.