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 }