Adding a Collector¶
This guide walks through adding a new collector to the OPNsense Exporter. The process involves five steps across three packages.
Overview¶
- Create the collector file in
internal/collector/ - Add an
init()function for auto-registration - Add a
Fetch*()method and data structs inopnsense/ - Add a disable flag in
internal/options/collectors.go - Wire the disable flag in
internal/collector/collector.goandmain.go - Update the docs: add
SubsystemDisplayNames+CollectorFlagsentries, then runmake docs
Step 1: Create the collector¶
Create a new file internal/collector/<subsystem>.go implementing the CollectorInstance interface:
package collector
import (
"log/slog"
"github.com/prometheus/client_golang/prometheus"
"github.com/rknightion/opnsense-exporter/opnsense"
)
type exampleCollector struct {
log *slog.Logger
instanceLabel string
// Define your metric descriptors
exampleGauge *prometheus.Desc
exampleTotal *prometheus.Desc
}
func init() {
collectorInstances = append(collectorInstances, &exampleCollector{})
}
func (c *exampleCollector) Name() string {
return "example" // Must match a subsystem constant
}
func (c *exampleCollector) Register(namespace, instance string, log *slog.Logger) {
c.log = log
c.instanceLabel = instance
c.exampleGauge = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "example", "value"),
"Description of the metric",
[]string{"label1", "label2"}, // variable labels
prometheus.Labels{
instanceLabelName: instance,
},
)
c.exampleTotal = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "example", "items_total"),
"Total number of example items",
nil,
prometheus.Labels{
instanceLabelName: instance,
},
)
}
func (c *exampleCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.exampleGauge
ch <- c.exampleTotal
}
func (c *exampleCollector) Update(
client *opnsense.Client,
ch chan<- prometheus.Metric,
) *opnsense.APICallError {
data, err := client.FetchExample()
if err != nil {
return err
}
// Emit the total count
ch <- prometheus.MustNewConstMetric(
c.exampleTotal,
prometheus.GaugeValue,
float64(len(data.Items)),
)
// Emit per-item metrics
for _, item := range data.Items {
ch <- prometheus.MustNewConstMetric(
c.exampleGauge,
prometheus.GaugeValue,
item.Value,
item.Label1,
item.Label2,
)
}
return nil
}
Step 2: Auto-registration¶
The init() function in the collector file (shown above) handles registration. It appends the collector instance to the global collectorInstances slice. No changes to any central registry file are needed.
Add a subsystem constant to internal/collector/collector.go:
Step 3: Add the API fetch method¶
Create or modify a file in opnsense/ to add the fetch method and data structures:
package opnsense
// ExampleData represents the API response structure
type ExampleData struct {
Items []ExampleItem `json:"items"`
}
type ExampleItem struct {
Label1 string `json:"label1"`
Label2 string `json:"label2"`
Value float64 `json:"value"`
}
// FetchExample retrieves example data from the OPNsense API
func (c *Client) FetchExample() (*ExampleData, *APICallError) {
var data ExampleData
url, ok := c.endpoints["example"]
if !ok {
return nil, &APICallError{
Endpoint: "example",
Err: ErrEndpointNotFound,
}
}
if err := c.do("GET", url, &data); err != nil {
return nil, &APICallError{
Endpoint: string(url),
Err: err,
}
}
return &data, nil
}
Register the endpoint in the client's endpoint map (in opnsense/client.go or the relevant setup):
Step 4: Add the disable flag¶
Add a new flag in internal/options/collectors.go:
exampleCollectorDisabled = kingpin.Flag(
"exporter.disable-example",
"Disable the scraping of example metrics",
).Envar("OPNSENSE_EXPORTER_DISABLE_EXAMPLE").Default("false").Bool()
Add the field to CollectorsDisableSwitch:
Wire it in CollectorsSwitches():
func CollectorsSwitches() CollectorsDisableSwitch {
return CollectorsDisableSwitch{
// ... existing fields ...
Example: !*exampleCollectorDisabled,
}
}
Step 5: Wire the disable flag¶
Add the Without option in internal/collector/collector.go:
Wire it in main.go:
if !collectorsSwitches.Example {
collectorOptionFuncs = append(collectorOptionFuncs, collector.WithoutExampleCollector())
logger.Info("example collector disabled")
}
Testing¶
Add tests for the new collector in internal/collector/example_test.go and for the fetch method in opnsense/example_test.go.
Run the tests:
Step 6: Update the documentation¶
Documentation is generated — never hand-edit content between <!-- docgen:begin/end --> markers
or the docgen-generated pages. After the code is in place:
- Add the subsystem's display name to
SubsystemDisplayNamesininternal/collector/collector.go(a unit test fails without it). - Add a
CollectorFlagsentry ininternal/options/collectors.gobinding the new flag to the subsystem string (a unit test fails without it). - Run
make docsand commit the result. It regenerates the metrics/collector references, re-injects the flag tables indocs/configuration.md, re-pins metric/collector counts across the site and README, lints all doc flag/env tokens, and cross-checks docs against the live collector registry. CI (make docs-check) fails if any of this is stale.
Checklist¶
- Collector implements
CollectorInstanceinterface -
init()function registers the collector - Subsystem constant added to
collector.go -
Fetch*()method and data structs added toopnsense/ - API endpoint registered in the client (and
testEndpoints()/ endpoint count updated) - Disable flag added to
internal/options/collectors.go -
Without*()option added tocollector.go - Disable logic wired in
main.go - Tests added and passing
-
SubsystemDisplayNamesentry added ininternal/collector/collector.go -
CollectorFlagsentry added ininternal/options/collectors.go -
make docsrun and output committed