Skip to content

Commit

Permalink
feat: add cluster license collector
Browse files Browse the repository at this point in the history
Signed-off-by: kekkokers <[email protected]>
  • Loading branch information
kekkker committed Dec 11, 2023
1 parent 763c5f8 commit 617e127
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 0 deletions.
176 changes: 176 additions & 0 deletions collector/cluster_license.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// 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"
"net/http"
"net/url"
"path"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)

type clusterLicenseMetric struct {
Type prometheus.ValueType
Desc *prometheus.Desc
Value func(clusterLicenseStats clusterLicenseResponse) float64
Labels func(clusterLicenseStats clusterLicenseResponse) []string
}

var (
defaultClusterLicenseLabels = []string{"cluster_license_type"}
defaultClusterLicenseValues = func(clusterLicense clusterLicenseResponse) []string {
return []string{clusterLicense.License.Type}
}
)

// License Information Struct
type ClusterLicense struct {
logger log.Logger
client *http.Client
url *url.URL

clusterLicenseMetrics []*clusterLicenseMetric
}

// NewClusterLicense defines ClusterLicense Prometheus metrics
func NewClusterLicense(logger log.Logger, client *http.Client, url *url.URL) *ClusterLicense {
return &ClusterLicense{
logger: logger,
client: client,
url: url,

clusterLicenseMetrics: []*clusterLicenseMetric{
{
Type: prometheus.GaugeValue,
Desc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "cluster_license", "max_nodes"),
"The max amount of nodes allowed by the license",
defaultClusterLicenseLabels, nil,
),
Value: func(clusterLicenseStats clusterLicenseResponse) float64 {
return float64(clusterLicenseStats.License.MaxNodes)
},
Labels: defaultClusterLicenseValues,
},
{
Type: prometheus.GaugeValue,
Desc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "cluster_license", "issue_date_in_millis"),
"License issue date in milliseconds",
defaultClusterLicenseLabels, nil,
),
Value: func(clusterLicenseStats clusterLicenseResponse) float64 {
return float64(clusterLicenseStats.License.IssueDateInMillis)
},
Labels: defaultClusterLicenseValues,
},
{
Type: prometheus.GaugeValue,
Desc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "cluster_license", "expiry_date_in_millis"),
"License expiry date in milliseconds",
defaultClusterLicenseLabels, nil,
),
Value: func(clusterLicenseStats clusterLicenseResponse) float64 {
return float64(clusterLicenseStats.License.ExpiryDateInMillis)
},
Labels: defaultClusterLicenseValues,
},
{
Type: prometheus.GaugeValue,
Desc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "cluster_license", "start_date_in_millis"),
"License start date in milliseconds",
defaultClusterLicenseLabels, nil,
),
Value: func(clusterLicenseStats clusterLicenseResponse) float64 {
return float64(clusterLicenseStats.License.StartDateInMillis)
},
Labels: defaultClusterLicenseValues,
},
},
}
}

// Describe adds License metrics descriptions
func (cl *ClusterLicense) Describe(ch chan<- *prometheus.Desc) {
for _, metric := range cl.clusterLicenseMetrics {
ch <- metric.Desc
}
}

func (cl *ClusterLicense) fetchAndDecodeClusterLicense() (clusterLicenseResponse, error) {
var clr clusterLicenseResponse

u := *cl.url
u.Path = path.Join(u.Path, "/_license")
res, err := cl.client.Get(u.String())
if err != nil {
return clr, fmt.Errorf("failed to get license 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 {
level.Warn(cl.logger).Log(
"msg", "failed to close http.Client",
"err", err,
)
}
}()

if res.StatusCode != http.StatusOK {
return clr, fmt.Errorf("HTTP Request failed with code %d", res.StatusCode)
}

bts, err := io.ReadAll(res.Body)
if err != nil {
return clr, err
}

if err := json.Unmarshal(bts, &clr); err != nil {
return clr, err
}

return clr, nil
}

