github.com/mackerelio/mackerel-agent-plugins@v0.89.3/mackerel-plugin-elasticsearch/lib/elasticsearch.go (about)

     1  package mpelasticsearch
     2  
     3  import (
     4  	"crypto/tls"
     5  	"encoding/json"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"net/http"
    10  
    11  	mp "github.com/mackerelio/go-mackerel-plugin"
    12  	"github.com/mackerelio/golib/logging"
    13  	"golang.org/x/text/cases"
    14  	"golang.org/x/text/language"
    15  )
    16  
    17  var logger = logging.GetLogger("metrics.plugin.elasticsearch")
    18  
    19  var metricPlace = map[string][]string{
    20  	"http_opened":                 {"http", "total_opened"},
    21  	"total_indexing_index":        {"indices", "indexing", "index_total"},
    22  	"total_indexing_delete":       {"indices", "indexing", "delete_total"},
    23  	"total_get":                   {"indices", "get", "total"},
    24  	"total_search_query":          {"indices", "search", "query_total"},
    25  	"total_search_fetch":          {"indices", "search", "fetch_total"},
    26  	"total_merges":                {"indices", "merges", "total"},
    27  	"total_refresh":               {"indices", "refresh", "total"},
    28  	"total_flush":                 {"indices", "flush", "total"},
    29  	"total_warmer":                {"indices", "warmer", "total"},
    30  	"total_percolate":             {"indices", "percolate", "total"}, // MISSINGv7 = no value after v7.0 (at least)
    31  	"total_suggest":               {"indices", "suggest", "total"},   // MISSINGv7
    32  	"docs_count":                  {"indices", "docs", "count"},
    33  	"docs_deleted":                {"indices", "docs", "deleted"},
    34  	"fielddata_size":              {"indices", "fielddata", "memory_size_in_bytes"},
    35  	"filter_cache_size":           {"indices", "filter_cache", "memory_size_in_bytes"}, // MISSINGv7
    36  	"segments_size":               {"indices", "segments", "memory_in_bytes"},
    37  	"segments_index_writer_size":  {"indices", "segments", "index_writer_memory_in_bytes"},
    38  	"segments_version_map_size":   {"indices", "segments", "version_map_memory_in_bytes"},
    39  	"segments_fixed_bit_set_size": {"indices", "segments", "fixed_bit_set_memory_in_bytes"},
    40  	"evictions_fielddata":         {"indices", "fielddata", "evictions"},
    41  	"evictions_filter_cache":      {"indices", "filter_cache", "evictions"}, // MISSINGv7
    42  	"heap_used":                   {"jvm", "mem", "heap_used_in_bytes"},
    43  	"heap_max":                    {"jvm", "mem", "heap_max_in_bytes"},
    44  	"threads_generic":             {"thread_pool", "generic", "threads"},
    45  	"threads_index":               {"thread_pool", "index", "threads"},         // MISSINGv7
    46  	"threads_snapshot_data":       {"thread_pool", "snapshot_data", "threads"}, // MISSINGv7
    47  	"threads_get":                 {"thread_pool", "get", "threads"},
    48  	"threads_bench":               {"thread_pool", "bench", "threads"}, // MISSINGv7
    49  	"threads_snapshot":            {"thread_pool", "snapshot", "threads"},
    50  	"threads_merge":               {"thread_pool", "merge", "threads"},    // MISSINGv7
    51  	"threads_suggest":             {"thread_pool", "suggest", "threads"},  // MISSINGv7
    52  	"threads_bulk":                {"thread_pool", "bulk", "threads"},     // MISSINGv7
    53  	"threads_optimize":            {"thread_pool", "optimize", "threads"}, // MISSINGv7
    54  	"threads_warmer":              {"thread_pool", "warmer", "threads"},
    55  	"threads_flush":               {"thread_pool", "flush", "threads"},
    56  	"threads_search":              {"thread_pool", "search", "threads"},
    57  	"threads_percolate":           {"thread_pool", "percolate", "threads"}, // MISSINGv7
    58  	"threads_refresh":             {"thread_pool", "refresh", "threads"},
    59  	"threads_management":          {"thread_pool", "management", "threads"},
    60  	"threads_fetch_shard_started": {"thread_pool", "fetch_shard_started", "threads"},
    61  	"threads_fetch_shard_store":   {"thread_pool", "fetch_shard_store", "threads"},
    62  	"threads_listener":            {"thread_pool", "listener", "threads"}, // MISSINGv8
    63  	"count_rx":                    {"transport", "rx_count"},
    64  	"count_tx":                    {"transport", "tx_count"},
    65  	"open_file_descriptors":       {"process", "open_file_descriptors"},
    66  	"compilations":                {"script", "compilations"},
    67  	"cache_evictions":             {"script", "cache_evictions"},
    68  	"compilation_limit_triggered": {"script", "compilation_limit_triggered"},
    69  }
    70  
    71  func getFloatValue(s map[string]interface{}, keys []string) (float64, error) {
    72  	var val float64
    73  	sm := s
    74  	for i, k := range keys {
    75  		if i+1 < len(keys) {
    76  			switch sm[k].(type) {
    77  			case map[string]interface{}:
    78  				sm = sm[k].(map[string]interface{})
    79  			default:
    80  				return 0, errors.New("Cannot handle as a hash") // nolint
    81  			}
    82  		} else {
    83  			switch sm[k].(type) {
    84  			case float64:
    85  				val = sm[k].(float64)
    86  			default:
    87  				return 0, errors.New("Not float64") // nolint
    88  			}
    89  		}
    90  	}
    91  
    92  	return val, nil
    93  }
    94  
    95  // ElasticsearchPlugin mackerel plugin for Elasticsearch
    96  type ElasticsearchPlugin struct {
    97  	URI                  string
    98  	Prefix               string
    99  	LabelPrefix          string
   100  	Insecure             bool
   101  	User                 string
   102  	Password             string
   103  	SuppressMissingError bool
   104  }
   105  
   106  // FetchMetrics interface for mackerelplugin
   107  func (p ElasticsearchPlugin) FetchMetrics() (map[string]float64, error) {
   108  	req, err := http.NewRequest(http.MethodGet, p.URI+"/_nodes/_local/stats", nil)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	req.Header.Set("User-Agent", "mackerel-plugin-elasticsearch")
   113  	if p.User != "" && p.Password != "" {
   114  		req.SetBasicAuth(p.User, p.Password)
   115  	}
   116  	client := http.Client{
   117  		Transport: &http.Transport{
   118  			TLSClientConfig: &tls.Config{InsecureSkipVerify: p.Insecure},
   119  		},
   120  	}
   121  	resp, err := client.Do(req)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	defer resp.Body.Close()
   126  
   127  	stat := make(map[string]float64)
   128  	decoder := json.NewDecoder(resp.Body)
   129  
   130  	var s map[string]interface{}
   131  	err = decoder.Decode(&s)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  
   136  	nodes := s["nodes"].(map[string]interface{})
   137  	n := ""
   138  	for k := range nodes {
   139  		if n != "" {
   140  			return nil, errors.New("Multiple node found") // nolint
   141  		}
   142  		n = k
   143  	}
   144  	node := nodes[n].(map[string]interface{})
   145  
   146  	for k, v := range metricPlace {
   147  		val, err := getFloatValue(node, v)
   148  		if err != nil {
   149  			if !p.SuppressMissingError {
   150  				logger.Errorf("Failed to find '%s': %s", k, err)
   151  			}
   152  			continue
   153  		}
   154  
   155  		stat[k] = val
   156  	}
   157  
   158  	return stat, nil
   159  }
   160  
   161  // GraphDefinition interface for mackerelplugin
   162  func (p ElasticsearchPlugin) GraphDefinition() map[string]mp.Graphs {
   163  	var graphdef = map[string]mp.Graphs{
   164  		p.Prefix + ".http": {
   165  			Label: (p.LabelPrefix + " HTTP"),
   166  			Unit:  "integer",
   167  			Metrics: []mp.Metrics{
   168  				{Name: "http_opened", Label: "Opened", Diff: true},
   169  			},
   170  		},
   171  		p.Prefix + ".indices": {
   172  			Label: (p.LabelPrefix + " Indices"),
   173  			Unit:  "integer",
   174  			Metrics: []mp.Metrics{
   175  				{Name: "total_indexing_index", Label: "Indexing-Index", Diff: true, Stacked: true},
   176  				{Name: "total_indexing_delete", Label: "Indexing-Delete", Diff: true, Stacked: true},
   177  				{Name: "total_get", Label: "Get", Diff: true, Stacked: true},
   178  				{Name: "total_search_query", Label: "Search-Query", Diff: true, Stacked: true},
   179  				{Name: "total_search_fetch", Label: "Search-fetch", Diff: true, Stacked: true},
   180  				{Name: "total_merges", Label: "Merges", Diff: true, Stacked: true},
   181  				{Name: "total_refresh", Label: "Refresh", Diff: true, Stacked: true},
   182  				{Name: "total_flush", Label: "Flush", Diff: true, Stacked: true},
   183  				{Name: "total_warmer", Label: "Warmer", Diff: true, Stacked: true},
   184  				{Name: "total_percolate", Label: "Percolate", Diff: true, Stacked: true},
   185  				{Name: "total_suggest", Label: "Suggest", Diff: true, Stacked: true},
   186  			},
   187  		},
   188  		p.Prefix + ".indices.docs": {
   189  			Label: (p.LabelPrefix + " Indices Docs"),
   190  			Unit:  "integer",
   191  			Metrics: []mp.Metrics{
   192  				{Name: "docs_count", Label: "Count", Stacked: true},
   193  				{Name: "docs_deleted", Label: "Deleted", Stacked: true},
   194  			},
   195  		},
   196  		p.Prefix + ".indices.memory_size": {
   197  			Label: (p.LabelPrefix + " Indices Memory Size"),
   198  			Unit:  "bytes",
   199  			Metrics: []mp.Metrics{
   200  				{Name: "fielddata_size", Label: "Fielddata", Stacked: true},
   201  				{Name: "filter_cache_size", Label: "Filter Cache", Stacked: true},
   202  				{Name: "segments_size", Label: "Lucene Segments", Stacked: true},
   203  				{Name: "segments_index_writer_size", Label: "Lucene Segments Index Writer", Stacked: true},
   204  				{Name: "segments_version_map_size", Label: "Lucene Segments Version Map", Stacked: true},
   205  				{Name: "segments_fixed_bit_set_size", Label: "Lucene Segments Fixed Bit Set", Stacked: true},
   206  			},
   207  		},
   208  		p.Prefix + ".indices.evictions": {
   209  			Label: (p.LabelPrefix + " Indices Evictions"),
   210  			Unit:  "integer",
   211  			Metrics: []mp.Metrics{
   212  				{Name: "evictions_fielddata", Label: "Fielddata", Diff: true},
   213  				{Name: "evictions_filter_cache", Label: "Filter Cache", Diff: true},
   214  			},
   215  		},
   216  		p.Prefix + ".jvm.heap": {
   217  			Label: (p.LabelPrefix + " JVM Heap Mem"),
   218  			Unit:  "bytes",
   219  			Metrics: []mp.Metrics{
   220  				{Name: "heap_used", Label: "Used"},
   221  				{Name: "heap_max", Label: "Max"},
   222  			},
   223  		},
   224  		p.Prefix + ".thread_pool.threads": {
   225  			Label: (p.LabelPrefix + " Thread-Pool Threads"),
   226  			Unit:  "integer",
   227  			Metrics: []mp.Metrics{
   228  				{Name: "threads_generic", Label: "Generic", Stacked: true},
   229  				{Name: "threads_index", Label: "Index", Stacked: true},
   230  				{Name: "threads_snapshot_data", Label: "Snapshot Data", Stacked: true},
   231  				{Name: "threads_get", Label: "Get", Stacked: true},
   232  				{Name: "threads_bench", Label: "Bench", Stacked: true},
   233  				{Name: "threads_snapshot", Label: "Snapshot", Stacked: true},
   234  				{Name: "threads_merge", Label: "Merge", Stacked: true},
   235  				{Name: "threads_suggest", Label: "Suggest", Stacked: true},
   236  				{Name: "threads_bulk", Label: "Bulk", Stacked: true},
   237  				{Name: "threads_optimize", Label: "Optimize", Stacked: true},
   238  				{Name: "threads_warmer", Label: "Warmer", Stacked: true},
   239  				{Name: "threads_flush", Label: "Flush", Stacked: true},
   240  				{Name: "threads_search", Label: "Search", Stacked: true},
   241  				{Name: "threads_percolate", Label: "Percolate", Stacked: true},
   242  				{Name: "threads_refresh", Label: "Refresh", Stacked: true},
   243  				{Name: "threads_management", Label: "Management", Stacked: true},
   244  				{Name: "threads_fetch_shard_started", Label: "Fetch Shard Started", Stacked: true},
   245  				{Name: "threads_fetch_shard_store", Label: "Fetch Shard Store", Stacked: true},
   246  				{Name: "threads_listener", Label: "Listener", Stacked: true},
   247  			},
   248  		},
   249  		p.Prefix + ".transport.count": {
   250  			Label: (p.LabelPrefix + " Transport Count"),
   251  			Unit:  "integer",
   252  			Metrics: []mp.Metrics{
   253  				{Name: "count_rx", Label: "TX", Diff: true},
   254  				{Name: "count_tx", Label: "RX", Diff: true},
   255  			},
   256  		},
   257  		p.Prefix + ".process": {
   258  			Label: (p.LabelPrefix + " Process"),
   259  			Unit:  "integer",
   260  			Metrics: []mp.Metrics{
   261  				{Name: "open_file_descriptors", Label: "Open File Descriptors"},
   262  			},
   263  		},
   264  		p.Prefix + ".script": {
   265  			Label: (p.LabelPrefix + " Script"),
   266  			Unit:  "integer",
   267  			Metrics: []mp.Metrics{
   268  				{Name: "compilations", Label: "Compilations", Diff: true},
   269  				{Name: "cache_evictions", Label: "Cache Evictions", Diff: true},
   270  				{Name: "compilation_limit_triggered", Label: "Compilation Limit Triggered", Diff: true},
   271  			},
   272  		},
   273  	}
   274  
   275  	return graphdef
   276  }
   277  
   278  // Do the plugin
   279  func Do() {
   280  	optScheme := flag.String("scheme", "http", "Scheme")
   281  	optHost := flag.String("host", "localhost", "Host")
   282  	optPort := flag.String("port", "9200", "Port")
   283  	optPrefix := flag.String("metric-key-prefix", "elasticsearch", "Metric key prefix")
   284  	optLabelPrefix := flag.String("metric-label-prefix", "", "Metric Label prefix")
   285  	optTempfile := flag.String("tempfile", "", "Temp file name")
   286  	optInsecure := flag.Bool("insecure", false, "Skip TLS certificate verification")
   287  	optUser := flag.String("user", "", "Basic auth user")
   288  	optPassword := flag.String("password", "", "Basic auth password")
   289  	optSuppressMissingError := flag.Bool("suppress-missing-error", false, "Suppress ERROR for missing values")
   290  	flag.Parse()
   291  
   292  	var elasticsearch ElasticsearchPlugin
   293  	elasticsearch.URI = fmt.Sprintf("%s://%s:%s", *optScheme, *optHost, *optPort)
   294  	elasticsearch.Prefix = *optPrefix
   295  	if *optLabelPrefix == "" {
   296  		elasticsearch.LabelPrefix = cases.Title(language.Und, cases.NoLower).String(*optPrefix)
   297  	} else {
   298  		elasticsearch.LabelPrefix = *optLabelPrefix
   299  	}
   300  	elasticsearch.Insecure = *optInsecure
   301  	elasticsearch.User = *optUser
   302  	elasticsearch.Password = *optPassword
   303  	elasticsearch.SuppressMissingError = *optSuppressMissingError
   304  
   305  	helper := mp.NewMackerelPlugin(elasticsearch)
   306  	if *optTempfile != "" {
   307  		helper.Tempfile = *optTempfile
   308  	} else {
   309  		helper.SetTempfileByBasename(fmt.Sprintf("mackerel-plugin-elasticsearch-%s-%s", *optHost, *optPort))
   310  	}
   311  
   312  	helper.Run()
   313  }