diff --git a/README.md b/README.md index 0ca203a..725ba57 100644 --- a/README.md +++ b/README.md @@ -33,16 +33,20 @@ See the `-help` output for more options. ``` -collector.apilistener Include APIListener data --collector.cib - Include CIB data -collector.checker Include CheckerComponent data +-collector.cib + Include CIB data +-collector.gelf + Include GelfWriter data -collector.graphite Include GraphiteWriter data -collector.influx Include InfluxDBWriter data -collector.influx2 Include InfluxDB2Writer data +-collector.opentsdb + Include OpenTSDBWriter data -collector.otlpmetrics Include OTLPMetricsWriter data -debug diff --git a/icinga2_exporter.go b/icinga2_exporter.go index 4f70d48..801f8d4 100644 --- a/icinga2_exporter.go +++ b/icinga2_exporter.go @@ -70,6 +70,8 @@ func main() { cliCollectorInflux bool cliCollectorInflux2 bool cliCollectorGraphite bool + cliCollectorOpenTSDB bool + cliCollectorGELF bool cliCollectorOTLP bool ) @@ -91,6 +93,8 @@ func main() { flag.BoolVar(&cliCollectorInflux, "collector.influx", false, "Include InfluxDBWriter data") flag.BoolVar(&cliCollectorInflux2, "collector.influx2", false, "Include InfluxDB2Writer data") flag.BoolVar(&cliCollectorGraphite, "collector.graphite", false, "Include GraphiteWriter data") + flag.BoolVar(&cliCollectorOpenTSDB, "collector.opentsdb", false, "Include OpenTSDBWriter data") + flag.BoolVar(&cliCollectorGELF, "collector.gelf", false, "Include GelfWriter data") flag.BoolVar(&cliCollectorOTLP, "collector.otlpmetrics", false, "Include OTLPMetricsWriter data") flag.BoolVar(&cliVersion, "version", false, "Print version") @@ -174,6 +178,14 @@ func main() { prometheus.MustRegister(collector.NewIcinga2GraphiteCollector(c, logger)) } + if cliCollectorGELF { + prometheus.MustRegister(collector.NewIcinga2GELFCollector(c, logger)) + } + + if cliCollectorOpenTSDB { + prometheus.MustRegister(collector.NewIcinga2OpenTSDBCollector(c, logger)) + } + if cliCollectorOTLP { prometheus.MustRegister(collector.NewIcinga2OTLPMetricsCollector(c, logger)) } diff --git a/internal/collector/gelf.go b/internal/collector/gelf.go new file mode 100644 index 0000000..d9326f2 --- /dev/null +++ b/internal/collector/gelf.go @@ -0,0 +1,60 @@ +package collector + +import ( + "fmt" + "log/slog" + + "github.com/NETWAYS/icinga2-exporter/internal/icinga" + + "github.com/prometheus/client_golang/prometheus" +) + +type Icinga2GELFCollector struct { + icingaClient icinga.IcingaClient + logger *slog.Logger +} + +func NewIcinga2GELFCollector(client icinga.IcingaClient, logger *slog.Logger) *Icinga2GELFCollector { + return &Icinga2GELFCollector{ + icingaClient: client, + logger: logger, + } +} + +func (collector *Icinga2GELFCollector) Describe(ch chan<- *prometheus.Desc) { +} + +func (collector *Icinga2GELFCollector) Collect(ch chan<- prometheus.Metric) { + perfdata, err := collector.icingaClient.GetPerfdataMetrics(icinga.EndpointGELFWriter) + + if err != nil { + collector.logger.Error("Could not retrieve GELFWriter metrics", "error", err.Error()) + return + } + + for _, result := range perfdata.Results { + for component, writers := range result.Status { + for writer, metrics := range writers { + for metricName, value := range metrics { + if !isPerfdataMetric(metricName) { + continue + } + + safeMetricName := ensureValidMetricName(metricName) + + name := fmt.Sprintf("icinga2_%s_%s", component, safeMetricName) + + description := prometheus.NewDesc(name, "GELFWriter "+name, []string{"writer"}, nil) + metric, err := prometheus.NewConstMetric(description, prometheus.GaugeValue, value, writer) + + if err != nil { + collector.logger.Error("Error creating metric "+metricName, "error", err.Error()) + continue + } + + ch <- metric + } + } + } + } +} diff --git a/internal/collector/gelf_test.go b/internal/collector/gelf_test.go new file mode 100644 index 0000000..dd658a5 --- /dev/null +++ b/internal/collector/gelf_test.go @@ -0,0 +1,48 @@ +package collector + +import ( + "encoding/json" + "io" + "log/slog" + "os" + "strings" + "testing" + + "github.com/NETWAYS/icinga2-exporter/internal/icinga" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" +) + +func TestIcinga2GELFWriterCollector_Collect(t *testing.T) { + client := &MockIcingaClient{} + + var result icinga.PerfdataResult + data, _ := os.ReadFile("testdata/gelf.json") + json.Unmarshal(data, &result) + + client.SetPerfdataMetrics(result) + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + collector := NewIcinga2GELFCollector(client, logger) + + reg := prometheus.NewRegistry() + reg.MustRegister(collector) + + metricCount := testutil.CollectAndCount(collector) + + expectedCount := 2 + if metricCount != expectedCount { + t.Errorf("expected %d metrics, got %d", expectedCount, metricCount) + } + + expectedMetrics := ` + # HELP icinga2_gelfwriter_work_queue_items GELFWriter icinga2_gelfwriter_work_queue_items + # TYPE icinga2_gelfwriter_work_queue_items gauge + icinga2_gelfwriter_work_queue_items{writer="gelf"} 4 + ` + + if err := testutil.CollectAndCompare(collector, strings.NewReader(expectedMetrics), "icinga2_gelfwriter_work_queue_items"); err != nil { + t.Errorf("unexpected metric difference:\n%s", err) + } +} diff --git a/internal/collector/opentsdb.go b/internal/collector/opentsdb.go new file mode 100644 index 0000000..007c8a5 --- /dev/null +++ b/internal/collector/opentsdb.go @@ -0,0 +1,60 @@ +package collector + +import ( + "fmt" + "log/slog" + + "github.com/NETWAYS/icinga2-exporter/internal/icinga" + + "github.com/prometheus/client_golang/prometheus" +) + +type Icinga2OpenTSDBCollector struct { + icingaClient icinga.IcingaClient + logger *slog.Logger +} + +func NewIcinga2OpenTSDBCollector(client icinga.IcingaClient, logger *slog.Logger) *Icinga2OpenTSDBCollector { + return &Icinga2OpenTSDBCollector{ + icingaClient: client, + logger: logger, + } +} + +func (collector *Icinga2OpenTSDBCollector) Describe(ch chan<- *prometheus.Desc) { +} + +func (collector *Icinga2OpenTSDBCollector) Collect(ch chan<- prometheus.Metric) { + perfdata, err := collector.icingaClient.GetPerfdataMetrics(icinga.EndpointOpenTsdbWriter) + + if err != nil { + collector.logger.Error("Could not retrieve OpenTSDBWriter metrics", "error", err.Error()) + return + } + + for _, result := range perfdata.Results { + for component, writers := range result.Status { + for writer, metrics := range writers { + for metricName, value := range metrics { + if !isPerfdataMetric(metricName) { + continue + } + + safeMetricName := ensureValidMetricName(metricName) + + name := fmt.Sprintf("icinga2_%s_%s", component, safeMetricName) + + description := prometheus.NewDesc(name, "OpenTSDBWriter "+name, []string{"writer"}, nil) + metric, err := prometheus.NewConstMetric(description, prometheus.GaugeValue, value, writer) + + if err != nil { + collector.logger.Error("Error creating metric "+metricName, "error", err.Error()) + continue + } + + ch <- metric + } + } + } + } +} diff --git a/internal/collector/opentsdb_test.go b/internal/collector/opentsdb_test.go new file mode 100644 index 0000000..a0f7a00 --- /dev/null +++ b/internal/collector/opentsdb_test.go @@ -0,0 +1,48 @@ +package collector + +import ( + "encoding/json" + "io" + "log/slog" + "os" + "strings" + "testing" + + "github.com/NETWAYS/icinga2-exporter/internal/icinga" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" +) + +func TestIcinga2OpenTSDBWriterCollector_Collect(t *testing.T) { + client := &MockIcingaClient{} + + var result icinga.PerfdataResult + data, _ := os.ReadFile("testdata/opentsdb.json") + json.Unmarshal(data, &result) + + client.SetPerfdataMetrics(result) + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + collector := NewIcinga2OpenTSDBCollector(client, logger) + + reg := prometheus.NewRegistry() + reg.MustRegister(collector) + + metricCount := testutil.CollectAndCount(collector) + + expectedCount := 2 + if metricCount != expectedCount { + t.Errorf("expected %d metrics, got %d", expectedCount, metricCount) + } + + expectedMetrics := ` + # HELP icinga2_opentsdbwriter_work_queue_items OpenTSDBWriter icinga2_opentsdbwriter_work_queue_items + # TYPE icinga2_opentsdbwriter_work_queue_items gauge + icinga2_opentsdbwriter_work_queue_items{writer="opentsdb"} 4 + ` + + if err := testutil.CollectAndCompare(collector, strings.NewReader(expectedMetrics), "icinga2_opentsdbwriter_work_queue_items"); err != nil { + t.Errorf("unexpected metric difference:\n%s", err) + } +} diff --git a/internal/collector/testdata/gelf.json b/internal/collector/testdata/gelf.json new file mode 100644 index 0000000..744ff70 --- /dev/null +++ b/internal/collector/testdata/gelf.json @@ -0,0 +1,41 @@ +{ + "results": [ + { + "name": "GelfWriter", + "perfdata": [ + { + "counter": false, + "crit": null, + "label": "gelfwriter_gelf_work_queue_items", + "max": null, + "min": null, + "type": "PerfdataValue", + "unit": "", + "value": 4, + "warn": null + }, + { + "counter": false, + "crit": null, + "label": "gelfwriter_gelf_work_queue_item_rate", + "max": null, + "min": null, + "type": "PerfdataValue", + "unit": "", + "value": 0, + "warn": null + } + ], + "status": { + "gelfwriter": { + "gelf": { + "connected": false, + "source": "icinga2", + "work_queue_item_rate": 0, + "work_queue_items": 4 + } + } + } + } + ] +} diff --git a/internal/collector/testdata/opentsdb.json b/internal/collector/testdata/opentsdb.json new file mode 100644 index 0000000..fd850a1 --- /dev/null +++ b/internal/collector/testdata/opentsdb.json @@ -0,0 +1,40 @@ +{ + "results": [ + { + "name": "OpenTsdbWriter", + "perfdata": [ + { + "counter": false, + "crit": null, + "label": "opentsdbwriter_opentsdb_work_queue_items", + "max": null, + "min": null, + "type": "PerfdataValue", + "unit": "", + "value": 4, + "warn": null + }, + { + "counter": false, + "crit": null, + "label": "opentsdbwriter_opentsdb_work_queue_item_rate", + "max": null, + "min": null, + "type": "PerfdataValue", + "unit": "", + "value": 0, + "warn": null + } + ], + "status": { + "opentsdbwriter": { + "opentsdb": { + "connected": false, + "work_queue_item_rate": 0, + "work_queue_items": 4 + } + } + } + } + ] +} diff --git a/internal/icinga/client.go b/internal/icinga/client.go index ad50069..14b1a65 100644 --- a/internal/icinga/client.go +++ b/internal/icinga/client.go @@ -29,7 +29,7 @@ const ( EndpointElasticsearchWriter = "/status/ElasticsearchWriter" EndpointExternalCommandListener = "/status/ExternalCommandListener" EndpointFileLogger = "/status/FileLogger" - EndpointGelfWriter = "/status/GelfWriter" + EndpointGELFWriter = "/status/GelfWriter" EndpointGraphiteWriter = "/status/GraphiteWriter" EndpointIdoMysqlConnection = "/status/IdoMysqlConnection" EndpointIdoPgsqlConnection = "/status/IdoPgsqlConnection"