From 71c0878eb4141e7293cc66d104536285ed94d87c Mon Sep 17 00:00:00 2001 From: Markus Opolka <7090372+martialblog@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:19:47 +0100 Subject: [PATCH 1/2] Make CIB data optional --- README.md | 5 ++++- icinga2_exporter.go | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5e9ed3..a213769 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ See the `-help` output for more options. ``` -collector.apilistener Include APIListener data +-collector.cib + Include CIB data -debug Enable debug logging -icinga.api string @@ -38,7 +40,7 @@ See the `-help` output for more options. ## Collectors -By default only the `CIB` metrics of the status API are collected. +By default only the `IcingaApplication` metrics of the status API are collected. There are more collectors that can be activated via the CLI. The tables below list all existing collectors. @@ -46,6 +48,7 @@ The tables below list all existing collectors. | Collector | Flag | | ------------- | ---------- | | APIListener | `-collector.apilistener` | +| CIB | `-collector.cib` | # Development diff --git a/icinga2_exporter.go b/icinga2_exporter.go index bbb9351..002eaa6 100644 --- a/icinga2_exporter.go +++ b/icinga2_exporter.go @@ -57,6 +57,7 @@ func main() { cliDebugLog bool cliInsecure bool cliCollectorApiListener bool + cliCollectorCIB bool ) flag.StringVar(&cliListenAddress, "web.listen-address", ":9665", "Address on which to expose metrics and web interface.") @@ -72,6 +73,7 @@ func main() { flag.BoolVar(&cliInsecure, "icinga.insecure", false, "Skip TLS verification for Icinga2 API") flag.BoolVar(&cliCollectorApiListener, "collector.apilistener", false, "Include APIListener data") + flag.BoolVar(&cliCollectorCIB, "collector.cib", false, "Include CIB data") flag.BoolVar(&cliVersion, "version", false, "Print version") flag.BoolVar(&cliDebugLog, "debug", false, "Enable debug logging") @@ -122,9 +124,12 @@ func main() { } // Register Collectors - prometheus.MustRegister(collector.NewIcinga2CIBCollector(c, logger)) prometheus.MustRegister(collector.NewIcinga2ApplicationCollector(c, logger)) + if cliCollectorCIB { + prometheus.MustRegister(collector.NewIcinga2CIBCollector(c, logger)) + } + if cliCollectorApiListener { prometheus.MustRegister(collector.NewIcinga2APICollector(c, logger)) } From 4f8286b974c29d7240e693835e4b56c23f474f52 Mon Sep 17 00:00:00 2001 From: Markus Opolka <7090372+martialblog@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:06:46 +0100 Subject: [PATCH 2/2] Add CheckerComponent collector --- README.md | 3 ++ icinga2_exporter.go | 6 +++ internal/collector/checker.go | 59 ++++++++++++++++++++++++++ internal/icinga/client.go | 18 ++++++++ internal/icinga/client_test.go | 55 ++++++++++++++++++++++-- internal/icinga/model.go | 7 +++ internal/icinga/testdata/checker1.json | 39 +++++++++++++++++ 7 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 internal/collector/checker.go create mode 100644 internal/icinga/testdata/checker1.json diff --git a/README.md b/README.md index a213769..eeef9c2 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ See the `-help` output for more options. Include APIListener data -collector.cib Include CIB data +-collector.checker + Include CheckerComponent data -debug Enable debug logging -icinga.api string @@ -49,6 +51,7 @@ The tables below list all existing collectors. | ------------- | ---------- | | APIListener | `-collector.apilistener` | | CIB | `-collector.cib` | +| CheckerComponent | `-collector.checker` | # Development diff --git a/icinga2_exporter.go b/icinga2_exporter.go index 002eaa6..27806cc 100644 --- a/icinga2_exporter.go +++ b/icinga2_exporter.go @@ -58,6 +58,7 @@ func main() { cliInsecure bool cliCollectorApiListener bool cliCollectorCIB bool + cliCollectorChecker bool ) flag.StringVar(&cliListenAddress, "web.listen-address", ":9665", "Address on which to expose metrics and web interface.") @@ -74,6 +75,7 @@ func main() { flag.BoolVar(&cliCollectorApiListener, "collector.apilistener", false, "Include APIListener data") flag.BoolVar(&cliCollectorCIB, "collector.cib", false, "Include CIB data") + flag.BoolVar(&cliCollectorChecker, "collector.checker", false, "Include CheckerComponent data") flag.BoolVar(&cliVersion, "version", false, "Print version") flag.BoolVar(&cliDebugLog, "debug", false, "Enable debug logging") @@ -134,6 +136,10 @@ func main() { prometheus.MustRegister(collector.NewIcinga2APICollector(c, logger)) } + if cliCollectorChecker { + prometheus.MustRegister(collector.NewIcinga2CheckerCollector(c, logger)) + } + // Create a central context to propagate a shutdown ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() diff --git a/internal/collector/checker.go b/internal/collector/checker.go new file mode 100644 index 0000000..f55f258 --- /dev/null +++ b/internal/collector/checker.go @@ -0,0 +1,59 @@ +package collector + +import ( + "log/slog" + + "github.com/martialblog/icinga2-exporter/internal/icinga" + + "github.com/prometheus/client_golang/prometheus" +) + +type Icinga2CheckerCollector struct { + icingaClient *icinga.Client + logger *slog.Logger + checkercomponent_checker_idle *prometheus.Desc + checkercomponent_checker_pending *prometheus.Desc +} + +func NewIcinga2CheckerCollector(client *icinga.Client, logger *slog.Logger) *Icinga2CheckerCollector { + return &Icinga2CheckerCollector{ + icingaClient: client, + logger: logger, + checkercomponent_checker_idle: prometheus.NewDesc("icinga2_checkercomponent_checker_idle", "CheckerComponent idle", nil, nil), + checkercomponent_checker_pending: prometheus.NewDesc("icinga2_checkercomponent_checker_pending", "CheckerComponent pending", nil, nil), + } +} + +func (collector *Icinga2CheckerCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- collector.checkercomponent_checker_idle + ch <- collector.checkercomponent_checker_pending +} + +func (collector *Icinga2CheckerCollector) Collect(ch chan<- prometheus.Metric) { + result, err := collector.icingaClient.GetCheckerComponentMetrics() + + if err != nil { + collector.logger.Error("Could not retrieve CheckerComponent metrics", "error", err.Error()) + return + } + + if len(result.Results) < 1 { + collector.logger.Debug("No results for CheckerComponent metrics") + return + } + + r := result.Results[0] + // There might be a better way + var perfdata = make(map[string]float64, len(r.Perfdata)) + for _, v := range r.Perfdata { + perfdata[v.Label] = v.Value + } + + if v, ok := perfdata["checkercomponent_checker_idle"]; ok { + ch <- prometheus.MustNewConstMetric(collector.checkercomponent_checker_idle, prometheus.GaugeValue, v) + } + + if v, ok := perfdata["checkercomponent_checker_pending"]; ok { + ch <- prometheus.MustNewConstMetric(collector.checkercomponent_checker_pending, prometheus.GaugeValue, v) + } +} diff --git a/internal/icinga/client.go b/internal/icinga/client.go index f910add..a3afa4c 100644 --- a/internal/icinga/client.go +++ b/internal/icinga/client.go @@ -149,6 +149,24 @@ func (icinga *Client) GetApplicationMetrics() (ApplicationResult, error) { return result, nil } +func (icinga *Client) GetCheckerComponentMetrics() (CheckerComponentResult, error) { + var result CheckerComponentResult + + body, errBody := icinga.fetchJSON(endpointCheckerComponent) + + if errBody != nil { + return result, fmt.Errorf("error fetching response: %w", errBody) + } + + errDecode := json.Unmarshal(body, &result) + + if errDecode != nil { + return result, fmt.Errorf("error parsing response: %w", errDecode) + } + + return result, nil +} + func (icinga *Client) fetchJSON(endpoint string) ([]byte, error) { // Lookup data in the cache we go out and bother the Icinga API if elem, ok := icinga.cache.Get(endpoint); ok { diff --git a/internal/icinga/client_test.go b/internal/icinga/client_test.go index 604aaff..f884610 100644 --- a/internal/icinga/client_test.go +++ b/internal/icinga/client_test.go @@ -11,9 +11,10 @@ import ( ) const ( - icingaTestDataCIB1 = "testdata/cib1.json" - icingaTestDataAPP1 = "testdata/app1.json" - icingaTestDataAPI1 = "testdata/api1.json" + icingaTestDataCIB1 = "testdata/cib1.json" + icingaTestDataAPP1 = "testdata/app1.json" + icingaTestDataAPI1 = "testdata/api1.json" + icingaTestDataCheck1 = "testdata/checker1.json" ) func loadTestdata(filepath string) []byte { @@ -185,3 +186,51 @@ func Test_GetApiListenerMetrics(t *testing.T) { }) } } + +func Test_GetCheckerMetrics(t *testing.T) { + testcases := map[string]struct { + expected CheckerComponentResult + server *httptest.Server + }{ + "application": { + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write(loadTestdata(icingaTestDataCheck1)) + })), + expected: CheckerComponentResult{ + Results: []struct { + Name string `json:"name"` + Perfdata []Perfdata `json:"perfdata,omitempty"` + }{ + { + Name: "CheckerComponent", + Perfdata: []Perfdata{ + {Label: "checkercomponent_checker_idle", Value: 15}, + {Label: "checkercomponent_checker_pending", Value: 10}, + }, + }, + }, + }, + }, + } + + for name, test := range testcases { + t.Run(name, func(t *testing.T) { + defer test.server.Close() + + cfg := testConfig(test.server) + + cli, _ := NewClient(cfg) + + actual, err := cli.GetCheckerComponentMetrics() + + if err != nil { + t.Fatalf("did not expect error got:\n %+v", err) + } + + if !reflect.DeepEqual(test.expected, actual) { + t.Fatalf("expected:\n %+v \ngot:\n %+v", test.expected, actual) + } + }) + } +} diff --git a/internal/icinga/model.go b/internal/icinga/model.go index 9ecc302..cd9476a 100644 --- a/internal/icinga/model.go +++ b/internal/icinga/model.go @@ -42,3 +42,10 @@ type App struct { EnableServiceChecks bool `json:"enable_service_checks"` Version string `json:"version"` } + +type CheckerComponentResult struct { + Results []struct { + Name string `json:"name"` + Perfdata []Perfdata `json:"perfdata,omitempty"` + } `json:"results"` +} diff --git a/internal/icinga/testdata/checker1.json b/internal/icinga/testdata/checker1.json new file mode 100644 index 0000000..209d997 --- /dev/null +++ b/internal/icinga/testdata/checker1.json @@ -0,0 +1,39 @@ +{ + "results": [ + { + "name": "CheckerComponent", + "perfdata": [ + { + "counter": false, + "crit": null, + "label": "checkercomponent_checker_idle", + "max": null, + "min": null, + "type": "PerfdataValue", + "unit": "", + "value": 15, + "warn": null + }, + { + "counter": false, + "crit": null, + "label": "checkercomponent_checker_pending", + "max": null, + "min": null, + "type": "PerfdataValue", + "unit": "", + "value": 10, + "warn": null + } + ], + "status": { + "checkercomponent": { + "checker": { + "idle": 14, + "pending": 0 + } + } + } + } + ] +}