diff --git a/README.md b/README.md index eeef9c2..0e40c0e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ See the `-help` output for more options. Include CIB data -collector.checker Include CheckerComponent data +-collector.graphite + Include GraphiteWriter data +-collector.influx + Include InfluxDBWriter data +-collector.influx2 + Include InfluxDB2Writer data -debug Enable debug logging -icinga.api string @@ -52,6 +58,9 @@ The tables below list all existing collectors. | APIListener | `-collector.apilistener` | | CIB | `-collector.cib` | | CheckerComponent | `-collector.checker` | +| InfluxDBWriter | `-collector.influx` | +| InfluxDB2Writer | `-collector.influx2` | +| GraphiteWriter | `-collector.graphite` | # Development diff --git a/icinga2_exporter.go b/icinga2_exporter.go index 27806cc..eb8684f 100644 --- a/icinga2_exporter.go +++ b/icinga2_exporter.go @@ -59,6 +59,9 @@ func main() { cliCollectorApiListener bool cliCollectorCIB bool cliCollectorChecker bool + cliCollectorInflux bool + cliCollectorInflux2 bool + cliCollectorGraphite bool ) flag.StringVar(&cliListenAddress, "web.listen-address", ":9665", "Address on which to expose metrics and web interface.") @@ -76,6 +79,9 @@ 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(&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(&cliVersion, "version", false, "Print version") flag.BoolVar(&cliDebugLog, "debug", false, "Enable debug logging") @@ -140,6 +146,18 @@ func main() { prometheus.MustRegister(collector.NewIcinga2CheckerCollector(c, logger)) } + if cliCollectorInflux { + prometheus.MustRegister(collector.NewIcinga2InfluxDBCollector(c, logger)) + } + + if cliCollectorInflux2 { + prometheus.MustRegister(collector.NewIcinga2InfluxDB2Collector(c, logger)) + } + + if cliCollectorGraphite { + prometheus.MustRegister(collector.NewIcinga2GraphiteCollector(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/api.go b/internal/collector/api.go index 821a091..39a45d4 100644 --- a/internal/collector/api.go +++ b/internal/collector/api.go @@ -36,27 +36,28 @@ func (collector *Icinga2APICollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *Icinga2APICollector) Collect(ch chan<- prometheus.Metric) { - result, err := collector.icingaClient.GetApiListenerMetrics() + perfdata, err := collector.icingaClient.GetPerfdataMetrics(icinga.EndpointApiListener) if err != nil { collector.logger.Error("Could not retrieve ApiListener metrics", "error", err.Error()) return } - if len(result.Results) < 1 { - collector.logger.Debug("No results for ApiListener metrics") - return - } + for _, datapoint := range perfdata { + if datapoint.Label == "api_num_conn_endpoints" { + ch <- prometheus.MustNewConstMetric(collector.api_num_conn_endpoints, prometheus.GaugeValue, datapoint.Value) + } - 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 datapoint.Label == "api_num_not_conn_endpoints" { + ch <- prometheus.MustNewConstMetric(collector.api_num_conn_endpoints, prometheus.GaugeValue, datapoint.Value) + } - ch <- prometheus.MustNewConstMetric(collector.api_num_conn_endpoints, prometheus.GaugeValue, perfdata["api_num_conn_endpoints"]) - ch <- prometheus.MustNewConstMetric(collector.api_num_not_conn_endpoints, prometheus.GaugeValue, perfdata["api_num_not_conn_endpoints"]) - ch <- prometheus.MustNewConstMetric(collector.api_num_endpoints, prometheus.GaugeValue, perfdata["api_num_endpoints"]) - ch <- prometheus.MustNewConstMetric(collector.api_num_http_clients, prometheus.GaugeValue, perfdata["api_num_http_clients"]) + if datapoint.Label == "api_num_endpoints" { + ch <- prometheus.MustNewConstMetric(collector.api_num_conn_endpoints, prometheus.GaugeValue, datapoint.Value) + } + + if datapoint.Label == "api_num_http_clients" { + ch <- prometheus.MustNewConstMetric(collector.api_num_conn_endpoints, prometheus.GaugeValue, datapoint.Value) + } + } } diff --git a/internal/collector/checker.go b/internal/collector/checker.go index f55f258..0e8fd79 100644 --- a/internal/collector/checker.go +++ b/internal/collector/checker.go @@ -30,30 +30,20 @@ func (collector *Icinga2CheckerCollector) Describe(ch chan<- *prometheus.Desc) { } func (collector *Icinga2CheckerCollector) Collect(ch chan<- prometheus.Metric) { - result, err := collector.icingaClient.GetCheckerComponentMetrics() + perfdata, err := collector.icingaClient.GetPerfdataMetrics(icinga.EndpointCheckerComponent) 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) - } + for _, datapoint := range perfdata { + if datapoint.Label == "checkercomponent_checker_idle" { + ch <- prometheus.MustNewConstMetric(collector.checkercomponent_checker_idle, prometheus.GaugeValue, datapoint.Value) + } - if v, ok := perfdata["checkercomponent_checker_pending"]; ok { - ch <- prometheus.MustNewConstMetric(collector.checkercomponent_checker_pending, prometheus.GaugeValue, v) + if datapoint.Label == "checkercomponent_checker_pending" { + ch <- prometheus.MustNewConstMetric(collector.checkercomponent_checker_pending, prometheus.GaugeValue, datapoint.Value) + } } } diff --git a/internal/collector/graphite.go b/internal/collector/graphite.go new file mode 100644 index 0000000..64c7024 --- /dev/null +++ b/internal/collector/graphite.go @@ -0,0 +1,54 @@ +package collector + +import ( + "log/slog" + + "github.com/martialblog/icinga2-exporter/internal/icinga" + + "github.com/prometheus/client_golang/prometheus" +) + +type Icinga2GraphiteCollector struct { + icingaClient *icinga.Client + logger *slog.Logger + graphitewriter_graphite_work_queue_items *prometheus.Desc + graphitewriter_graphite_work_queue_item_rate *prometheus.Desc + graphitewriter_graphite_data_queue_items *prometheus.Desc +} + +func NewIcinga2GraphiteCollector(client *icinga.Client, logger *slog.Logger) *Icinga2GraphiteCollector { + return &Icinga2GraphiteCollector{ + icingaClient: client, + logger: logger, + graphitewriter_graphite_work_queue_items: prometheus.NewDesc("icinga2_graphitewriter_graphite_work_queue_items", "GraphiteWriter work queue items", nil, nil), + graphitewriter_graphite_work_queue_item_rate: prometheus.NewDesc("icinga2_graphitewriter_graphite_work_queue_item_rate", "GraphiteWriter work queue item rate", nil, nil), + } +} + +func (collector *Icinga2GraphiteCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- collector.graphitewriter_graphite_work_queue_items + ch <- collector.graphitewriter_graphite_work_queue_item_rate +} + +func (collector *Icinga2GraphiteCollector) Collect(ch chan<- prometheus.Metric) { + perfdata, err := collector.icingaClient.GetPerfdataMetrics(icinga.EndpointGraphiteWriter) + + if err != nil { + collector.logger.Error("Could not retrieve Graphite metrics", "error", err.Error()) + return + } + + for _, datapoint := range perfdata { + if datapoint.Label == "graphitewriter_graphite_work_queue_items" { + ch <- prometheus.MustNewConstMetric(collector.graphitewriter_graphite_work_queue_items, prometheus.GaugeValue, datapoint.Value) + } + + if datapoint.Label == "graphitewriter_graphite_work_queue_item_rate" { + ch <- prometheus.MustNewConstMetric(collector.graphitewriter_graphite_work_queue_item_rate, prometheus.GaugeValue, datapoint.Value) + } + + if datapoint.Label == "graphitewriter_graphite_data_queue_items" { + ch <- prometheus.MustNewConstMetric(collector.graphitewriter_graphite_data_queue_items, prometheus.GaugeValue, datapoint.Value) + } + } +} diff --git a/internal/collector/influxdb.go b/internal/collector/influxdb.go new file mode 100644 index 0000000..65b6d89 --- /dev/null +++ b/internal/collector/influxdb.go @@ -0,0 +1,56 @@ +package collector + +import ( + "log/slog" + + "github.com/martialblog/icinga2-exporter/internal/icinga" + + "github.com/prometheus/client_golang/prometheus" +) + +type Icinga2InfluxDBCollector struct { + icingaClient *icinga.Client + logger *slog.Logger + influxdbwriter_influxdb_work_queue_items *prometheus.Desc + influxdbwriter_influxdb_work_queue_item_rate *prometheus.Desc + influxdbwriter_influxdb_data_queue_items *prometheus.Desc +} + +func NewIcinga2InfluxDBCollector(client *icinga.Client, logger *slog.Logger) *Icinga2InfluxDBCollector { + return &Icinga2InfluxDBCollector{ + icingaClient: client, + logger: logger, + influxdbwriter_influxdb_work_queue_items: prometheus.NewDesc("icinga2_influxdbwriter_influxdb_work_queue_items", "InfluxDBWriter work queue items", nil, nil), + influxdbwriter_influxdb_work_queue_item_rate: prometheus.NewDesc("icinga2_influxdbwriter_influxdb_work_queue_item_rate", "InfluxDBWriter work queue item rate", nil, nil), + influxdbwriter_influxdb_data_queue_items: prometheus.NewDesc("icinga2_influxdbwriter_influxdb_data_queue_items", "InfluxDBWriter data queue items", nil, nil), + } +} + +func (collector *Icinga2InfluxDBCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- collector.influxdbwriter_influxdb_work_queue_items + ch <- collector.influxdbwriter_influxdb_work_queue_item_rate + ch <- collector.influxdbwriter_influxdb_data_queue_items +} + +func (collector *Icinga2InfluxDBCollector) Collect(ch chan<- prometheus.Metric) { + perfdata, err := collector.icingaClient.GetPerfdataMetrics(icinga.EndpointInfluxdbWriter) + + if err != nil { + collector.logger.Error("Could not retrieve InfluxDB metrics", "error", err.Error()) + return + } + + for _, datapoint := range perfdata { + if datapoint.Label == "influxdbwriter_influxdb_work_queue_items" { + ch <- prometheus.MustNewConstMetric(collector.influxdbwriter_influxdb_work_queue_items, prometheus.GaugeValue, datapoint.Value) + } + + if datapoint.Label == "influxdbwriter_influxdb_work_queue_item_rate" { + ch <- prometheus.MustNewConstMetric(collector.influxdbwriter_influxdb_work_queue_item_rate, prometheus.GaugeValue, datapoint.Value) + } + + if datapoint.Label == "influxdbwriter_influxdb_data_queue_items" { + ch <- prometheus.MustNewConstMetric(collector.influxdbwriter_influxdb_data_queue_items, prometheus.GaugeValue, datapoint.Value) + } + } +} diff --git a/internal/collector/influxdb2.go b/internal/collector/influxdb2.go new file mode 100644 index 0000000..6687048 --- /dev/null +++ b/internal/collector/influxdb2.go @@ -0,0 +1,56 @@ +package collector + +import ( + "log/slog" + + "github.com/martialblog/icinga2-exporter/internal/icinga" + + "github.com/prometheus/client_golang/prometheus" +) + +type Icinga2InfluxDB2Collector struct { + icingaClient *icinga.Client + logger *slog.Logger + influxdb2writer_influxdb2_work_queue_items *prometheus.Desc + influxdb2writer_influxdb2_work_queue_item_rate *prometheus.Desc + influxdb2writer_influxdb2_data_queue_items *prometheus.Desc +} + +func NewIcinga2InfluxDB2Collector(client *icinga.Client, logger *slog.Logger) *Icinga2InfluxDB2Collector { + return &Icinga2InfluxDB2Collector{ + icingaClient: client, + logger: logger, + influxdb2writer_influxdb2_work_queue_items: prometheus.NewDesc("icinga2_influxdb2writer_influxdb2_work_queue_items", "InfluxDB2Writer work queue items", nil, nil), + influxdb2writer_influxdb2_work_queue_item_rate: prometheus.NewDesc("icinga2_influxdb2writer_influxdb2_work_queue_item_rate", "InfluxDB2Writer work queue item rate", nil, nil), + influxdb2writer_influxdb2_data_queue_items: prometheus.NewDesc("icinga2_influxdb2writer_influxdb2_data_queue_items", "InfluxDB2Writer data queue items", nil, nil), + } +} + +func (collector *Icinga2InfluxDB2Collector) Describe(ch chan<- *prometheus.Desc) { + ch <- collector.influxdb2writer_influxdb2_work_queue_items + ch <- collector.influxdb2writer_influxdb2_work_queue_item_rate + ch <- collector.influxdb2writer_influxdb2_data_queue_items +} + +func (collector *Icinga2InfluxDB2Collector) Collect(ch chan<- prometheus.Metric) { + perfdata, err := collector.icingaClient.GetPerfdataMetrics(icinga.EndpointInfluxdb2Writer) + + if err != nil { + collector.logger.Error("Could not retrieve InfluxDB2 metrics", "error", err.Error()) + return + } + + for _, datapoint := range perfdata { + if datapoint.Label == "influxdb2writer_influxdb2_work_queue_items" { + ch <- prometheus.MustNewConstMetric(collector.influxdb2writer_influxdb2_work_queue_items, prometheus.GaugeValue, datapoint.Value) + } + + if datapoint.Label == "influxdb2writer_influxdb2_work_queue_item_rate" { + ch <- prometheus.MustNewConstMetric(collector.influxdb2writer_influxdb2_work_queue_item_rate, prometheus.GaugeValue, datapoint.Value) + } + + if datapoint.Label == "influxdb2writer_influxdb2_data_queue_items" { + ch <- prometheus.MustNewConstMetric(collector.influxdb2writer_influxdb2_data_queue_items, prometheus.GaugeValue, datapoint.Value) + } + } +} diff --git a/internal/icinga/client.go b/internal/icinga/client.go index a3afa4c..1b61abd 100644 --- a/internal/icinga/client.go +++ b/internal/icinga/client.go @@ -12,27 +12,27 @@ import ( ) const ( - endpointApiListener = "/status/ApiListener" - endpointApplication = "/status/IcingaApplication" - endpointCIB = "/status/CIB" - endpointCheckerComponent = "/status/CheckerComponent" - endpointCompatLogger = "/status/CompatLogger" - endpointElasticsearchWriter = "/status/ElasticsearchWriter" - endpointExternalCommandListener = "/status/ExternalCommandListener" - endpointFileLogger = "/status/FileLogger" - endpointGelfWriter = "/status/GelfWriter" - endpointGraphiteWriter = "/status/GraphiteWriter" - endpointIcingaApplication = "/status/IcingaApplication" - endpointIdoMysqlConnection = "/status/IdoMysqlConnection" - endpointIdoPgsqlConnection = "/status/IdoPgsqlConnection" - endpointInfluxdb2Writer = "/status/Influxdb2Writer" - endpointInfluxdbWriter = "/status/InfluxdbWriter" - endpointJournaldLogger = "/status/JournaldLogger" - endpointLivestatusListener = "/status/LivestatusListener" - endpointNotificationComponent = "/status/NotificationComponent" - endpointOpenTsdbWriter = "/status/OpenTsdbWriter" - endpointPerfdataWriter = "/status/PerfdataWriter" - endpointSyslogLogger = "/status/SyslogLogger" + EndpointApiListener = "/status/ApiListener" + EndpointApplication = "/status/IcingaApplication" + EndpointCIB = "/status/CIB" + EndpointCheckerComponent = "/status/CheckerComponent" + EndpointCompatLogger = "/status/CompatLogger" + EndpointElasticsearchWriter = "/status/ElasticsearchWriter" + EndpointExternalCommandListener = "/status/ExternalCommandListener" + EndpointFileLogger = "/status/FileLogger" + EndpointGelfWriter = "/status/GelfWriter" + EndpointGraphiteWriter = "/status/GraphiteWriter" + EndpointIcingaApplication = "/status/IcingaApplication" + EndpointIdoMysqlConnection = "/status/IdoMysqlConnection" + EndpointIdoPgsqlConnection = "/status/IdoPgsqlConnection" + EndpointInfluxdb2Writer = "/status/Influxdb2Writer" + EndpointInfluxdbWriter = "/status/InfluxdbWriter" + EndpointJournaldLogger = "/status/JournaldLogger" + EndpointLivestatusListener = "/status/LivestatusListener" + EndpointNotificationComponent = "/status/NotificationComponent" + EndpointOpenTsdbWriter = "/status/OpenTsdbWriter" + EndpointPerfdataWriter = "/status/PerfdataWriter" + EndpointSyslogLogger = "/status/SyslogLogger" ) type Config struct { @@ -95,28 +95,35 @@ func NewClient(c Config) (*Client, error) { return cli, nil } -func (icinga *Client) GetApiListenerMetrics() (APIResult, error) { - var result APIResult +// GetPerfdataMetrics returns the perfdata from a given status API endpoint +func (icinga *Client) GetPerfdataMetrics(endpoint string) ([]Perfdata, error) { + var result PerfdataResult - body, errBody := icinga.fetchJSON(endpointApiListener) + body, errBody := icinga.fetchJSON(endpoint) if errBody != nil { - return result, fmt.Errorf("error fetching response: %w", errBody) + return nil, 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 nil, fmt.Errorf("error parsing response: %w", errDecode) } - return result, nil + if len(result.Results) < 1 { + return nil, fmt.Errorf("no results for '%s' endpoint", endpoint) + } + + r := result.Results[0] + + return r.Perfdata, nil } func (icinga *Client) GetCIBMetrics() (CIBResult, error) { var result CIBResult - body, errBody := icinga.fetchJSON(endpointCIB) + body, errBody := icinga.fetchJSON(EndpointCIB) if errBody != nil { return result, fmt.Errorf("error fetching response: %w", errBody) @@ -134,25 +141,7 @@ func (icinga *Client) GetCIBMetrics() (CIBResult, error) { func (icinga *Client) GetApplicationMetrics() (ApplicationResult, error) { var result ApplicationResult - body, errBody := icinga.fetchJSON(endpointApplication) - - 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) GetCheckerComponentMetrics() (CheckerComponentResult, error) { - var result CheckerComponentResult - - body, errBody := icinga.fetchJSON(endpointCheckerComponent) + body, errBody := icinga.fetchJSON(EndpointApplication) if errBody != nil { return result, fmt.Errorf("error fetching response: %w", errBody) diff --git a/internal/icinga/client_test.go b/internal/icinga/client_test.go index f884610..eac4cf6 100644 --- a/internal/icinga/client_test.go +++ b/internal/icinga/client_test.go @@ -140,75 +140,39 @@ func Test_GetApplicationMetrics(t *testing.T) { } } -func Test_GetApiListenerMetrics(t *testing.T) { +func Test_GetPerfdataMetrics(t *testing.T) { testcases := map[string]struct { - expected APIResult + expected []Perfdata + endpoint string server *httptest.Server }{ - "application": { + "api": { server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(loadTestdata(icingaTestDataAPI1)) })), - expected: APIResult{ - Results: []struct { - Name string `json:"name"` - Perfdata []Perfdata `json:"perfdata,omitempty"` - }{ - { - Name: "ApiListener", - Perfdata: []Perfdata{ - {Label: "api_num_conn_endpoints", Value: 11}, - }, - }, + endpoint: EndpointApiListener, + expected: []Perfdata{ + { + Label: "api_num_conn_endpoints", + Value: 11, }, }, }, - } - - 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.GetApiListenerMetrics() - - 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) - } - }) - } -} - -func Test_GetCheckerMetrics(t *testing.T) { - testcases := map[string]struct { - expected CheckerComponentResult - server *httptest.Server - }{ - "application": { + "checker": { 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}, - }, - }, + endpoint: EndpointCheckerComponent, + expected: []Perfdata{ + { + Label: "checkercomponent_checker_idle", + Value: 15, + }, + { + Label: "checkercomponent_checker_pending", + Value: 10, }, }, }, @@ -222,7 +186,7 @@ func Test_GetCheckerMetrics(t *testing.T) { cli, _ := NewClient(cfg) - actual, err := cli.GetCheckerComponentMetrics() + actual, err := cli.GetPerfdataMetrics(test.endpoint) if err != nil { t.Fatalf("did not expect error got:\n %+v", err) diff --git a/internal/icinga/model.go b/internal/icinga/model.go index cd9476a..6b7fbaa 100644 --- a/internal/icinga/model.go +++ b/internal/icinga/model.go @@ -6,7 +6,7 @@ type Perfdata struct { Value float64 `json:"value"` } -type APIResult struct { +type PerfdataResult struct { Results []struct { Name string `json:"name"` Perfdata []Perfdata `json:"perfdata,omitempty"`