package collector import ( "errors" "fmt" "sync" "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" "github.com/st3ga/opnsense-exporter/opnsense" ) const namespace = "opnsense" // CollectorInstance is the interface a service specific collectors must implement. type CollectorInstance interface { Register(namespace, isntance string, log log.Logger) Name() string Describe(ch chan<- *prometheus.Desc) Update(client *opnsense.Client, ch chan<- prometheus.Metric) *opnsense.APICallError } // collectorInstances is a list of collectorInstances that will be registered // from the init() function in each collector file var collectorInstances []CollectorInstance type Collector struct { instanceLabel string mutex sync.RWMutex Client *opnsense.Client log log.Logger collectors []CollectorInstance scrapes prometheus.CounterVec endpointErrors prometheus.CounterVec } type Option func(*Collector) error // withoutCollectorInstance removes a collector by given name from the list of collectors // that are registered from their init functions. func withoutCollectorInstance(name string) Option { return func(o *Collector) error { for i, collector := range o.collectors { if collector.Name() == name { o.collectors = append(o.collectors[:i], o.collectors[i+1:]...) return nil } } return fmt.Errorf("collector %s not found", name) } } // WithoutArpTableCollector Option // removes the arp_table collector from the list of collectors func WithoutArpTableCollector() Option { return withoutCollectorInstance("arp_table") } // WithoutCronCollector Option // removes the cron collector from the list of collectors func WithoutCronCollector() Option { return withoutCollectorInstance("cron") } // New creates a new Collector instance. func New(client *opnsense.Client, log log.Logger, instanceName string, options ...Option) (*Collector, error) { c := Collector{ Client: client, log: log, instanceLabel: instanceName, collectors: collectorInstances, } for _, option := range options { if err := option(&c); err != nil { return nil, errors.Join(err, fmt.Errorf("failed to apply option")) } } for _, collector := range c.collectors { collector.Register(namespace, instanceName, c.log) } c.scrapes = *prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: namespace, Name: "exporter_scrapes_total", Help: "Total number of times OPNsense was scraped for metrics.", }, []string{"opnsense_instance"}) c.endpointErrors = *prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: namespace, Name: "exporter_endpoint_errors_total", Help: "Total number of errors by endpoint returned by the OPNsense API during data fetching", }, []string{"endpoint", "opnsense_instance"}) prometheus.MustRegister(c.scrapes) prometheus.MustRegister(c.endpointErrors) c.scrapes.WithLabelValues(c.instanceLabel).Add(0) for _, path := range c.Client.Endpoints() { c.endpointErrors.WithLabelValues(string(path), c.instanceLabel).Add(0) } return &c, nil } // Describe implements the prometheus.Collector interface. func (c *Collector) Describe(ch chan<- *prometheus.Desc) { c.scrapes.Describe(ch) c.endpointErrors.Describe(ch) for _, collector := range c.collectors { collector.Describe(ch) } } // Collect implements the prometheus.Collector interface. func (c *Collector) Collect(ch chan<- prometheus.Metric) { c.mutex.Lock() defer c.mutex.Unlock() var wg sync.WaitGroup wg.Add(len(c.collectors)) for _, collector := range c.collectors { go func(coll CollectorInstance) { if err := coll.Update(c.Client, ch); err != nil { level.Error(c.log).Log( "msg", "failed to update", "component", "collector", "collector_name", coll.Name(), "err", err, ) c.endpointErrors.WithLabelValues(err.Endpoint, c.instanceLabel).Inc() } wg.Done() }(collector) } wg.Wait() c.scrapes.WithLabelValues(c.instanceLabel).Inc() c.scrapes.Collect(ch) c.endpointErrors.Collect(ch) }