Skip to content

Commit 5e4d4a9

Browse files
committed
feat: add cluster license collector
1 parent 763c5f8 commit 5e4d4a9

File tree

6 files changed

+346
-0
lines changed

6 files changed

+346
-0
lines changed

collector/cluster_license.go

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package collector
15+
16+
import (
17+
"encoding/json"
18+
"fmt"
19+
"io"
20+
"net/http"
21+
"net/url"
22+
"path"
23+
24+
"github.com/go-kit/log"
25+
"github.com/go-kit/log/level"
26+
"github.com/prometheus/client_golang/prometheus"
27+
)
28+
29+
type clusterLicenseMetric struct {
30+
Type prometheus.ValueType
31+
Desc *prometheus.Desc
32+
Value func(clusterLicenseStats clusterLicenseResponse) float64
33+
Labels func(clusterLicenseStats clusterLicenseResponse) []string
34+
}
35+
36+
var (
37+
defaultClusterLicenseLabels = []string{"cluster_license_type"}
38+
defaultClusterLicenseValues = func(clusterLicense clusterLicenseResponse) []string {
39+
return []string{clusterLicense.License.Type}
40+
}
41+
)
42+
43+
// License Information Struct
44+
type ClusterLicense struct {
45+
logger log.Logger
46+
client *http.Client
47+
url *url.URL
48+
49+
clusterLicenseMetrics []*clusterLicenseMetric
50+
}
51+
52+
// NewClusterLicense defines ClusterLicense Prometheus metrics
53+
func NewClusterLicense(logger log.Logger, client *http.Client, url *url.URL) *ClusterLicense {
54+
return &ClusterLicense{
55+
logger: logger,
56+
client: client,
57+
url: url,
58+
59+
clusterLicenseMetrics: []*clusterLicenseMetric{
60+
{
61+
Type: prometheus.GaugeValue,
62+
Desc: prometheus.NewDesc(
63+
prometheus.BuildFQName(namespace, "cluster_license", "max_nodes"),
64+
"The max amount of nodes allowed by the license",
65+
defaultClusterLicenseLabels, nil,
66+
),
67+
Value: func(clusterLicenseStats clusterLicenseResponse) float64 {
68+
return float64(clusterLicenseStats.License.MaxNodes)
69+
},
70+
Labels: defaultClusterLicenseValues,
71+
},
72+
{
73+
Type: prometheus.GaugeValue,
74+
Desc: prometheus.NewDesc(
75+
prometheus.BuildFQName(namespace, "cluster_license", "issue_date_in_millis"),
76+
"License issue date in milliseconds",
77+
defaultClusterLicenseLabels, nil,
78+
),
79+
Value: func(clusterLicenseStats clusterLicenseResponse) float64 {
80+
return float64(clusterLicenseStats.License.IssueDateInMillis)
81+
},
82+
Labels: defaultClusterLicenseValues,
83+
},
84+
{
85+
Type: prometheus.GaugeValue,
86+
Desc: prometheus.NewDesc(
87+
prometheus.BuildFQName(namespace, "cluster_license", "expiry_date_in_millis"),
88+
"License expiry date in milliseconds",
89+
defaultClusterLicenseLabels, nil,
90+
),
91+
Value: func(clusterLicenseStats clusterLicenseResponse) float64 {
92+
return float64(clusterLicenseStats.License.ExpiryDateInMillis)
93+
},
94+
Labels: defaultClusterLicenseValues,
95+
},
96+
{
97+
Type: prometheus.GaugeValue,
98+
Desc: prometheus.NewDesc(
99+
prometheus.BuildFQName(namespace, "cluster_license", "start_date_in_millis"),
100+
"License start date in milliseconds",
101+
defaultClusterLicenseLabels, nil,
102+
),
103+
Value: func(clusterLicenseStats clusterLicenseResponse) float64 {
104+
return float64(clusterLicenseStats.License.StartDateInMillis)
105+
},
106+
Labels: defaultClusterLicenseValues,
107+
},
108+
},
109+
}
110+
}
111+
112+
// Describe adds License metrics descriptions
113+
func (cl *ClusterLicense) Describe(ch chan<- *prometheus.Desc) {
114+
for _, metric := range cl.clusterLicenseMetrics {
115+
ch <- metric.Desc
116+
}
117+
}
118+
119+
func (cl *ClusterLicense) fetchAndDecodeClusterLicense() (clusterLicenseResponse, error) {
120+
var clr clusterLicenseResponse
121+
122+
u := *cl.url
123+
u.Path = path.Join(u.Path, "/_license")
124+
res, err := cl.client.Get(u.String())
125+
if err != nil {
126+
return clr, fmt.Errorf("failed to get license stats from %s://%s:%s%s: %s",
127+
u.Scheme, u.Hostname(), u.Port(), u.Path, err)
128+
}
129+
130+
defer func() {
131+
err = res.Body.Close()
132+
if err != nil {
133+
level.Warn(cl.logger).Log(
134+
"msg", "failed to close http.Client",
135+
"err", err,
136+
)
137+
}
138+
}()
139+
140+
if res.StatusCode != http.StatusOK {
141+
return clr, fmt.Errorf("HTTP Request failed with code %d", res.StatusCode)
142+
}
143+
144+
bts, err := io.ReadAll(res.Body)
145+
if err != nil {
146+
return clr, err
147+
}
148+
149+
if err := json.Unmarshal(bts, &clr); err != nil {
150+
return clr, err
151+
}
152+
153+
return clr, nil
154+
}
155+
156+
// Collect gets ClusterLicense metric values
157+
func (cl *ClusterLicense) Collect(ch chan<- prometheus.Metric) {
158+
159+
clusterLicenseResp, err := cl.fetchAndDecodeClusterLicense()
160+
if err != nil {
161+
level.Warn(cl.logger).Log(
162+
"msg", "failed to fetch and decode license stats",
163+
"err", err,
164+
)
165+
return
166+
}
167+
168+
for _, metric := range cl.clusterLicenseMetrics {
169+
ch <- prometheus.MustNewConstMetric(
170+
metric.Desc,
171+
metric.Type,
172+
metric.Value(clusterLicenseResp),
173+
metric.Labels(clusterLicenseResp)...,
174+
)
175+
}
176+
}

