src/go/plugin/go.d/docs/how-to-write-a-collector.md
This is the canonical starting point for new go.d collectors. New collectors MUST use framework V2. V1 collectors remain in the tree for compatibility and maintenance only.
For migrating an existing V1 collector, use src/go/plugin/go.d/docs/migrate-v1-to-v2.md instead. Migration is
compatibility work and has different rules from new collector authoring.
Use src/go/plugin/go.d/collector/cato_networks/ as the primary modern example. It is large, so copy the pattern, not
the whole shape. The useful references are called out below by responsibility.
Do the design work first:
src/go/plugin/go.d/docs/helper-packages.md.src/go/plugin/framework/docs/changing-framework-code.md instead of hiding it behind collector-local glue.src/go/pkg/topology/v1 and validate against
src/plugins.d/FUNCTION_TOPOLOGY_SCHEMA.json..agents/skills/integrations-lifecycle/consistency.md. Generated integration pages
and README symlinks are outputs, not hand-authored sources.Primary V2 reference:
src/go/plugin/go.d/collector/cato_networks/Read these files by responsibility:
collector.go: registration, defaults, public lifecycle methods, MetricStore(), ChartTemplateYAML(), and Function
wiring.config.go: config defaults, normalization, validation, and intentionally small public config.collect.go, collect_metrics.go, collect_bgp.go: collection orchestration and split domain operations.metrix.go, write_metrics.go, charts.yaml: typed instruments, metric writes, chart template, StateSet,
instances.by_labels, and label_promotion. Audit every instances.by_labels identity choice instead of copying
labels from the example blindly.host_scope.go: deterministic per-site V2 host scopes/vnodes.func_deps.go, catofunc/: Function subpackage boundary behind a narrow dependency interface.topology_store.go, topology.go, topology_test.go: immutable topology snapshot publishing and topology v1 schema
validation.config_test.go, collector_lifecycle_test.go, collector_collect_test.go, charts_test.go: table-driven V2 tests
and fixture validation.Framework/API references:
src/go/plugin/framework/collectorapi/collector.gosrc/go/plugin/framework/docs/changing-framework-code.mdsrc/go/plugin/go.d/docs/helper-packages.mdsrc/go/pkg/metrix/README.mdsrc/go/plugin/framework/charttpl/README.mdsrc/go/plugin/framework/chartengine/README.mdsrc/go/plugin/framework/functions/README.mdsrc/go/tools/functions-validation/README.md.agents/skills/project-writing-go-modules-framework-v2/go-v2-host-scope.md.agents/skills/integrations-lifecycle/consistency.mdStart with this layout and add focused files only when a responsibility needs its own boundary:
src/go/plugin/go.d/collector/<name>/
|-- collector.go # registration, New, public lifecycle, store/template
|-- init.go # Init helper methods for clients/matchers/state
|-- config.go # Config, defaults, validation
|-- collect.go # Collect orchestration
|-- metrix.go # typed metrix instruments built once in New
|-- write_metrics.go # normalized state -> metrix observations
|-- models.go # collector-local state/DTOs
|-- client.go # API/client boundary
|-- charts.yaml # V2 chart template
|-- config_schema.json # DYNCFG schema
|-- metadata.yaml # integration metadata source
|-- taxonomy.yaml # dashboard TOC placement source
|-- integrations/ # generated integration page
|-- README.md # symlink to generated integration page
|-- testdata/ # fixtures and config serialization files
`-- *_test.go # table-driven tests
Common optional splits:
Most collectors need none of these optional files. Add one only when the collector has the corresponding product surface
or state boundary; do not create empty host_scope.go, topology.go, or <name>func/ files just because Cato has
them.
init.go when Init() needs helper setup for clients, matchers, caches, or other persistent state. Keep the public
Init() method itself in collector.go; let it call focused helpers such as initClient() or initSiteSelector().collect_<operation>.go when the collector has multiple distinct collection operations, such as discovery, account
metrics, BGP, or inventory.normalize_<operation>.go when API payload normalization would otherwise dominate collect.go.host_scope.go when the collector emits generated vnodes.<name>func/ plus func_deps.go when the collector exposes Functions.topology.go and topology_store.go when the collector emits topology.Avoid files whose names hide their responsibility. For example, a file named diagnostics.go SHOULD NOT contain only
error classification.
New collectors MUST implement collectorapi.CollectorV2 from src/go/plugin/framework/collectorapi/collector.go and
register via CreateV2. In practice, collector.go should:
config_schema.json for JobConfigSchema;charts.yaml for ChartTemplateYAML();Config: func() any { return &Config{} };CreateV2;Methods and MethodHandler only when the collector has Functions.New() SHOULD own defaults and test seams:
metrix.NewCollectorStore();Public lifecycle and framework-contract methods MUST stay in collector.go:
Configuration() anyInit(context.Context) errorCheck(context.Context) errorCollect(context.Context) errorCleanup(context.Context)MetricStore() metrix.CollectorStoreChartTemplateYAML() stringCollectors that need a long-running side-effect loop MAY additionally implement collectorapi.CollectorV2Runner with
Run(context.Context) error. Use this only when work must start with the running job lifecycle but must not wait for
the next globally aligned Collect() tick, such as an agent-wide Function state refresh. The runtime starts Run()
only after the job starts, never during autodetection or DynCfg test, cancels it on stop, and waits for it before
Cleanup(). The implementation MUST return promptly after ctx.Done() and SHOULD make in-flight I/O cancellation-aware
where the underlying library allows it.
Init() validates config, prepares matchers/clients, and initializes persistent state. Explicit setup details SHOULD
live in helper methods, preferably in init.go, so the public method reads as the lifecycle sequence. Check() MUST be
a cheap auth/connectivity probe, not a full collection. Collect() MUST run the real write path through metrix.
Cleanup() closes idle connections and forwards Function cleanup.
Config SHOULD stay small and operator-oriented:
update_every, timeout, and vnode when relevant;Implementation tuning SHOULD use constants:
You MUST NOT add a config option just because it is easy to expose. Once shipped, it is hard to remove and MUST stay
synchronized across Config, config_schema.json, stock .conf, metadata, generated docs, and tests. A proposed
config option MUST name the concrete operator decision it enables; "operators may want to tune it" is not enough.
For SaaS/API credentials, examples SHOULD prefer secret indirection such as ${env:COLLECTOR_API_KEY} or
${file:/run/secrets/collector_api_key} instead of realistic-looking inline credentials. Schema fields that carry
secrets MUST be marked sensitive and use password-style UI handling where the schema supports it.
Selectors SHOULD use existing matcher packages such as src/go/pkg/matcher unless the upstream API forces a different
grammar. Document the exact matching input, for example "site name when present, otherwise site ID."
Collect() SHOULD stay orchestration, not a large parser. A typical flow is:
metrix.When the collector performs several upstream calls or collection operations, those operations SHOULD be split into
focused files named by operation, for example collect_metrics.go or collect_bgp.go. collect.go SHOULD explain the
cycle; the operation files SHOULD own the operation-specific API calls, fail-soft behavior, and merge rules.
Fail-soft behavior MUST be used only when partial data is still truthful. If one optional operation fails, log a rate-limited warning and omit or preserve only values that remain honest. If the core operation fails, return an error with context.
Collect() MUST preserve context cancellation. If the context is canceled during a partial path, Collect() MUST
return the context error so the runtime aborts the cycle instead of committing a stale or partial frame.
Metric instruments SHOULD be built once in New() when the metric surface is known. Use a typed collector metrics
struct so write code is a value mapping, not repeated dynamic instrument lookup.
Use the right instrument:
SnapshotGaugeVec for labeled current values; use scalar SnapshotMeter.Gauge only when the metric is intentionally
unlabeled;Counter.ObserveTotal() for source counters;StateSet SHOULD be used for fixed mutually exclusive states, such as connected vs disconnected or up vs down.charts.yaml is the chart contract. Every template MUST define:
version: v1;context_namespace;Templates SHOULD also group charts by operational area, use instances.by_labels for stable instance identity when
charts are entity-scoped, use label_promotion for descriptive labels that should not define uniqueness, and keep the
default lifecycle unless a concrete reason exists to override it.
Metric labels and chart instance labels MUST be bounded and stable. Use IDs for identity. Mutable display names SHOULD
be promoted with label_promotion. Do not blindly copy instances.by_labels from Cato or any other example; audit
every label used for chart identity and record why it is stable enough for that collector.
Use metrix.HostScope when one job emits data for remote entities that SHOULD appear as separate Netdata nodes. Labels
alone are not enough for that product semantics.
Rules:
ScopeKey and GUID are deterministic and based on stable IDs._vnode_type=<source> and useful source labels.Use .agents/skills/project-writing-go-modules-framework-v2/go-v2-host-scope.md for the framework contract.
Functions MUST NOT freely access collector internals. Put Function code in a dedicated subpackage, for example
<name>func/, with a narrow Deps interface declared by that subpackage.
Non-topology Function responses MUST conform to src/plugins.d/FUNCTION_UI_SCHEMA.json; topology Function responses
MUST conform to src/plugins.d/FUNCTION_TOPOLOGY_SCHEMA.json. Function payloads MUST be validated with
src/go/tools/functions-validation/ or an equivalent schema-validation test, and the validation method must be
recorded.
Pattern:
func_deps.go;Deps interface MUST expose only the methods the Function needs and MUST NOT expose, return, or
embed *Collector;Collector.Cleanup() forwards cleanup to the Function router.Use catofunc/ as the primary example. Test the Function package with fake deps so the boundary is compile-enforced.
New topology producers MUST use src/go/pkg/topology/v1. The non-v1 root src/go/pkg/topology payload model has been
retired and MUST NOT be reintroduced for topology payloads.
Rules:
src/go/plugin/go.d/collector/cato_networks/topology.go as the concrete construction reference for actors, links,
detail tables, and telemetry fields;topologyv1.ValidateDecodedData and
src/plugins.d/FUNCTION_TOPOLOGY_SCHEMA.json; see src/go/plugin/go.d/collector/cato_networks/topology_test.go
validateCatoTopologyV1Data for the full marshal/decode/schema check shape;.agents/skills/project-create-topology/SKILL.md for actor/link/table design.For a new collector <name>:
src/go/plugin/go.d/collector/<name>/.src/go/plugin/go.d/collector/init.go.src/go/plugin/go.d/config/go.d/<name>.conf.src/go/plugin/go.d/config/go.d.conf.src/go/plugin/go.d/README.md.src/health/health.d/<name>.conf only when alerts are useful and backed by emitted chart
contexts.src/go/plugin/go.d/config/go.d/sd/ or sdext, update generated
service-discovery documentation through the integrations lifecycle recipe.integrations/<slug>.md and the README symlink from metadata.yaml. Single-integration collector
directories normally use the symlinked README. Multi-integration plugin directories may keep a hand-authored umbrella
README; follow .agents/skills/integrations-lifecycle/consistency.md.Use .agents/skills/integrations-lifecycle/recipes/add-go-collector.md for the integration-generation commands and
taxonomy pipeline details.
The PR description or design note MUST enumerate the relevant collector consistency artifacts and justify every artifact that did not need a matching change. Most of this is not CI-enforced; it must be reviewer-visible.
Tests SHOULD be table-driven with map[string]struct{} when cases share setup and assertion shape.
Recommended test coverage:
collecttest.TestConfigurationSerialize;Init, cheap Check, Collect, Cleanup, MetricStore;collecttest.AssertChartTemplateSchema and chart-template compile validation
through the chartengine path used by nearby V2 collectors;collecttest.AssertChartCoverage;Do not let tests depend on real credentials or live services unless the test is explicitly an integration test gated outside the default unit-test path.
From src/go, run the narrow collector tests:
go test -count=1 ./plugin/go.d/collector/<name>/...
Verify that go.d can load the module:
timeout 15s go run ./cmd/godplugin -m <name> -d
Success means the module is registered, a job starts, and the command keeps running until the timeout stops it. Treat
unknown module, no jobs started, config-load errors, or an immediate exit before the timeout as failures. Use
-c <config-dir> when the test config lives outside the normal go.d config search path.
When the collector uses concurrency or Functions, also run:
go test -race -count=1 ./plugin/go.d/collector/<name>/...
When integration metadata, generated pages, taxonomy, or health alerts change, run the relevant integrations pipeline
checks from .agents/skills/integrations-lifecycle/.
Do not claim full-project validation from a narrow collector command. State exactly what was run.
Collect() map[string]int64.Check().Check() or Init(); use the optional V2 runner hook when polling must
be tied to the running job lifecycle.*Collector.README.md when the integration page should be generated.