From 4be9c8ff01557692d972a58a75f5792c2761308e Mon Sep 17 00:00:00 2001 From: Markus Opolka <7090372+martialblog@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:50:06 +0100 Subject: [PATCH] Add various object metrics --- README.md | 3 + icinga2_exporter.go | 6 ++ internal/collector/objects.go | 76 ++++++++++++++++++++++++++ internal/icinga/client.go | 19 +++++++ internal/icinga/model.go | 7 +++ internal/icinga/testdata/objects1.json | 14 +++++ 6 files changed, 125 insertions(+) create mode 100644 internal/collector/objects.go create mode 100644 internal/icinga/testdata/objects1.json diff --git a/README.md b/README.md index eeef9c2..5f0dce4 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ See the `-help` output for more options. Include CIB data -collector.checker Include CheckerComponent data +-collector.objects + Include Object data -debug Enable debug logging -icinga.api string @@ -52,6 +54,7 @@ The tables below list all existing collectors. | APIListener | `-collector.apilistener` | | CIB | `-collector.cib` | | CheckerComponent | `-collector.checker` | +| Objects | `-collector.objects` | # Development diff --git a/icinga2_exporter.go b/icinga2_exporter.go index 27806cc..5c7e896 100644 --- a/icinga2_exporter.go +++ b/icinga2_exporter.go @@ -59,6 +59,7 @@ func main() { cliCollectorApiListener bool cliCollectorCIB bool cliCollectorChecker bool + cliCollectorObjects bool ) flag.StringVar(&cliListenAddress, "web.listen-address", ":9665", "Address on which to expose metrics and web interface.") @@ -76,6 +77,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(&cliCollectorObjects, "collector.objects", false, "Include Object data") flag.BoolVar(&cliVersion, "version", false, "Print version") flag.BoolVar(&cliDebugLog, "debug", false, "Enable debug logging") @@ -140,6 +142,10 @@ func main() { prometheus.MustRegister(collector.NewIcinga2CheckerCollector(c, logger)) } + if cliCollectorObjects { + prometheus.MustRegister(collector.NewIcinga2ObjectsCollector(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/objects.go b/internal/collector/objects.go new file mode 100644 index 0000000..f56a846 --- /dev/null +++ b/internal/collector/objects.go @@ -0,0 +1,76 @@ +package collector + +import ( + "log/slog" + + "github.com/martialblog/icinga2-exporter/internal/icinga" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + zonesObject = "zones" + usersObject = "users" +) + +type Icinga2ObjectsCollector struct { + icingaClient *icinga.Client + logger *slog.Logger + objects_zones_num *prometheus.Desc + objects_users_num *prometheus.Desc +} + +func NewIcinga2ObjectsCollector(client *icinga.Client, logger *slog.Logger) *Icinga2ObjectsCollector { + return &Icinga2ObjectsCollector{ + icingaClient: client, + logger: logger, + objects_zones_num: prometheus.NewDesc("icinga2_objects_zones_num", "Number of Zone objects", nil, nil), + objects_users_num: prometheus.NewDesc("icinga2_objects_users_num", "Number of User objects", nil, nil), + } +} + +func (collector *Icinga2ObjectsCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- collector.objects_zones_num + ch <- collector.objects_users_num +} + +func (collector *Icinga2ObjectsCollector) Collect(ch chan<- prometheus.Metric) { + collector.collectZones(ch) + collector.collectUsers(ch) +} + +func (collector *Icinga2ObjectsCollector) collectZones(ch chan<- prometheus.Metric) { + result, err := collector.icingaClient.GetObjects(zonesObject) + + if err != nil { + collector.logger.Error("Could not retrieve Zones metrics", "error", err.Error()) + return + } + + if len(result.Results) < 1 { + collector.logger.Debug("No results for Zones metrics") + return + } + + zoneCount := float64(len(result.Results)) + + ch <- prometheus.MustNewConstMetric(collector.objects_zones_num, prometheus.GaugeValue, zoneCount) +} + +func (collector *Icinga2ObjectsCollector) collectUsers(ch chan<- prometheus.Metric) { + result, err := collector.icingaClient.GetObjects(usersObject) + + if err != nil { + collector.logger.Error("Could not retrieve Users metrics", "error", err.Error()) + return + } + + if len(result.Results) < 1 { + collector.logger.Debug("No results for Users metrics") + return + } + + userCount := float64(len(result.Results)) + + ch <- prometheus.MustNewConstMetric(collector.objects_users_num, prometheus.GaugeValue, userCount) +} diff --git a/internal/icinga/client.go b/internal/icinga/client.go index a3afa4c..a2a9301 100644 --- a/internal/icinga/client.go +++ b/internal/icinga/client.go @@ -12,6 +12,7 @@ import ( ) const ( + endpointObjects = "/objects" endpointApiListener = "/status/ApiListener" endpointApplication = "/status/IcingaApplication" endpointCIB = "/status/CIB" @@ -167,6 +168,24 @@ func (icinga *Client) GetCheckerComponentMetrics() (CheckerComponentResult, erro return result, nil } +func (icinga *Client) GetObjects(object string) (ObjectsResult, error) { + var result ObjectsResult + + body, errBody := icinga.fetchJSON(endpointObjects + "/" + object) + + 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/model.go b/internal/icinga/model.go index cd9476a..b66fa54 100644 --- a/internal/icinga/model.go +++ b/internal/icinga/model.go @@ -49,3 +49,10 @@ type CheckerComponentResult struct { Perfdata []Perfdata `json:"perfdata,omitempty"` } `json:"results"` } + +type ObjectsResult struct { + Results []struct { + Name string `json:"name"` + Type string `json:"type"` + } `json:"results"` +} diff --git a/internal/icinga/testdata/objects1.json b/internal/icinga/testdata/objects1.json new file mode 100644 index 0000000..9e7f1b2 --- /dev/null +++ b/internal/icinga/testdata/objects1.json @@ -0,0 +1,14 @@ +curl -X GET -s -k -u 'root:0e7ff601de4a5426' 'https://localhost:5665/v1/objects/users?pretty=1' -d '{ "attrs": [ "__name"]}' +{ + "results": [ + { + "attrs": { + "__name": "icingaadmin" + }, + "joins": {}, + "meta": {}, + "name": "icingaadmin", + "type": "User" + } + ] +}