Skip to content

Commit 16b27bc

Browse files
authored
Merge pull request #217 from taxibeat/master
Indices Settings Extension (#1)
2 parents 3c7df9a + 7efcd8c commit 16b27bc

File tree

5 files changed

+239
-0
lines changed

5 files changed

+239
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ elasticsearch_exporter --help
4747
| es.all | If true, query stats for all nodes in the cluster, rather than just the node we connect to. | false |
4848
| es.cluster_settings | If true, query stats for cluster settings. | false |
4949
| es.indices | If true, query stats for all indices in the cluster. | false |
50+
| es.indices_settings | If true, query settings stats for all indices in the cluster. | false |
5051
| es.shards | If true, query stats for all indices in the cluster, including shard-level stats (implies `es.indices=true`). | false |
5152
| es.snapshots | If true, query stats for the cluster snapshots. | false |
5253
| es.timeout | Timeout for trying to get stats from Elasticsearch. (ex: 20s) | 5s |
@@ -126,6 +127,7 @@ elasticsearch_exporter --help
126127
| elasticsearch_indices_search_query_total | counter | 1 | Total number of queries
127128
| elasticsearch_indices_segments_count | gauge | 1 | Count of index segments on this node
128129
| elasticsearch_indices_segments_memory_bytes | gauge | 1 | Current memory size of segments in bytes
130+
| elasticsearch_indices_settings_stats_read_only_indices | gauge | 1 | Count of indices that have read_only_allow_delete=true
129131
| elasticsearch_indices_shards_docs | gauge | 3 | Count of documents on this shard
130132
| elasticsearch_indices_shards_docs_deleted | gauge | 3 | Count of deleted documents on each shard
131133
| elasticsearch_indices_store_size_bytes | gauge | 1 | Current size of stored index data in bytes

collector/indices_settings.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package collector
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"path"
9+
10+
"github.com/go-kit/kit/log"
11+
"github.com/go-kit/kit/log/level"
12+
"github.com/prometheus/client_golang/prometheus"
13+
)
14+
15+
// IndicesSettings information struct
16+
type IndicesSettings struct {
17+
logger log.Logger
18+
client *http.Client
19+
url *url.URL
20+
21+
up prometheus.Gauge
22+
readOnlyIndices prometheus.Gauge
23+
totalScrapes, jsonParseFailures prometheus.Counter
24+
}
25+
26+
// NewIndicesSettings defines Indices Settings Prometheus metrics
27+
func NewIndicesSettings(logger log.Logger, client *http.Client, url *url.URL) *IndicesSettings {
28+
return &IndicesSettings{
29+
logger: logger,
30+
client: client,
31+
url: url,
32+
33+
up: prometheus.NewGauge(prometheus.GaugeOpts{
34+
Name: prometheus.BuildFQName(namespace, "indices_settings_stats", "up"),
35+
Help: "Was the last scrape of the ElasticSearch Indices Settings endpoint successful.",
36+
}),
37+
totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{
38+
Name: prometheus.BuildFQName(namespace, "indices_settings_stats", "total_scrapes"),
39+
Help: "Current total ElasticSearch Indices Settings scrapes.",
40+
}),
41+
readOnlyIndices: prometheus.NewGauge(prometheus.GaugeOpts{
42+
Name: prometheus.BuildFQName(namespace, "indices_settings_stats", "read_only_indices"),
43+
Help: "Current number of read only indices within cluster",
44+
}),
45+
jsonParseFailures: prometheus.NewCounter(prometheus.CounterOpts{
46+
Name: prometheus.BuildFQName(namespace, "indices_settings_stats", "json_parse_failures"),
47+
Help: "Number of errors while parsing JSON.",
48+
}),
49+
}
50+
}
51+
52+
// Describe add Snapshots metrics descriptions
53+
func (cs *IndicesSettings) Describe(ch chan<- *prometheus.Desc) {
54+
ch <- cs.up.Desc()
55+
ch <- cs.totalScrapes.Desc()
56+
ch <- cs.readOnlyIndices.Desc()
57+
ch <- cs.jsonParseFailures.Desc()
58+
}
59+
60+
func (cs *IndicesSettings) getAndParseURL(u *url.URL, data interface{}) error {
61+
res, err := cs.client.Get(u.String())
62+
if err != nil {
63+
return fmt.Errorf("failed to get from %s://%s:%s%s: %s",
64+
u.Scheme, u.Hostname(), u.Port(), u.Path, err)
65+
}
66+
67+
defer func() {
68+
err = res.Body.Close()
69+
if err != nil {
70+
_ = level.Warn(cs.logger).Log(
71+
"msg", "failed to close http.Client",
72+
"err", err,
73+
)
74+
}
75+
}()
76+
77+
if res.StatusCode != http.StatusOK {
78+
return fmt.Errorf("HTTP Request failed with code %d", res.StatusCode)
79+
}
80+
81+
if err := json.NewDecoder(res.Body).Decode(data); err != nil {
82+
cs.jsonParseFailures.Inc()
83+
return err
84+
}
85+
return nil
86+
}
87+
88+
func (cs *IndicesSettings) fetchAndDecodeIndicesSettings() (IndicesSettingsResponse, error) {
89+
90+
u := *cs.url
91+
u.Path = path.Join(u.Path, "/_all/_settings")
92+
var asr IndicesSettingsResponse
93+
err := cs.getAndParseURL(&u, &asr)
94+
if err != nil {
95+
return asr, err
96+
}
97+
98+
return asr, err
99+
}
100+
101+
// Collect gets all indices settings metric values
102+
func (cs *IndicesSettings) Collect(ch chan<- prometheus.Metric) {
103+
104+
cs.totalScrapes.Inc()
105+
defer func() {
106+
ch <- cs.up
107+
ch <- cs.totalScrapes
108+
ch <- cs.jsonParseFailures
109+
ch <- cs.readOnlyIndices
110+
}()
111+
112+
asr, err := cs.fetchAndDecodeIndicesSettings()
113+
if err != nil {
114+
cs.readOnlyIndices.Set(0)
115+
cs.up.Set(0)
116+
_ = level.Warn(cs.logger).Log(
117+
"msg", "failed to fetch and decode cluster settings stats",
118+
"err", err,
119+
)
120+
return
121+
}
122+
cs.up.Set(1)
123+
124+
var c int
125+
for _, value := range asr {
126+
if value.Settings.IndexInfo.Blocks.ReadOnly == "true" {
127+
c++
128+
}
129+
}
130+
cs.readOnlyIndices.Set(float64(c))
131+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package collector
2+
3+
// IndicesSettingsResponse is a representation of Elasticsearch Settings for each Index
4+
type IndicesSettingsResponse map[string]Index
5+
6+
// Index defines the struct of the tree for the settings of each index
7+
type Index struct {
8+
Settings Settings `json:"settings"`
9+
}
10+
11+
// Settings defines current index settings
12+
type Settings struct {
13+
IndexInfo IndexInfo `json:"index"`
14+
}
15+
16+
// IndexInfo defines the blocks of the current index
17+
type IndexInfo struct {
18+
Blocks Blocks `json:"blocks"`
19+
}
20+
21+
// Blocks defines whether current index has read_only_allow_delete enabled
22+
type Blocks struct {
23+
ReadOnly string `json:"read_only_allow_delete"`
24+
}

collector/indices_settings_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package collector
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/http/httptest"
7+
"net/url"
8+
"testing"
9+
10+
"github.com/go-kit/kit/log"
11+
)
12+
13+
func TestIndicesSettings(t *testing.T) {
14+
// Testcases created using:
15+
// docker run -d -p 9200:9200 elasticsearch:VERSION
16+
// curl -XPUT http://localhost:9200/twitter
17+
// curl -XPUT http://localhost:9200/facebook
18+
// curl -XPUT http://localhost:9200/instagram
19+
// curl -XPUT http://localhost:9200/viber
20+
// curl -XPUT http://localhost:9200/instagram/_settings --header "Content-Type: application/json" -d '
21+
// {
22+
// "index": {
23+
// "blocks": {
24+
// "read_only_allow_delete": "true"
25+
// }
26+
// }
27+
// }'
28+
// curl -XPUT http://localhost:9200/twitter/_settings --header "Content-Type: application/json" -d '
29+
// {
30+
// "index": {
31+
// "blocks": {
32+
// "read_only_allow_delete": "true"
33+
// }
34+
// }
35+
// }'
36+
37+
// curl http://localhost:9200/_all/_settings
38+
39+
tcs := map[string]string{
40+
"6.5.4": `{"viber":{"settings":{"index":{"creation_date":"1548066996192","number_of_shards":"5","number_of_replicas":"1","uuid":"kt2cGV-yQRaloESpqj2zsg","version":{"created":"6050499"},"provided_name":"viber"}}},"facebook":{"settings":{"index":{"creation_date":"1548066984670","number_of_shards":"5","number_of_replicas":"1","uuid":"jrU8OWQZQD--9v5eg0tjbg","version":{"created":"6050499"},"provided_name":"facebook"}}},"twitter":{"settings":{"index":{"number_of_shards":"5","blocks":{"read_only_allow_delete":"true"},"provided_name":"twitter","creation_date":"1548066697559","number_of_replicas":"1","uuid":"-sqtc4fVRrS2jHJCZ2hQ9Q","version":{"created":"6050499"}}}},"instagram":{"settings":{"index":{"number_of_shards":"5","blocks":{"read_only_allow_delete":"true"},"provided_name":"instagram","creation_date":"1548066991932","number_of_replicas":"1","uuid":"WeGWaxa_S3KrgE5SZHolTw","version":{"created":"6050499"}}}}}`,
41+
}
42+
for ver, out := range tcs {
43+
for hn, handler := range map[string]http.Handler{
44+
"plain": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45+
fmt.Fprintln(w, out)
46+
}),
47+
} {
48+
ts := httptest.NewServer(handler)
49+
defer ts.Close()
50+
51+
u, err := url.Parse(ts.URL)
52+
if err != nil {
53+
t.Fatalf("Failed to parse URL: %s", err)
54+
}
55+
c := NewIndicesSettings(log.NewNopLogger(), http.DefaultClient, u)
56+
nsr, err := c.fetchAndDecodeIndicesSettings()
57+
if err != nil {
58+
t.Fatalf("Failed to fetch or decode indices settings: %s", err)
59+
}
60+
t.Logf("[%s/%s] All Indices Settings Response: %+v", hn, ver, nsr)
61+
// if nsr.Cluster.Routing.Allocation.Enabled != "ALL" {
62+
// t.Errorf("Wrong setting for cluster routing allocation enabled")
63+
// }
64+
var counter int
65+
for key, value := range nsr {
66+
if value.Settings.IndexInfo.Blocks.ReadOnly == "true" {
67+
counter++
68+
if key != "instagram" && key != "twitter" {
69+
t.Errorf("Wrong read_only index")
70+
}
71+
}
72+
}
73+
if counter != 2 {
74+
t.Errorf("Wrong number of read_only indexes")
75+
}
76+
}
77+
}
78+
}

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func main() {
2525
esNode = flag.String("es.node", "_local", "Node's name of which metrics should be exposed.")
2626
esExportIndices = flag.Bool("es.indices", false, "Export stats for indices in the cluster.")
2727
esExportClusterSettings = flag.Bool("es.cluster_settings", false, "Export stats for cluster settings.")
28+
esExportIndicesSettings = flag.Bool("es.indices_settings", false, "Export stats for settings of all indices of the cluster.")
2829
esExportShards = flag.Bool("es.shards", false, "Export stats for shards in the cluster (implies es.indices=true).")
2930
esExportSnapshots = flag.Bool("es.snapshots", false, "Export stats for the cluster snapshots.")
3031
esCA = flag.String("es.ca", "", "Path to PEM file that contains trusted Certificate Authorities for the Elasticsearch connection.")
@@ -83,6 +84,9 @@ func main() {
8384
if *esExportClusterSettings {
8485
prometheus.MustRegister(collector.NewClusterSettings(logger, httpClient, esURL))
8586
}
87+
if *esExportIndicesSettings {
88+
prometheus.MustRegister(collector.NewIndicesSettings(logger, httpClient, esURL))
89+
}
8690
http.Handle(*metricsPath, prometheus.Handler())
8791
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
8892
_, err = w.Write([]byte(`<html>

0 commit comments

Comments
 (0)