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 }