collector/cluster_license_response.go

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package collector
15+
16+
import "time"
17+
18+
type clusterLicenseResponse struct {
19+
License struct {
20+
Status string `json:"status"`
21+
UID string `json:"uid"`
22+
Type string `json:"type"`
23+
IssueDate time.Time `json:"issue_date"`
24+
IssueDateInMillis int64 `json:"issue_date_in_millis"`
25+
ExpiryDate time.Time `json:"expiry_date"`
26+
ExpiryDateInMillis int64 `json:"expiry_date_in_millis"`
27+
MaxNodes int `json:"max_nodes"`
28+
IssuedTo string `json:"issued_to"`
29+
Issuer string `json:"issuer"`
30+
StartDateInMillis int64 `json:"start_date_in_millis"`
31+
} `json:"license"`
32+
}

collector/cluster_license_test.go

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package collector
15+
16+
import (
17+
"io"
18+
"net/http"
19+
"net/http/httptest"
20+
"net/url"
21+
"os"
22+
"strings"
23+
"testing"
24+
25+
"github.com/go-kit/log"
26+
"github.com/prometheus/client_golang/prometheus/testutil"
27+
)
28+
29+
func TestIndicesHealth(t *testing.T) {
30+
// Testcases created using:
31+
// docker run -d -p 9200:9200 elasticsearch:VERSION
32+
// curl http://localhost:9200/_license
33+
tests := []struct {
34+
name string
35+
file string
36+
want string
37+
}{
38+
{
39+
name: "basic",
40+
file: "../fixtures/clusterlicense/basic.json",
41+
want: `
42+
# HELP elasticsearch_cluster_license_expiry_date_in_millis License expiry date in milliseconds
43+
# TYPE elasticsearch_cluster_license_expiry_date_in_millis gauge
44+
elasticsearch_cluster_license_expiry_date_in_millis{cluster_license_type="basic"} 0
45+
# HELP elasticsearch_cluster_license_issue_date_in_millis License issue date in milliseconds
46+
# TYPE elasticsearch_cluster_license_issue_date_in_millis gauge
47+
elasticsearch_cluster_license_issue_date_in_millis{cluster_license_type="basic"} 1.702196247064e+12
48+
# HELP elasticsearch_cluster_license_max_nodes The max amount of nodes allowed by the license
49+
# TYPE elasticsearch_cluster_license_max_nodes gauge
50+
elasticsearch_cluster_license_max_nodes{cluster_license_type="basic"} 1000
51+
# HELP elasticsearch_cluster_license_start_date_in_millis License start date in milliseconds
52+
# TYPE elasticsearch_cluster_license_start_date_in_millis gauge
53+
elasticsearch_cluster_license_start_date_in_millis{cluster_license_type="basic"} -1
54+
`,
55+
},
56+
{
57+
name: "platinum",
58+
file: "../fixtures/clusterlicense/platinum.json",
59+
want: `
60+
# HELP elasticsearch_cluster_license_expiry_date_in_millis License expiry date in milliseconds
61+
# TYPE elasticsearch_cluster_license_expiry_date_in_millis gauge
62+
elasticsearch_cluster_license_expiry_date_in_millis{cluster_license_type="platinum"} 1.714521599999e+12
63+
# HELP elasticsearch_cluster_license_issue_date_in_millis License issue date in milliseconds
64+
# TYPE elasticsearch_cluster_license_issue_date_in_millis gauge
65+
elasticsearch_cluster_license_issue_date_in_millis{cluster_license_type="platinum"} 1.6192224e+12
66+
# HELP elasticsearch_cluster_license_max_nodes The max amount of nodes allowed by the license
67+
# TYPE elasticsearch_cluster_license_max_nodes gauge
68+
elasticsearch_cluster_license_max_nodes{cluster_license_type="platinum"} 10
69+
# HELP elasticsearch_cluster_license_start_date_in_millis License start date in milliseconds
70+
# TYPE elasticsearch_cluster_license_start_date_in_millis gauge
71+
elasticsearch_cluster_license_start_date_in_millis{cluster_license_type="platinum"} 1.6192224e+12
72+
`,
73+
},
74+
}
75+
76+
for _, tt := range tests {
77+
t.Run(tt.name, func(t *testing.T) {
78+
f, err := os.Open(tt.file)
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
defer f.Close()
83+
84+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
85+
io.Copy(w, f)
86+
}))
87+
88+
defer ts.Close()
89+
90+
u, err := url.Parse(ts.URL)
91+
if err != nil {
92+
t.Fatal(err)
93+
}
94+
95+
c := NewClusterLicense(log.NewNopLogger(), http.DefaultClient, u)
96+
97+
if err := testutil.CollectAndCompare(c, strings.NewReader(tt.want)); err != nil {
98+
t.Fatal(err)
99+
}
100+
})
101+
}
102+
}

