diff --git a/CHANGELOG.md b/CHANGELOG.md index 940dfe9f..b87a17cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,12 @@ BREAKING CHANGES: The flag `--es.slm` has been renamed to `--collector.slm`. +The flag `--es.ilm` has been renamed to `--collector.ilm`. The logging system has been replaced with log/slog from the stdlib. This change is being made across the prometheus ecosystem. The logging output has changed, but the messages and levels remain the same. The `ts` label for the timestamp has bewen replaced with `time`, the accuracy is less, and the timezone is not forced to UTC. The `caller` field has been replaced by the `source` field, which now includes the full path to the source file. The `level` field now exposes the log level in capital letters. * [CHANGE] Rename --es.slm to --collector.slm #932 +* [CHANGE] Rename --es.ilm to --collector.ilm #XXX * [CHANGE] Replace logging system #942 ## 1.8.0 / 2024-09-14 diff --git a/collector/ilm.go b/collector/ilm.go new file mode 100644 index 00000000..b91a90dd --- /dev/null +++ b/collector/ilm.go @@ -0,0 +1,129 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "net/url" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + ilmStatusOptions = []string{"STOPPED", "RUNNING", "STOPPING"} + + ilmIndexStatus = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "ilm_index", "status"), + "Status of ILM policy for index", + []string{"index", "phase", "action", "step"}, nil) + + ilmStatus = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "ilm", "status"), + "Current status of ilm. Status can be STOPPED, RUNNING, STOPPING.", + []string{"operation_mode"}, nil, + ) +) + +func init() { + registerCollector("ilm", defaultDisabled, NewILM) +} + +type ILM struct { + logger *slog.Logger + hc *http.Client + u *url.URL +} + +func NewILM(logger *slog.Logger, u *url.URL, hc *http.Client) (Collector, error) { + return &ILM{ + logger: logger, + hc: hc, + u: u, + }, nil +} + +type IlmResponse struct { + Indices map[string]IlmIndexResponse `json:"indices"` +} + +type IlmIndexResponse struct { + Index string `json:"index"` + Managed bool `json:"managed"` + Phase string `json:"phase"` + Action string `json:"action"` + Step string `json:"step"` + StepTimeMillis float64 `json:"step_time_millis"` +} + +type IlmStatusResponse struct { + OperationMode string `json:"operation_mode"` +} + +func (i *ILM) Update(ctx context.Context, ch chan<- prometheus.Metric) error { + var ir IlmResponse + + indexURL := i.u.ResolveReference(&url.URL{Path: "/_all/_ilm/explain"}) + + indexResp, err := getURL(ctx, i.hc, i.logger, indexURL.String()) + if err != nil { + return fmt.Errorf("failed to load ILM url: %w", err) + } + + if err := json.Unmarshal(indexResp, &ir); err != nil { + return fmt.Errorf("failed to decode JSON body: %w", err) + } + + var isr IlmStatusResponse + + indexStatusURL := i.u.ResolveReference(&url.URL{Path: "/_ilm/status"}) + + indexStatusResp, err := getURL(ctx, i.hc, i.logger, indexStatusURL.String()) + if err != nil { + return fmt.Errorf("failed to load ILM url: %w", err) + } + + if err := json.Unmarshal(indexStatusResp, &isr); err != nil { + return fmt.Errorf("failed to decode JSON body: %w", err) + } + + for name, ilm := range ir.Indices { + ch <- prometheus.MustNewConstMetric( + ilmIndexStatus, + prometheus.GaugeValue, + bool2Float(ilm.Managed), + name, ilm.Phase, ilm.Action, ilm.Step, + ) + } + + for _, status := range ilmStatusOptions { + statusActive := false + if isr.OperationMode == status { + statusActive = true + } + + ch <- prometheus.MustNewConstMetric( + ilmStatus, + prometheus.GaugeValue, + bool2Float(statusActive), + status, + ) + + } + + return nil +} diff --git a/collector/ilm_indices.go b/collector/ilm_indices.go deleted file mode 100644 index 9f30c726..00000000 --- a/collector/ilm_indices.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2023 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package collector - -import ( - "encoding/json" - "fmt" - "io" - "log/slog" - "net/http" - "net/url" - "path" - - "github.com/prometheus/client_golang/prometheus" -) - -type ilmMetric struct { - Type prometheus.ValueType - Desc *prometheus.Desc - Value func(timeMillis float64) float64 - Labels []string -} - -// Index Lifecycle Management information object -type IlmIndiciesCollector struct { - logger *slog.Logger - client *http.Client - url *url.URL - - ilmMetric ilmMetric -} - -type IlmResponse struct { - Indices map[string]IlmIndexResponse `json:"indices"` -} - -type IlmIndexResponse struct { - Index string `json:"index"` - Managed bool `json:"managed"` - Phase string `json:"phase"` - Action string `json:"action"` - Step string `json:"step"` - StepTimeMillis float64 `json:"step_time_millis"` -} - -var ( - defaultIlmIndicesMappingsLabels = []string{"index", "phase", "action", "step"} -) - -// NewIlmIndicies defines Index Lifecycle Management Prometheus metrics -func NewIlmIndicies(logger *slog.Logger, client *http.Client, url *url.URL) *IlmIndiciesCollector { - subsystem := "ilm_index" - - return &IlmIndiciesCollector{ - logger: logger, - client: client, - url: url, - - ilmMetric: ilmMetric{ - Type: prometheus.GaugeValue, - Desc: prometheus.NewDesc( - prometheus.BuildFQName(namespace, subsystem, "status"), - "Status of ILM policy for index", - defaultIlmIndicesMappingsLabels, nil), - Value: func(timeMillis float64) float64 { - return timeMillis - }, - }, - } -} - -// Describe adds metrics description -func (i *IlmIndiciesCollector) Describe(ch chan<- *prometheus.Desc) { - ch <- i.ilmMetric.Desc -} - -func (i *IlmIndiciesCollector) fetchAndDecodeIlm() (IlmResponse, error) { - var ir IlmResponse - - u := *i.url - u.Path = path.Join(u.Path, "/_all/_ilm/explain") - - res, err := i.client.Get(u.String()) - if err != nil { - return ir, fmt.Errorf("failed to get index stats from %s://%s:%s%s: %s", - u.Scheme, u.Hostname(), u.Port(), u.Path, err) - } - - defer func() { - err = res.Body.Close() - if err != nil { - i.logger.Warn( - "failed to close http.Client", - "err", err, - ) - } - }() - - if res.StatusCode != http.StatusOK { - return ir, fmt.Errorf("HTTP Request failed with code %d", res.StatusCode) - } - - bts, err := io.ReadAll(res.Body) - if err != nil { - return ir, err - } - - if err := json.Unmarshal(bts, &ir); err != nil { - return ir, err - } - - return ir, nil -} - -func bool2int(managed bool) float64 { - if managed { - return 1 - } - return 0 -} - -// Collect pulls metric values from Elasticsearch -func (i *IlmIndiciesCollector) Collect(ch chan<- prometheus.Metric) { - // indices - ilmResp, err := i.fetchAndDecodeIlm() - if err != nil { - i.logger.Warn( - "failed to fetch and decode ILM stats", - "err", err, - ) - return - } - - for indexName, indexIlm := range ilmResp.Indices { - ch <- prometheus.MustNewConstMetric( - i.ilmMetric.Desc, - i.ilmMetric.Type, - i.ilmMetric.Value(bool2int(indexIlm.Managed)), - indexName, indexIlm.Phase, indexIlm.Action, indexIlm.Step, - ) - } -} diff --git a/collector/ilm_indices_test.go b/collector/ilm_indices_test.go deleted file mode 100644 index b091f6fb..00000000 --- a/collector/ilm_indices_test.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2023 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package collector - -import ( - "io" - "net/http" - "net/http/httptest" - "net/url" - "os" - "strings" - "testing" - - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/prometheus/common/promslog" -) - -func TestILMMetrics(t *testing.T) { - // Testcases created using: - // docker run -d -p 9200:9200 elasticsearch:VERSION - // curl -XPUT http://localhost:9200/twitter - // curl -X PUT "localhost:9200/_ilm/policy/my_policy?pretty" -H 'Content-Type: application/json' -d' - // { - // "policy": { - // "phases": { - // "warm": { - // "min_age": "10d", - // "actions": { - // "forcemerge": { - // "max_num_segments": 1 - // } - // } - // }, - // "delete": { - // "min_age": "30d", - // "actions": { - // "delete": {} - // } - // } - // } - // } - // } - // ' - // curl -X PUT "localhost:9200/facebook?pretty" -H 'Content-Type: application/json' -d' - // { - // "settings": { - // "index": { - // "lifecycle": { - // "name": "my_policy" - // } - // } - // } - // } - // ' - // curl http://localhost:9200/_all/_ilm/explain - tests := []struct { - name string - file string - want string - }{ - { - name: "6.6.0", - file: "../fixtures/ilm_indices/6.6.0.json", - want: ` -# HELP elasticsearch_ilm_index_status Status of ILM policy for index -# TYPE elasticsearch_ilm_index_status gauge -elasticsearch_ilm_index_status{action="",index="twitter",phase="",step=""} 0 -elasticsearch_ilm_index_status{action="complete",index="facebook",phase="new",step="complete"} 1 - `, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f, err := os.Open(tt.file) - if err != nil { - t.Fatal(err) - } - defer f.Close() - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - io.Copy(w, f) - })) - defer ts.Close() - - u, err := url.Parse(ts.URL) - if err != nil { - t.Fatal(err) - } - - c := NewIlmIndicies(promslog.NewNopLogger(), http.DefaultClient, u) - if err != nil { - t.Fatal(err) - } - - if err := testutil.CollectAndCompare(c, strings.NewReader(tt.want)); err != nil { - t.Fatal(err) - } - }) - } -} diff --git a/collector/ilm_status.go b/collector/ilm_status.go deleted file mode 100644 index 067e375c..00000000 --- a/collector/ilm_status.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2023 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package collector - -import ( - "encoding/json" - "fmt" - "io" - "log/slog" - "net/http" - "net/url" - "path" - - "github.com/prometheus/client_golang/prometheus" -) - -var ( - ilmStatuses = []string{"STOPPED", "RUNNING", "STOPPING"} -) - -type ilmStatusMetric struct { - Type prometheus.ValueType - Desc *prometheus.Desc - Value func(ilm *IlmStatusResponse, status string) float64 - Labels func(status string) []string -} - -// IlmStatusCollector information struct -type IlmStatusCollector struct { - logger *slog.Logger - client *http.Client - url *url.URL - - metric ilmStatusMetric -} - -type IlmStatusResponse struct { - OperationMode string `json:"operation_mode"` -} - -// NewIlmStatus defines Indices IndexIlms Prometheus metrics -func NewIlmStatus(logger *slog.Logger, client *http.Client, url *url.URL) *IlmStatusCollector { - subsystem := "ilm" - - return &IlmStatusCollector{ - logger: logger, - client: client, - url: url, - - metric: ilmStatusMetric{ - Type: prometheus.GaugeValue, - Desc: prometheus.NewDesc( - prometheus.BuildFQName(namespace, subsystem, "status"), - "Current status of ilm. Status can be STOPPED, RUNNING, STOPPING.", - []string{"operation_mode"}, nil, - ), - Value: func(ilm *IlmStatusResponse, status string) float64 { - if ilm.OperationMode == status { - return 1 - } - return 0 - }, - }, - } -} - -// Describe add Snapshots metrics descriptions -func (im *IlmStatusCollector) Describe(ch chan<- *prometheus.Desc) { - ch <- im.metric.Desc -} - -func (im *IlmStatusCollector) fetchAndDecodeIlm() (*IlmStatusResponse, error) { - u := *im.url - u.Path = path.Join(im.url.Path, "/_ilm/status") - - res, err := im.client.Get(u.String()) - if err != nil { - return nil, fmt.Errorf("failed to get from %s://%s:%s%s: %s", - u.Scheme, u.Hostname(), u.Port(), u.Path, err) - } - - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP Request failed with code %d", res.StatusCode) - } - - body, err := io.ReadAll(res.Body) - if err != nil { - im.logger.Warn("failed to read response body", "err", err) - return nil, err - } - - err = res.Body.Close() - if err != nil { - im.logger.Warn("failed to close response body", "err", err) - return nil, err - } - - var imr IlmStatusResponse - if err := json.Unmarshal(body, &imr); err != nil { - return nil, err - } - - return &imr, nil -} - -// Collect gets all indices Ilms metric values -func (im *IlmStatusCollector) Collect(ch chan<- prometheus.Metric) { - indicesIlmsResponse, err := im.fetchAndDecodeIlm() - if err != nil { - im.logger.Warn( - "failed to fetch and decode cluster ilm status", - "err", err, - ) - return - } - - for _, status := range ilmStatuses { - ch <- prometheus.MustNewConstMetric( - im.metric.Desc, - im.metric.Type, - im.metric.Value(indicesIlmsResponse, status), - status, - ) - } - -} diff --git a/collector/ilm_status_test.go b/collector/ilm_status_test.go deleted file mode 100644 index cc7e9ecb..00000000 --- a/collector/ilm_status_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2023 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package collector - -import ( - "io" - "net/http" - "net/http/httptest" - "net/url" - "os" - "strings" - "testing" - - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/prometheus/common/promslog" -) - -func TestILMStatus(t *testing.T) { - // Testcases created using: - // docker run -d -p 9200:9200 elasticsearch:VERSION - // curl http://localhost:9200/_ilm/status - tests := []struct { - name string - file string - want string - }{ - { - name: "6.6.0", - file: "../fixtures/ilm_status/6.6.0.json", - want: ` -# HELP elasticsearch_ilm_status Current status of ilm. Status can be STOPPED, RUNNING, STOPPING. -# TYPE elasticsearch_ilm_status gauge -elasticsearch_ilm_status{operation_mode="RUNNING"} 1 -elasticsearch_ilm_status{operation_mode="STOPPED"} 0 -elasticsearch_ilm_status{operation_mode="STOPPING"} 0 - `, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f, err := os.Open(tt.file) - if err != nil { - t.Fatal(err) - } - defer f.Close() - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - io.Copy(w, f) - })) - defer ts.Close() - - u, err := url.Parse(ts.URL) - if err != nil { - t.Fatal(err) - } - - c := NewIlmStatus(promslog.NewNopLogger(), http.DefaultClient, u) - if err != nil { - t.Fatal(err) - } - - if err := testutil.CollectAndCompare(c, strings.NewReader(tt.want)); err != nil { - t.Fatal(err) - } - }) - } -} diff --git a/collector/ilm_test.go b/collector/ilm_test.go new file mode 100644 index 00000000..c769b7e9 --- /dev/null +++ b/collector/ilm_test.go @@ -0,0 +1,97 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "strings" + "testing" + + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/prometheus/common/promslog" +) + +func TestILM(t *testing.T) { + tests := []struct { + name string + file string + want string + }{ + { + name: "6.6.0", + file: "6.6.0.json", + want: ` + # HELP elasticsearch_ilm_index_status Status of ILM policy for index + # TYPE elasticsearch_ilm_index_status gauge + elasticsearch_ilm_index_status{action="",index="twitter",phase="",step=""} 0 + elasticsearch_ilm_index_status{action="complete",index="facebook",phase="new",step="complete"} 1 + # HELP elasticsearch_ilm_status Current status of ilm. Status can be STOPPED, RUNNING, STOPPING. + # TYPE elasticsearch_ilm_status gauge + elasticsearch_ilm_status{operation_mode="RUNNING"} 1 + elasticsearch_ilm_status{operation_mode="STOPPED"} 0 + elasticsearch_ilm_status{operation_mode="STOPPING"} 0 + `, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + indexF, err := os.Open(path.Join("../fixtures/ilm_indices", tt.file)) + if err != nil { + t.Fatal(err) + + } + defer indexF.Close() + + statusF, err := os.Open(path.Join("../fixtures/ilm_status", tt.file)) + if err != nil { + t.Fatal(err) + + } + defer statusF.Close() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sm := http.NewServeMux() + sm.HandleFunc("/_all/_ilm/explain", func(w http.ResponseWriter, r *http.Request) { + io.Copy(w, indexF) + }) + sm.HandleFunc("/_ilm/status", func(w http.ResponseWriter, r *http.Request) { + io.Copy(w, statusF) + }) + + sm.ServeHTTP(w, r) + + })) + defer ts.Close() + + u, err := url.Parse(ts.URL) + if err != nil { + t.Fatal(err) + } + + c, err := NewILM(promslog.NewNopLogger(), u, http.DefaultClient) + if err != nil { + t.Fatal(err) + } + + if err := testutil.CollectAndCompare(wrapCollector{c}, strings.NewReader(tt.want)); err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/collector/util.go b/collector/util.go index a2df06ed..7aef9a21 100644 --- a/collector/util.go +++ b/collector/util.go @@ -53,3 +53,11 @@ func getURL(ctx context.Context, hc *http.Client, log *slog.Logger, u string) ([ return b, nil } + +// bool2Float converts a bool to a float64. True is 1, false is 0. +func bool2Float(managed bool) float64 { + if managed { + return 1 + } + return 0 +} diff --git a/main.go b/main.go index 663fb772..3e34f3be 100644 --- a/main.go +++ b/main.go @@ -81,9 +81,6 @@ func main() { esExportIndexAliases = kingpin.Flag("es.aliases", "Export informational alias metrics."). Default("true").Bool() - esExportILM = kingpin.Flag("es.ilm", - "Export index lifecycle politics for indices in the cluster."). - Default("false").Bool() esExportShards = kingpin.Flag("es.shards", "Export stats for shards in the cluster (implies --es.indices)."). Default("false").Bool() @@ -229,11 +226,6 @@ func main() { prometheus.MustRegister(collector.NewIndicesMappings(logger, httpClient, esURL)) } - if *esExportILM { - prometheus.MustRegister(collector.NewIlmStatus(logger, httpClient, esURL)) - prometheus.MustRegister(collector.NewIlmIndicies(logger, httpClient, esURL)) - } - // Create a context that is cancelled on SIGKILL or SIGINT. ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) defer cancel()