github.com/netdata/go.d.plugin@v0.58.1/modules/elasticsearch/collect.go (about)

     1  // SPDX-License-Identifier: GPL-3.0-or-later
     2  
     3  package elasticsearch
     4  
     5  import (
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"math"
    11  	"net/http"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/netdata/go.d.plugin/pkg/stm"
    17  	"github.com/netdata/go.d.plugin/pkg/web"
    18  )
    19  
    20  const (
    21  	urlPathLocalNodeStats = "/_nodes/_local/stats"
    22  	urlPathNodesStats     = "/_nodes/stats"
    23  	urlPathIndicesStats   = "/_cat/indices"
    24  	urlPathClusterHealth  = "/_cluster/health"
    25  	urlPathClusterStats   = "/_cluster/stats"
    26  )
    27  
    28  func (es *Elasticsearch) collect() (map[string]int64, error) {
    29  	if es.clusterName == "" {
    30  		name, err := es.getClusterName()
    31  		if err != nil {
    32  			return nil, err
    33  		}
    34  		es.clusterName = name
    35  	}
    36  
    37  	ms := es.scrapeElasticsearch()
    38  	if ms.empty() {
    39  		return nil, nil
    40  	}
    41  
    42  	mx := make(map[string]int64)
    43  
    44  	es.collectNodesStats(mx, ms)
    45  	es.collectClusterHealth(mx, ms)
    46  	es.collectClusterStats(mx, ms)
    47  	es.collectLocalIndicesStats(mx, ms)
    48  
    49  	return mx, nil
    50  }
    51  
    52  func (es *Elasticsearch) collectNodesStats(mx map[string]int64, ms *esMetrics) {
    53  	if !ms.hasNodesStats() {
    54  		return
    55  	}
    56  
    57  	seen := make(map[string]bool)
    58  
    59  	for nodeID, node := range ms.NodesStats.Nodes {
    60  		seen[nodeID] = true
    61  
    62  		if !es.nodes[nodeID] {
    63  			es.nodes[nodeID] = true
    64  			es.addNodeCharts(nodeID, node)
    65  		}
    66  
    67  		merge(mx, stm.ToMap(node), "node_"+nodeID)
    68  	}
    69  
    70  	for nodeID := range es.nodes {
    71  		if !seen[nodeID] {
    72  			delete(es.nodes, nodeID)
    73  			es.removeNodeCharts(nodeID)
    74  		}
    75  	}
    76  }
    77  
    78  func (es *Elasticsearch) collectClusterHealth(mx map[string]int64, ms *esMetrics) {
    79  	if !ms.hasClusterHealth() {
    80  		return
    81  	}
    82  
    83  	es.addClusterHealthChartsOnce.Do(es.addClusterHealthCharts)
    84  
    85  	merge(mx, stm.ToMap(ms.ClusterHealth), "cluster")
    86  
    87  	mx["cluster_status_green"] = boolToInt(ms.ClusterHealth.Status == "green")
    88  	mx["cluster_status_yellow"] = boolToInt(ms.ClusterHealth.Status == "yellow")
    89  	mx["cluster_status_red"] = boolToInt(ms.ClusterHealth.Status == "red")
    90  }
    91  
    92  func (es *Elasticsearch) collectClusterStats(mx map[string]int64, ms *esMetrics) {
    93  	if !ms.hasClusterStats() {
    94  		return
    95  	}
    96  
    97  	es.addClusterStatsChartsOnce.Do(es.addClusterStatsCharts)
    98  
    99  	merge(mx, stm.ToMap(ms.ClusterStats), "cluster")
   100  }
   101  
   102  func (es *Elasticsearch) collectLocalIndicesStats(mx map[string]int64, ms *esMetrics) {
   103  	if !ms.hasLocalIndicesStats() {
   104  		return
   105  	}
   106  
   107  	seen := make(map[string]bool)
   108  
   109  	for _, v := range ms.LocalIndicesStats {
   110  		seen[v.Index] = true
   111  
   112  		if !es.indices[v.Index] {
   113  			es.indices[v.Index] = true
   114  			es.addIndexCharts(v.Index)
   115  		}
   116  
   117  		px := fmt.Sprintf("node_index_%s_stats_", v.Index)
   118  
   119  		mx[px+"health_green"] = boolToInt(v.Health == "green")
   120  		mx[px+"health_yellow"] = boolToInt(v.Health == "yellow")
   121  		mx[px+"health_red"] = boolToInt(v.Health == "red")
   122  		mx[px+"shards_count"] = strToInt(v.Rep)
   123  		mx[px+"docs_count"] = strToInt(v.DocsCount)
   124  		mx[px+"store_size_in_bytes"] = convertIndexStoreSizeToBytes(v.StoreSize)
   125  	}
   126  
   127  	for index := range es.indices {
   128  		if !seen[index] {
   129  			delete(es.indices, index)
   130  			es.removeIndexCharts(index)
   131  		}
   132  	}
   133  }
   134  
   135  func (es *Elasticsearch) scrapeElasticsearch() *esMetrics {
   136  	ms := &esMetrics{}
   137  	wg := &sync.WaitGroup{}
   138  
   139  	if es.DoNodeStats {
   140  		wg.Add(1)
   141  		go func() { defer wg.Done(); es.scrapeNodesStats(ms) }()
   142  	}
   143  	if es.DoClusterHealth {
   144  		wg.Add(1)
   145  		go func() { defer wg.Done(); es.scrapeClusterHealth(ms) }()
   146  	}
   147  	if es.DoClusterStats {
   148  		wg.Add(1)
   149  		go func() { defer wg.Done(); es.scrapeClusterStats(ms) }()
   150  	}
   151  	if !es.ClusterMode && es.DoIndicesStats {
   152  		wg.Add(1)
   153  		go func() { defer wg.Done(); es.scrapeLocalIndicesStats(ms) }()
   154  	}
   155  	wg.Wait()
   156  
   157  	return ms
   158  }
   159  
   160  func (es *Elasticsearch) scrapeNodesStats(ms *esMetrics) {
   161  	req, _ := web.NewHTTPRequest(es.Request)
   162  	if es.ClusterMode {
   163  		req.URL.Path = urlPathNodesStats
   164  	} else {
   165  		req.URL.Path = urlPathLocalNodeStats
   166  	}
   167  
   168  	var stats esNodesStats
   169  	if err := es.doOKDecode(req, &stats); err != nil {
   170  		es.Warning(err)
   171  		return
   172  	}
   173  
   174  	ms.NodesStats = &stats
   175  }
   176  
   177  func (es *Elasticsearch) scrapeClusterHealth(ms *esMetrics) {
   178  	req, _ := web.NewHTTPRequest(es.Request)
   179  	req.URL.Path = urlPathClusterHealth
   180  
   181  	var health esClusterHealth
   182  	if err := es.doOKDecode(req, &health); err != nil {
   183  		es.Warning(err)
   184  		return
   185  	}
   186  
   187  	ms.ClusterHealth = &health
   188  }
   189  
   190  func (es *Elasticsearch) scrapeClusterStats(ms *esMetrics) {
   191  	req, _ := web.NewHTTPRequest(es.Request)
   192  	req.URL.Path = urlPathClusterStats
   193  
   194  	var stats esClusterStats
   195  	if err := es.doOKDecode(req, &stats); err != nil {
   196  		es.Warning(err)
   197  		return
   198  	}
   199  
   200  	ms.ClusterStats = &stats
   201  }
   202  
   203  func (es *Elasticsearch) scrapeLocalIndicesStats(ms *esMetrics) {
   204  	req, _ := web.NewHTTPRequest(es.Request)
   205  	req.URL.Path = urlPathIndicesStats
   206  	req.URL.RawQuery = "local=true&format=json"
   207  
   208  	var stats []esIndexStats
   209  	if err := es.doOKDecode(req, &stats); err != nil {
   210  		es.Warning(err)
   211  		return
   212  	}
   213  
   214  	ms.LocalIndicesStats = removeSystemIndices(stats)
   215  }
   216  
   217  func (es *Elasticsearch) getClusterName() (string, error) {
   218  	req, _ := web.NewHTTPRequest(es.Request)
   219  
   220  	var info struct {
   221  		ClusterName string `json:"cluster_name"`
   222  	}
   223  
   224  	if err := es.doOKDecode(req, &info); err != nil {
   225  		return "", err
   226  	}
   227  
   228  	if info.ClusterName == "" {
   229  		return "", errors.New("empty cluster name")
   230  	}
   231  
   232  	return info.ClusterName, nil
   233  }
   234  
   235  func (es *Elasticsearch) doOKDecode(req *http.Request, in interface{}) error {
   236  	resp, err := es.httpClient.Do(req)
   237  	if err != nil {
   238  		return fmt.Errorf("error on HTTP request '%s': %v", req.URL, err)
   239  	}
   240  	defer closeBody(resp)
   241  
   242  	if resp.StatusCode != http.StatusOK {
   243  		return fmt.Errorf("'%s' returned HTTP status code: %d", req.URL, resp.StatusCode)
   244  	}
   245  
   246  	if err := json.NewDecoder(resp.Body).Decode(in); err != nil {
   247  		return fmt.Errorf("error on decoding response from '%s': %v", req.URL, err)
   248  	}
   249  	return nil
   250  }
   251  
   252  func closeBody(resp *http.Response) {
   253  	if resp != nil && resp.Body != nil {
   254  		_, _ = io.Copy(io.Discard, resp.Body)
   255  		_ = resp.Body.Close()
   256  	}
   257  }
   258  
   259  func convertIndexStoreSizeToBytes(size string) int64 {
   260  	var num float64
   261  	switch {
   262  	case strings.HasSuffix(size, "kb"):
   263  		num, _ = strconv.ParseFloat(size[:len(size)-2], 64)
   264  		num *= math.Pow(1024, 1)
   265  	case strings.HasSuffix(size, "mb"):
   266  		num, _ = strconv.ParseFloat(size[:len(size)-2], 64)
   267  		num *= math.Pow(1024, 2)
   268  	case strings.HasSuffix(size, "gb"):
   269  		num, _ = strconv.ParseFloat(size[:len(size)-2], 64)
   270  		num *= math.Pow(1024, 3)
   271  	case strings.HasSuffix(size, "tb"):
   272  		num, _ = strconv.ParseFloat(size[:len(size)-2], 64)
   273  		num *= math.Pow(1024, 4)
   274  	case strings.HasSuffix(size, "b"):
   275  		num, _ = strconv.ParseFloat(size[:len(size)-1], 64)
   276  	}
   277  	return int64(num)
   278  }
   279  
   280  func strToInt(s string) int64 {
   281  	v, _ := strconv.Atoi(s)
   282  	return int64(v)
   283  }
   284  
   285  func boolToInt(v bool) int64 {
   286  	if v {
   287  		return 1
   288  	}
   289  	return 0
   290  }
   291  
   292  func removeSystemIndices(indices []esIndexStats) []esIndexStats {
   293  	var i int
   294  	for _, index := range indices {
   295  		if strings.HasPrefix(index.Index, ".") {
   296  			continue
   297  		}
   298  		indices[i] = index
   299  		i++
   300  	}
   301  	return indices[:i]
   302  }
   303  
   304  func merge(dst, src map[string]int64, prefix string) {
   305  	for k, v := range src {
   306  		dst[prefix+"_"+k] = v
   307  	}
   308  }