fixtures/clusterlicense/basic.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"license": {
3+
"status": "active",
4+
"uid": "redacted",
5+
"type": "basic",
6+
"issue_date": "2023-12-10T08:17:27.064Z",
7+
"issue_date_in_millis": 1702196247064,
8+
"max_nodes": 1000,
9+
"max_resource_units": null,
10+
"issued_to": "redacted",
11+
"issuer": "elasticsearch",
12+
"start_date_in_millis": -1
13+
}
14+
}

fixtures/clusterlicense/platinum.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"license": {
3+
"status": "active",
4+
"uid": "redacted",
5+
"type": "platinum",
6+
"issue_date": "2021-04-24T00:00:00.000Z",
7+
"issue_date_in_millis": 1619222400000,
8+
"expiry_date": "2024-04-30T23:59:59.999Z",
9+
"expiry_date_in_millis": 1714521599999,
10+
"max_nodes": 10,
11+
"issued_to": "redacted",
12+
"issuer": "API",
13+
"start_date_in_millis": 1619222400000
14+
}
15+
}

main.go

+7
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ func main() {
9292
esClusterInfoInterval = kingpin.Flag("es.clusterinfo.interval",
9393
"Cluster info update interval for the cluster label").
9494
Default("5m").Duration()
95+
esExportLicense = kingpin.Flag("es.license",
96+
"Export license information").
97+
Default("false").Bool()
9598
esCA = kingpin.Flag("es.ca",
9699
"Path to PEM file that contains trusted Certificate Authorities for the Elasticsearch connection.").
97100
Default("").String()
@@ -229,6 +232,10 @@ func main() {
229232
prometheus.MustRegister(collector.NewIlmIndicies(logger, httpClient, esURL))
230233
}
231234

235+
if *esExportLicense {
236+
prometheus.MustRegister(collector.NewClusterLicense(logger, httpClient, esURL))
237+
}
238+
232239
// Create a context that is cancelled on SIGKILL or SIGINT.
233240
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
234241
defer cancel()

0 commit comments

Comments
 (0)