// Collect gets ClusterLicense metric values
func (cl *ClusterLicense) Collect(ch chan<- prometheus.Metric) {

clusterLicenseResp, err := cl.fetchAndDecodeClusterLicense()
if err != nil {
level.Warn(cl.logger).Log(
"msg", "failed to fetch and decode license stats",
"err", err,
)
return
}

for _, metric := range cl.clusterLicenseMetrics {
ch <- prometheus.MustNewConstMetric(
metric.Desc,
metric.Type,
metric.Value(clusterLicenseResp),
metric.Labels(clusterLicenseResp)...,
)
}
}
32 changes: 32 additions & 0 deletions collector/cluster_license_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 "time"

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"`
}
102 changes: 102 additions & 0 deletions collector/cluster_license_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// 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 TestIndicesHealth(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: "basic",
file: "../fixtures/clusterlicense/basic.json",
want: `
# HELP elasticsearch_cluster_license_expiry_date_in_millis License expiry date in milliseconds
# TYPE elasticsearch_cluster_license_expiry_date_in_millis gauge
elasticsearch_cluster_license_expiry_date_in_millis{cluster_license_type="basic"} 0
# HELP elasticsearch_cluster_license_issue_date_in_millis License issue date in milliseconds
# TYPE elasticsearch_cluster_license_issue_date_in_millis gauge
elasticsearch_cluster_license_issue_date_in_millis{cluster_license_type="basic"} 1.702196247064e+12
# 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{cluster_license_type="basic"} 1000
# HELP elasticsearch_cluster_license_start_date_in_millis License start date in milliseconds
# TYPE elasticsearch_cluster_license_start_date_in_millis gauge
elasticsearch_cluster_license_start_date_in_millis{cluster_license_type="basic"} -1
`,
},
{
name: "platinum",
file: "../fixtures/clusterlicense/platinum.json",
want: `
# HELP elasticsearch_cluster_license_expiry_date_in_millis License expiry date in milliseconds
# TYPE elasticsearch_cluster_license_expiry_date_in_millis gauge
elasticsearch_cluster_license_expiry_date_in_millis{cluster_license_type="platinum"} 1.714521599999e+12
# HELP elasticsearch_cluster_license_issue_date_in_millis License issue date in milliseconds
# TYPE elasticsearch_cluster_license_issue_date_in_millis gauge
elasticsearch_cluster_license_issue_date_in_millis{cluster_license_type="platinum"} 1.6192224e+12
# 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{cluster_license_type="platinum"} 10
# HELP elasticsearch_cluster_license_start_date_in_millis License start date in milliseconds
# TYPE elasticsearch_cluster_license_start_date_in_millis gauge
elasticsearch_cluster_license_start_date_in_millis{cluster_license_type="platinum"} 1.6192224e+12
`,
},
}

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 := NewClusterLicense(log.NewNopLogger(), http.DefaultClient, u)

if err := testutil.CollectAndCompare(c, strings.NewReader(tt.want)); err != nil {
t.Fatal(err)
}
})
}
}
14 changes: 14 additions & 0 deletions fixtures/clusterlicense/basic.json
Original file line number Diff line number Diff line change
@@ -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
}
}
15 changes: 15 additions & 0 deletions fixtures/clusterlicense/platinum.json
Original file line number Diff line number Diff line change
@@ -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
}
}
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ func main() {
esClusterInfoInterval = kingpin.Flag("es.clusterinfo.interval",
"Cluster info update interval for the cluster label").
Default("5m").Duration()
esExportLicense = kingpin.Flag("es.license",
"Export license information").
Default("false").Bool()
esCA = kingpin.Flag("es.ca",
"Path to PEM file that contains trusted Certificate Authorities for the Elasticsearch connection.").
Default("").String()
Expand Down Expand Up @@ -229,6 +232,10 @@ func main() {
prometheus.MustRegister(collector.NewIlmIndicies(logger, httpClient, esURL))
}

if *esExportLicense {
prometheus.MustRegister(collector.NewClusterLicense(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()
Expand Down

0 comments on commit 617e127

Please sign in to comment.