diff --git a/README.md b/README.md index 4d22d104..ab73d058 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,10 @@ Further Information | elasticsearch_data_stream_stats_json_parse_failures | counter | 0 | Number of parsing failures for Data Stream stats | | elasticsearch_data_stream_backing_indices_total | gauge | 1 | Number of backing indices for Data Stream | | elasticsearch_data_stream_store_size_bytes | gauge | 1 | Current size of data stream backing indices in bytes | +| elasticsearch_cluster_license_expiry_date_seconds | gauge | 1 | License expiry date since unix epoch in second s | +| elasticsearch_cluster_license_issue_date_seconds | gauge | 1 | License issue date since unix epoch in seconds | +| elasticsearch_cluster_license_max_nodes | gauge | 1 | The max amount of nodes allowed by the license | +| elasticsearch_cluster_license_start_date_seconds | gauge | 1 | License start date since unix epoch in seconds | ### Alerts & Recording Rules diff --git a/collector/cluster_license.go b/collector/cluster_license.go new file mode 100644 index 00000000..d0a4b697 --- /dev/null +++ b/collector/cluster_license.go @@ -0,0 +1,159 @@ +// 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 ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "time" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/client_golang/prometheus" +) + +type clusterLicenseResponse struct { + License struct { + Status string `json:"status"` + UID string `json:"uid"` + Type string `json:"type"` + IssueDate time.Time `json:"issue_date"` + IssueDateInMillis int64 `json:"issue_date_in_millis"` + ExpiryDate time.Time `json:"expiry_date"` + ExpiryDateInMillis int64 `json:"expiry_date_in_millis"` + MaxNodes int `json:"max_nodes"` + IssuedTo string `json:"issued_to"` + Issuer string `json:"issuer"` + StartDateInMillis int64 `json:"start_date_in_millis"` + } `json:"license"` +} + +var ( + defaultClusterLicenseLabels = []string{"issued_to", "issuer", "type", "status"} + defaultClusterLicenseLabelsValues = func(clusterLicense clusterLicenseResponse) []string { + return []string{clusterLicense.License.IssuedTo, clusterLicense.License.Issuer, clusterLicense.License.Type, clusterLicense.License.Status} + } +) + +var ( + licenseMaxNodes = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "cluster_license", "max_nodes"), + "The max amount of nodes allowed by the license.", + defaultClusterLicenseLabels, nil, + ) + licenseIssueDate = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "cluster_license", "issue_date_seconds"), + "License issue date since unix epoch in seconds.", + defaultClusterLicenseLabels, nil, + ) + licenseExpiryDate = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "cluster_license", "expiry_date_seconds"), + "License expiry date since unix epoch in seconds.", + defaultClusterLicenseLabels, nil, + ) + licenseStartDate = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "cluster_license", "start_date_seconds"), + "License start date since unix epoch in seconds.", + defaultClusterLicenseLabels, nil, + ) +) + +func init() { + registerCollector("cluster-license", defaultDisabled, NewClusterLicense) +} + +// License Information Struct +type ClusterLicense struct { + logger log.Logger + hc *http.Client + u *url.URL +} + +func NewClusterLicense(logger log.Logger, u *url.URL, hc *http.Client) (Collector, error) { + return &ClusterLicense{ + logger: logger, + u: u, + hc: hc, + }, nil +} + +func (c *ClusterLicense) Update(ctx context.Context, ch chan<- prometheus.Metric) error { + var clr clusterLicenseResponse + + u := *c.u + u.Path = path.Join(u.Path, "/_license") + res, err := c.hc.Get(u.String()) + + if err != nil { + return err + } + + defer func() { + err = res.Body.Close() + if err != nil { + level.Warn(c.logger).Log( + "msg", "failed to close http.Client", + "err", err, + ) + } + }() + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP Request failed with code %d", res.StatusCode) + } + + bts, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + if err := json.Unmarshal(bts, &clr); err != nil { + return err + } + + ch <- prometheus.MustNewConstMetric( + licenseMaxNodes, + prometheus.GaugeValue, + float64(clr.License.MaxNodes), + defaultClusterLicenseLabelsValues(clr)..., + ) + + ch <- prometheus.MustNewConstMetric( + licenseIssueDate, + prometheus.GaugeValue, + float64(clr.License.IssueDateInMillis/1000), + defaultClusterLicenseLabelsValues(clr)..., + ) + + ch <- prometheus.MustNewConstMetric( + licenseExpiryDate, + prometheus.GaugeValue, + float64(clr.License.ExpiryDateInMillis/1000), + defaultClusterLicenseLabelsValues(clr)..., + ) + + ch <- prometheus.MustNewConstMetric( + licenseStartDate, + prometheus.GaugeValue, + float64(clr.License.StartDateInMillis/1000), + defaultClusterLicenseLabelsValues(clr)..., + ) + + return nil +} diff --git a/collector/cluster_license_test.go b/collector/cluster_license_test.go new file mode 100644 index 00000000..db531bdd --- /dev/null +++ b/collector/cluster_license_test.go @@ -0,0 +1,106 @@ +// 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/go-kit/log" + "github.com/prometheus/client_golang/prometheus/testutil" +) + +func TestClusterLicense(t *testing.T) { + // Testcases created using: + // docker run -d -p 9200:9200 elasticsearch:VERSION + // curl http://localhost:9200/_license + tests := []struct { + name string + file string + want string + }{ + { + name: "7.17.10-basic", + file: "../fixtures/clusterlicense/7.17.10-basic.json", + want: ` + # HELP elasticsearch_cluster_license_expiry_date_seconds License expiry date since unix epoch in seconds. + # TYPE elasticsearch_cluster_license_expiry_date_seconds gauge + elasticsearch_cluster_license_expiry_date_seconds{issued_to="redacted",issuer="elasticsearch",status="active",type="basic"} 0 + # HELP elasticsearch_cluster_license_issue_date_seconds License issue date since unix epoch in seconds. + # TYPE elasticsearch_cluster_license_issue_date_seconds gauge + elasticsearch_cluster_license_issue_date_seconds{issued_to="redacted",issuer="elasticsearch",status="active",type="basic"} 1.702196247e+09 + # HELP elasticsearch_cluster_license_max_nodes The max amount of nodes allowed by the license. + # TYPE elasticsearch_cluster_license_max_nodes gauge + elasticsearch_cluster_license_max_nodes{issued_to="redacted",issuer="elasticsearch",status="active",type="basic"} 1000 + # HELP elasticsearch_cluster_license_start_date_seconds License start date since unix epoch in seconds. + # TYPE elasticsearch_cluster_license_start_date_seconds gauge + elasticsearch_cluster_license_start_date_seconds{issued_to="redacted",issuer="elasticsearch",status="active",type="basic"} 0 + `, + }, + { + name: "7.17.10-platinum", + file: "../fixtures/clusterlicense/7.17.10-platinum.json", + want: ` + # HELP elasticsearch_cluster_license_expiry_date_seconds License expiry date since unix epoch in seconds. + # TYPE elasticsearch_cluster_license_expiry_date_seconds gauge + elasticsearch_cluster_license_expiry_date_seconds{issued_to="redacted",issuer="API",status="active",type="platinum"} 1.714521599e+09 + # HELP elasticsearch_cluster_license_issue_date_seconds License issue date since unix epoch in seconds. + # TYPE elasticsearch_cluster_license_issue_date_seconds gauge + elasticsearch_cluster_license_issue_date_seconds{issued_to="redacted",issuer="API",status="active",type="platinum"} 1.6192224e+09 + # HELP elasticsearch_cluster_license_max_nodes The max amount of nodes allowed by the license. + # TYPE elasticsearch_cluster_license_max_nodes gauge + elasticsearch_cluster_license_max_nodes{issued_to="redacted",issuer="API",status="active",type="platinum"} 10 + # HELP elasticsearch_cluster_license_start_date_seconds License start date since unix epoch in seconds. + # TYPE elasticsearch_cluster_license_start_date_seconds gauge + elasticsearch_cluster_license_start_date_seconds{issued_to="redacted",issuer="API",status="active",type="platinum"} 1.6192224e+09 + `, + }, + } + + 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, err := NewClusterLicense(log.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/fixtures/clusterlicense/7.17.10-basic.json b/fixtures/clusterlicense/7.17.10-basic.json new file mode 100644 index 00000000..67cdf833 --- /dev/null +++ b/fixtures/clusterlicense/7.17.10-basic.json @@ -0,0 +1,14 @@ +{ + "license": { + "status": "active", + "uid": "redacted", + "type": "basic", + "issue_date": "2023-12-10T08:17:27.064Z", + "issue_date_in_millis": 1702196247064, + "max_nodes": 1000, + "max_resource_units": null, + "issued_to": "redacted", + "issuer": "elasticsearch", + "start_date_in_millis": -1 + } +} diff --git a/fixtures/clusterlicense/7.17.10-platinum.json b/fixtures/clusterlicense/7.17.10-platinum.json new file mode 100644 index 00000000..b9b06009 --- /dev/null +++ b/fixtures/clusterlicense/7.17.10-platinum.json @@ -0,0 +1,15 @@ +{ + "license": { + "status": "active", + "uid": "redacted", + "type": "platinum", + "issue_date": "2021-04-24T00:00:00.000Z", + "issue_date_in_millis": 1619222400000, + "expiry_date": "2024-04-30T23:59:59.999Z", + "expiry_date_in_millis": 1714521599999, + "max_nodes": 10, + "issued_to": "redacted", + "issuer": "API", + "start_date_in_millis": 1619222400000 + } +}