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

     1  package mpsolr
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  
    14  	mp "github.com/mackerelio/go-mackerel-plugin-helper"
    15  	"github.com/mackerelio/golib/logging"
    16  )
    17  
    18  var (
    19  	logger       = logging.GetLogger("metrics.plugin.solr")
    20  	coreStatKeys = []string{"numDocs", "deletedDocs", "indexHeapUsageBytes", "version",
    21  		"segmentCount", "sizeInBytes"}
    22  	handlerPaths = []string{"/update/json", "/select", "/update/json/docs", "/get",
    23  		"/update/csv", "/replication", "/update", "/dataimport"}
    24  	// Solr5 ... "5minRateReqsPerSecond", "15minRateReqsPerSecond"
    25  	// Solr6 ... "5minRateRequestsPerSecond", "15minRateRequestsPerSecond"
    26  	legacyHandlerStatKeys = []string{"requests", "errors", "timeouts", "avgRequestsPerSecond",
    27  		"5minRateReqsPerSecond", "5minRateRequestsPerSecond", "15minRateReqsPerSecond",
    28  		"15minRateRequestsPerSecond", "avgTimePerRequest", "medianRequestTime", "75thPcRequestTime",
    29  		"95thPcRequestTime", "99thPcRequestTime", "999thPcRequestTime"}
    30  	handlerStatKeys = []string{"requests", "errors", "timeouts", "clientErrors",
    31  		"serverErrors", "requestTimes"}
    32  	cacheTypes = []string{"filterCache", "perSegFilter", "queryResultCache",
    33  		"documentCache", "fieldValueCache"}
    34  	cacheStatKeys = []string{"lookups", "hits", "hitratio", "inserts", "evictions",
    35  		"size", "warmupTime"}
    36  	legacyMbeanHandlerKeys = []string{"QUERYHANDLER", "UPDATEHANDLER", "REPLICATION"}
    37  	mbeanHandlerKeys       = []string{"QUERY", "UPDATE", "REPLICATION"}
    38  )
    39  
    40  // SolrPlugin mackerel plugin for Solr
    41  type SolrPlugin struct {
    42  	Protocol string
    43  	Host     string
    44  	Port     string
    45  	BaseURL  string
    46  	Version  string
    47  	Cores    []string
    48  	Prefix   string
    49  	Stats    map[string](map[string]float64)
    50  	Tempfile string
    51  }
    52  
    53  func (s *SolrPlugin) greaterThanOrEqualToMajorVersion(minVer int) bool {
    54  	currentVer, err := strconv.Atoi(strings.Split(s.Version, ".")[0])
    55  	if err != nil {
    56  		logger.Errorf("Failed to parse major version %s", err)
    57  	}
    58  
    59  	return currentVer >= minVer
    60  }
    61  
    62  func fetchJSONData(url string) (map[string]interface{}, error) {
    63  	req, err := http.NewRequest(http.MethodGet, url, nil)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	req.Header.Set("User-Agent", "mackerel-plugin-solr")
    68  
    69  	resp, err := http.DefaultClient.Do(req)
    70  	if err != nil {
    71  		logger.Errorf("Failed to %s", err)
    72  		return nil, err
    73  	}
    74  	defer resp.Body.Close()
    75  	dec := json.NewDecoder(resp.Body)
    76  	var stats map[string]interface{}
    77  	err = dec.Decode(&stats)
    78  	if err != nil {
    79  		logger.Errorf("Failed to %s", err)
    80  		return nil, err
    81  	}
    82  	return stats, nil
    83  }
    84  
    85  func (s *SolrPlugin) loadStatsCore(core string, values interface{}) error {
    86  	coreStats := values.(map[string]interface{})["index"].(map[string]interface{})
    87  	for _, k := range coreStatKeys {
    88  		v, ok := coreStats[k].(float64)
    89  		if !ok {
    90  			logger.Errorf("Failed to cast from %s to %s", coreStats[k], "float64")
    91  			return errors.New("type assersion error")
    92  		}
    93  		s.Stats[core][k] = v
    94  	}
    95  	return nil
    96  }
    97  
    98  func (s *SolrPlugin) setStatsMbean(core string, stats map[string]interface{}, allowKeys []string, keyIndex int) {
    99  	for _, values := range stats["solr-mbeans"].([]interface{}) {
   100  		switch values.(type) {
   101  		case string:
   102  			continue
   103  		default:
   104  			for key, value := range values.(map[string]interface{}) {
   105  				for k, v := range value.(map[string]interface{}) {
   106  					if k != "stats" {
   107  						continue
   108  					}
   109  					if v == nil {
   110  						continue
   111  					}
   112  					statValues := v.(map[string]interface{})
   113  					if s.greaterThanOrEqualToMajorVersion(7) {
   114  						keyConvertedStatValues := make(map[string]interface{})
   115  						var keyParts []string
   116  						for k, v := range statValues {
   117  							keyParts = strings.Split(k, ".")
   118  							if len(keyParts) > keyIndex {
   119  								keyConvertedStatValues[keyParts[keyIndex]] = v
   120  							}
   121  						}
   122  						statValues = keyConvertedStatValues
   123  					}
   124  					for _, allowKey := range allowKeys {
   125  						if statValues[allowKey] == nil {
   126  							continue
   127  						}
   128  						s.Stats[core][allowKey+"_"+escapeSlash(key)] = statValues[allowKey].(float64)
   129  					}
   130  				}
   131  			}
   132  		}
   133  		break // if QUERYHANDLER and QUERY or UPDATEHANDLER and UPDATE
   134  	}
   135  }
   136  
   137  func (s *SolrPlugin) loadStatsMbeanHandler(core string, cat string) error {
   138  	uri := s.BaseURL + "/" + core + "/admin/mbeans?stats=true&wt=json&cat=" + cat
   139  	for _, path := range handlerPaths {
   140  		uri += fmt.Sprintf("&key=%s", url.QueryEscape(path))
   141  	}
   142  	stats, err := fetchJSONData(uri)
   143  	if err != nil {
   144  		return err
   145  	}
   146  	var sKeys []string
   147  	if s.greaterThanOrEqualToMajorVersion(7) {
   148  		sKeys = handlerStatKeys
   149  	} else {
   150  		sKeys = legacyHandlerStatKeys
   151  	}
   152  	s.setStatsMbean(core, stats, sKeys, 2)
   153  	return nil
   154  }
   155  
   156  func (s *SolrPlugin) loadStatsMbeanCache(core string) error {
   157  	stats, err := fetchJSONData(s.BaseURL + "/" + core + "/admin/mbeans?stats=true&wt=json&cat=CACHE&key=filterCache&key=perSegFilter&key=queryResultCache&key=documentCache&key=fieldValueCache")
   158  	if err != nil {
   159  		return err
   160  	}
   161  	s.setStatsMbean(core, stats, cacheStatKeys, 3)
   162  	return nil
   163  }
   164  
   165  func (s *SolrPlugin) loadVersion() error {
   166  	stats, err := fetchJSONData(s.BaseURL + "/admin/info/system?wt=json")
   167  	if err != nil {
   168  		return err
   169  	}
   170  	lucene, ok := stats["lucene"].(map[string]interface{})
   171  	if !ok {
   172  		return errors.New("type assersion error")
   173  	}
   174  	solrVersion, ok := lucene["solr-spec-version"].(string)
   175  	if !ok {
   176  		return errors.New("type assersion error")
   177  	}
   178  	s.Version = solrVersion
   179  	return nil
   180  }
   181  
   182  func (s *SolrPlugin) loadStats() error {
   183  	s.Stats = map[string](map[string]float64){}
   184  
   185  	stats, err := fetchJSONData(s.BaseURL + "/admin/cores?wt=json")
   186  	if err != nil {
   187  		return err
   188  	}
   189  	s.Cores = []string{}
   190  	for core, values := range stats["status"].(map[string]interface{}) {
   191  		s.Cores = append(s.Cores, core)
   192  		s.Stats[core] = map[string]float64{}
   193  		err := s.loadStatsCore(core, values)
   194  		if err != nil {
   195  			return err
   196  		}
   197  
   198  		var mKeys []string
   199  		if s.greaterThanOrEqualToMajorVersion(7) {
   200  			mKeys = mbeanHandlerKeys
   201  		} else {
   202  			mKeys = legacyMbeanHandlerKeys
   203  		}
   204  		for _, mKey := range mKeys {
   205  			err = s.loadStatsMbeanHandler(core, mKey)
   206  			if err != nil {
   207  				return err
   208  			}
   209  		}
   210  		err = s.loadStatsMbeanCache(core)
   211  		if err != nil {
   212  			return err
   213  		}
   214  	}
   215  	return nil
   216  }
   217  
   218  func escapeSlash(slashIncludedString string) (str string) {
   219  	str = strings.ReplaceAll(slashIncludedString, "/", "")
   220  	return
   221  }
   222  
   223  // FetchMetrics interface for mackerelplugin
   224  func (s SolrPlugin) FetchMetrics() (map[string]interface{}, error) {
   225  	stat := make(map[string]interface{})
   226  	for core, stats := range s.Stats {
   227  		for k, v := range stats {
   228  			stat[core+"_"+k] = v
   229  		}
   230  	}
   231  	return stat, nil
   232  }
   233  
   234  // GraphDefinition interface for mackerelplugin
   235  func (s SolrPlugin) GraphDefinition() map[string]mp.Graphs {
   236  	graphdef := make(map[string]mp.Graphs)
   237  
   238  	for _, core := range s.Cores {
   239  		graphdef[fmt.Sprintf("%s.%s.docsCount", s.Prefix, core)] = mp.Graphs{
   240  			Label: fmt.Sprintf("%s DocsCount", core),
   241  			Unit:  "integer",
   242  			Metrics: []mp.Metrics{
   243  				{Name: core + "_numDocs", Label: "NumDocs"},
   244  				{Name: core + "_deletedDocs", Label: "DeletedDocs"},
   245  			},
   246  		}
   247  
   248  		for _, key := range []string{"indexHeapUsageBytes", "segmentCount", "sizeInBytes"} {
   249  			metricLabel := strings.Title(key) // nolint because this label will use in solr plugin's graph-defs, it should keep same representation
   250  			graphdef[fmt.Sprintf("%s.%s.%s", s.Prefix, core, key)] = mp.Graphs{
   251  				Label: fmt.Sprintf("%s %s", core, metricLabel),
   252  				Unit:  "integer",
   253  				Metrics: []mp.Metrics{
   254  					{Name: core + "_" + key, Label: metricLabel},
   255  				},
   256  			}
   257  		}
   258  
   259  		var sKeys []string
   260  		if s.greaterThanOrEqualToMajorVersion(7) {
   261  			sKeys = handlerStatKeys
   262  		} else {
   263  			sKeys = legacyHandlerStatKeys
   264  		}
   265  		for _, key := range sKeys {
   266  			var metrics []mp.Metrics
   267  			for _, path := range handlerPaths {
   268  				path = escapeSlash(path)
   269  				metricLabel := strings.Title(key) // nolint because this label will use in solr plugin's graph-defs, it should keep same representation
   270  				diff := false
   271  				if key == "requests" || key == "errors" || key == "timeouts" ||
   272  					key == "clientErrors" || key == "serverErrors" || key == "requestTimes" {
   273  					diff = true
   274  				}
   275  				metrics = append(metrics,
   276  					mp.Metrics{Name: fmt.Sprintf("%s_%s_%s", core, key, path), Label: metricLabel, Diff: diff},
   277  				)
   278  			}
   279  			unit := "float"
   280  			if key == "requests" || key == "errors" || key == "timeouts" {
   281  				unit = "integer"
   282  			}
   283  			graphLabel := fmt.Sprintf("%s %s", core, strings.Title(key)) // nolint because this label will use in solr plugin's graph-defs, it should keep same representation
   284  			graphdef[fmt.Sprintf("%s.%s.%s", s.Prefix, core, key)] = mp.Graphs{
   285  				Label:   graphLabel,
   286  				Unit:    unit,
   287  				Metrics: metrics,
   288  			}
   289  		}
   290  
   291  		for _, key := range cacheStatKeys {
   292  			var metrics []mp.Metrics
   293  			for _, cacheType := range cacheTypes {
   294  				metricLabel := strings.Title(key) // nolint because this label will use in solr plugin's graph-defs, it should keep same representation
   295  				metrics = append(metrics,
   296  					mp.Metrics{Name: fmt.Sprintf("%s_%s_%s", core, key, cacheType), Label: metricLabel},
   297  				)
   298  			}
   299  			unit := "integer"
   300  			if key == "hitratio" {
   301  				unit = "float"
   302  			}
   303  			graphLabel := fmt.Sprintf("%s %s", core, strings.Title(key)) // nolint because this label will use in solr plugin's graph-defs, it should keep same representation
   304  			graphdef[fmt.Sprintf("%s.%s.%s", s.Prefix, core, key)] = mp.Graphs{
   305  				Label:   graphLabel,
   306  				Unit:    unit,
   307  				Metrics: metrics,
   308  			}
   309  		}
   310  	}
   311  	return graphdef
   312  }
   313  
   314  // Do the plugin
   315  func Do() {
   316  	optHost := flag.String("host", "localhost", "Hostname")
   317  	optPort := flag.String("port", "8983", "Port")
   318  	optTempfile := flag.String("tempfile", "", "Temp file name")
   319  	flag.Parse()
   320  
   321  	solr := SolrPlugin{
   322  		Protocol: "http",
   323  		Host:     *optHost,
   324  		Port:     *optPort,
   325  		Prefix:   "solr",
   326  	}
   327  
   328  	solr.BaseURL = fmt.Sprintf("%s://%s:%s/solr", solr.Protocol, solr.Host, solr.Port)
   329  	err := solr.loadVersion()
   330  	if err != nil {
   331  		logger.Errorf("loadVersion : %v", err)
   332  		os.Exit(1)
   333  	}
   334  	err = solr.loadStats()
   335  	if err != nil {
   336  		logger.Errorf("loadStats : %v", err)
   337  	}
   338  
   339  	helper := mp.NewMackerelPlugin(solr)
   340  	helper.Tempfile = *optTempfile
   341  
   342  	helper.Run()
   343  }