root / plugins / solr / solr4_ @ 9f85e0ae
Historique | Voir | Annoter | Télécharger (13,9 ko)
| 1 | 52191f68 | antonio | #!/usr/bin/env python |
|---|---|---|---|
| 2 | # |
||
| 3 | # Copyright (c) 2013, Antonio Verni, me.verni@gmail.com |
||
| 4 | # |
||
| 5 | # Permission is hereby granted, free of charge, to any person obtaining a |
||
| 6 | # copy of this software and associated documentation files (the "Software"), |
||
| 7 | # to deal in the Software without restriction, including without limitation |
||
| 8 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||
| 9 | # and/or sell copies of the Software, and to permit persons to whom the |
||
| 10 | # Software is furnished to do so, subject to the following conditions: |
||
| 11 | # |
||
| 12 | # The above copyright notice and this permission notice shall be included |
||
| 13 | # in all copies or substantial portions of the Software. |
||
| 14 | # |
||
| 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
||
| 16 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||
| 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
||
| 18 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||
| 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||
| 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||
| 21 | # DEALINGS IN THE SOFTWARE. |
||
| 22 | # |
||
| 23 | # Munin plugin for monitoring a multicore solr 4.* installation via mbean. |
||
| 24 | # It calls: |
||
| 25 | # > http://localhost:8080/solr/admin/cores?action=STATUS&wt=json |
||
| 26 | # and |
||
| 27 | # > http://localhost:8080/solr/corename/admin/mbeans?stats=true&wt=json |
||
| 28 | # for each core to retrieve cores and data. Verify those urls on your instance. |
||
| 29 | # |
||
| 30 | # Configuration parameters: |
||
| 31 | # [solr_*] |
||
| 32 | # host_port <host:port> |
||
| 33 | # qpshandler_<handlerlabel> <handlerpath> |
||
| 34 | # availableram <ramsize in bytes> |
||
| 35 | # |
||
| 36 | # Example: |
||
| 37 | # host_port solrhost:8080 |
||
| 38 | # qpshandler_select /select |
||
| 39 | # availableram 3221225472 |
||
| 40 | # |
||
| 41 | # Defined checks: |
||
| 42 | # numdocs |
||
| 43 | # qps |
||
| 44 | # indexsize |
||
| 45 | # requesttimes |
||
| 46 | # documentcache |
||
| 47 | # fieldvaluecache |
||
| 48 | # filtercache |
||
| 49 | # queryresultcache |
||
| 50 | # |
||
| 51 | # Installation example: |
||
| 52 | # ln -s /usr/share/munin/plugins/solr_.py /etc/munin/plugins/solr_numdocs_core_1 |
||
| 53 | # ln -s /usr/share/munin/plugins/solr_.py /etc/munin/plugins/solr_requesttimes_select |
||
| 54 | # ln -s /usr/share/munin/plugins/solr_.py /etc/munin/plugins/solr_qps_core_1_select |
||
| 55 | # |
||
| 56 | # Source repo: https://github.com/averni/munin-solr |
||
| 57 | |||
| 58 | import sys |
||
| 59 | import os |
||
| 60 | import httplib |
||
| 61 | import json |
||
| 62 | |||
| 63 | def parse_params(): |
||
| 64 | plugname = os.path.basename(sys.argv[0]).split('_', 2)[1:]
|
||
| 65 | params = {
|
||
| 66 | 'type': plugname[0], |
||
| 67 | 'op': 'config' if sys.argv[-1] == 'config' else 'fetch', |
||
| 68 | 'core': plugname[1] if len(plugname) > 1 else '', |
||
| 69 | 'params': {}
|
||
| 70 | } |
||
| 71 | if plugname[0] in[ 'qps', 'requesttimes']: |
||
| 72 | data = params['core'].rsplit('_', 1)
|
||
| 73 | handler = data.pop() |
||
| 74 | params['params'] = {
|
||
| 75 | 'handler': os.environ.get('qpshandler_%s' % handler, '/select')
|
||
| 76 | } |
||
| 77 | if not data: |
||
| 78 | params['core'] = '' |
||
| 79 | else: |
||
| 80 | params['core'] = data[0] |
||
| 81 | elif plugname[0] == 'indexsize': |
||
| 82 | params['params']['core'] = params['core'] |
||
| 83 | return params |
||
| 84 | |||
| 85 | ############################################################################# |
||
| 86 | # Datasources |
||
| 87 | |||
| 88 | class CheckException(Exception): |
||
| 89 | pass |
||
| 90 | |||
| 91 | class JSONReader: |
||
| 92 | @classmethod |
||
| 93 | def readValue(cls, struct, path): |
||
| 94 | if not path[0] in struct: |
||
| 95 | return -1 |
||
| 96 | obj = struct[path[0]] |
||
| 97 | if not obj: |
||
| 98 | return -1 |
||
| 99 | for k in path[1:]: |
||
| 100 | obj = obj[k] |
||
| 101 | return obj |
||
| 102 | |||
| 103 | class SolrCoresAdmin: |
||
| 104 | def __init__(self, host): |
||
| 105 | self.host = host |
||
| 106 | self.data = None |
||
| 107 | |||
| 108 | def fetchcores(self): |
||
| 109 | uri = "/solr/admin/cores?action=STATUS&wt=json" |
||
| 110 | conn = httplib.HTTPConnection(self.host) |
||
| 111 | conn.request("GET", uri)
|
||
| 112 | res = conn.getresponse() |
||
| 113 | data = res.read() |
||
| 114 | if res.status != 200: |
||
| 115 | raise CheckException("Cores status fetch failed: %s\n%s" %( str(res.status), res.read()))
|
||
| 116 | self.data = json.loads(data) |
||
| 117 | |||
| 118 | def getCores(self): |
||
| 119 | if not self.data: |
||
| 120 | self.fetchcores() |
||
| 121 | cores = JSONReader.readValue(self.data, ['status']) |
||
| 122 | return cores.keys() |
||
| 123 | |||
| 124 | def indexsize(self, core = None): |
||
| 125 | if not self.data: |
||
| 126 | self.fetchcores() |
||
| 127 | if core: |
||
| 128 | return {
|
||
| 129 | core: JSONReader.readValue(self.data, ['status', core, 'index', 'sizeInBytes']) |
||
| 130 | } |
||
| 131 | else: |
||
| 132 | ret = {}
|
||
| 133 | for core in self.getCores(): |
||
| 134 | ret[core] = JSONReader.readValue(self.data, ['status', core, 'index', 'sizeInBytes']) |
||
| 135 | return ret |
||
| 136 | |||
| 137 | class SolrCoreMBean: |
||
| 138 | def __init__(self, host, core): |
||
| 139 | self.host = host |
||
| 140 | self.data = None |
||
| 141 | self.core = core |
||
| 142 | |||
| 143 | def _fetch(self): |
||
| 144 | uri = "/solr/%s/admin/mbeans?stats=true&wt=json" % self.core |
||
| 145 | conn = httplib.HTTPConnection(self.host) |
||
| 146 | conn.request("GET", uri)
|
||
| 147 | res = conn.getresponse() |
||
| 148 | data = res.read() |
||
| 149 | if res.status != 200: |
||
| 150 | raise CheckException("MBean fetch failed: %s\n%s" %( str(res.status), res.read()))
|
||
| 151 | raw_data = json.loads(data) |
||
| 152 | data = {}
|
||
| 153 | self.data = {
|
||
| 154 | 'solr-mbeans': data |
||
| 155 | } |
||
| 156 | key = None |
||
| 157 | for pos, el in enumerate(raw_data['solr-mbeans']): |
||
| 158 | if pos % 2 == 1: |
||
| 159 | data[key] = el |
||
| 160 | else: |
||
| 161 | key = el |
||
| 162 | |||
| 163 | def _read(self, path): |
||
| 164 | if self.data is None: |
||
| 165 | self._fetch() |
||
| 166 | return JSONReader.readValue(self.data, path) |
||
| 167 | |||
| 168 | def _readCache(self, cache): |
||
| 169 | result = {}
|
||
| 170 | for key in ['lookups', 'hits', 'inserts', 'evictions', 'hitratio']: |
||
| 171 | path = ['solr-mbeans', 'CACHE', cache, 'stats', 'cumulative_%s' % key] |
||
| 172 | result[key] = self._read(path) |
||
| 173 | result['size'] = self._read(['solr-mbeans', 'CACHE', cache, 'stats', 'size']) |
||
| 174 | return result |
||
| 175 | |||
| 176 | def getCore(self): |
||
| 177 | return self.core |
||
| 178 | |||
| 179 | def qps(self, handler): |
||
| 180 | path = ['solr-mbeans', 'QUERYHANDLER', handler, 'stats', 'avgRequestsPerSecond'] |
||
| 181 | return self._read(path) |
||
| 182 | |||
| 183 | def requesttimes(self, handler): |
||
| 184 | times = {}
|
||
| 185 | path = ['solr-mbeans', 'QUERYHANDLER', handler, 'stats'] |
||
| 186 | for perc in ['avgTimePerRequest', '75thPcRequestTime', '99thPcRequestTime']: |
||
| 187 | times[perc] = self._read(path + [perc]) |
||
| 188 | return times |
||
| 189 | |||
| 190 | def numdocs(self): |
||
| 191 | path = ['solr-mbeans', 'CORE', 'searcher', 'stats', 'numDocs'] |
||
| 192 | return self._read(path) |
||
| 193 | |||
| 194 | def documentcache(self): |
||
| 195 | return self._readCache('documentCache')
|
||
| 196 | |||
| 197 | def filtercache(self): |
||
| 198 | return self._readCache('filterCache')
|
||
| 199 | |||
| 200 | def fieldvaluecache(self): |
||
| 201 | return self._readCache('fieldValueCache')
|
||
| 202 | |||
| 203 | def queryresultcache(self): |
||
| 204 | return self._readCache('queryResultCache')
|
||
| 205 | |||
| 206 | ############################################################################# |
||
| 207 | # Graph Templates |
||
| 208 | |||
| 209 | CACHE_GRAPH_TPL = """multigraph solr_{core}_{cacheType}_hit_rates
|
||
| 210 | graph_category solr |
||
| 211 | graph_title Solr {core} {cacheName} Hit rates
|
||
| 212 | graph_order lookups hits inserts |
||
| 213 | graph_scale no |
||
| 214 | graph_vlabel Hit Rate |
||
| 215 | graph_args -u 100 --rigid |
||
| 216 | lookups.label Cache lookups |
||
| 217 | lookups.graph no |
||
| 218 | lookups.min 0 |
||
| 219 | lookups.type DERIVE |
||
| 220 | inserts.label Cache misses |
||
| 221 | inserts.min 0 |
||
| 222 | inserts.draw STACK |
||
| 223 | inserts.cdef inserts,lookups,/,100,* |
||
| 224 | inserts.type DERIVE |
||
| 225 | hits.label Cache hits |
||
| 226 | hits.min 0 |
||
| 227 | hits.draw AREA |
||
| 228 | hits.cdef hits,lookups,/,100,* |
||
| 229 | hits.type DERIVE |
||
| 230 | |||
| 231 | multigraph solr_{core}_{cacheType}_size
|
||
| 232 | graph_title Solr {core} {cacheName} Size
|
||
| 233 | graph_args -l 0 |
||
| 234 | graph_category solr |
||
| 235 | graph_vlabel Size |
||
| 236 | size.label Size |
||
| 237 | size.draw LINE2 |
||
| 238 | evictions.label Evictions |
||
| 239 | evictions.draw LINE2 |
||
| 240 | |||
| 241 | """ |
||
| 242 | |||
| 243 | QPSMAIN_GRAPH_TPL = """graph_title Solr {core} {handler} Request per second"
|
||
| 244 | graph_args -l 0 |
||
| 245 | graph_vlabel request / second |
||
| 246 | graph_category solr |
||
| 247 | {cores_qps_graphs}"""
|
||
| 248 | |||
| 249 | QPSCORE_GRAPH_TPL = """qps_{core}.label {core} Request per second
|
||
| 250 | qps_{core}.type LINESTACK1
|
||
| 251 | qps_{core}.graph yes"""
|
||
| 252 | |||
| 253 | REQUESTTIMES_GRAPH_TPL = """multigraph {core}_requesttimes
|
||
| 254 | graph_title Solr {core} {handler} Time per request
|
||
| 255 | graph_args -l 0 |
||
| 256 | graph_vlabel millis |
||
| 257 | graph_category solr |
||
| 258 | savgtimeperrequest_{core}.label {core} Avg time per request
|
||
| 259 | savgtimeperrequest_{core}.type gauge
|
||
| 260 | savgtimeperrequest_{core}.graph yes
|
||
| 261 | s75thpcrequesttime_{core}.label {core} 75th perc
|
||
| 262 | s75thpcrequesttime_{core}.type gauge
|
||
| 263 | s75thpcrequesttime_{core}.graph yes
|
||
| 264 | s99thpcrequesttime_{core}.label {core} 99th perc
|
||
| 265 | s99thpcrequesttime_{core}.type gauge
|
||
| 266 | s99thpcrequesttime_{core}.graph yes
|
||
| 267 | |||
| 268 | """ |
||
| 269 | |||
| 270 | NUMDOCS_GRAPH_TPL = """graph_title Solr Docs %s |
||
| 271 | graph_vlabel docs |
||
| 272 | docs.label Docs |
||
| 273 | graph_category solr""" |
||
| 274 | |||
| 275 | INDEXSIZE_GRAPH_TPL = """graph_args --base 1024 -l 0 --upper-limit {availableram}
|
||
| 276 | graph_vlabel Bytes |
||
| 277 | graph_title Index Size |
||
| 278 | graph_category solr |
||
| 279 | graph_info Solr Index Memory Usage. |
||
| 280 | graph_order {cores}
|
||
| 281 | {cores_config}
|
||
| 282 | """ |
||
| 283 | |||
| 284 | INDEXSIZECORE_GRAPH_TPL = """{core}.label {core}
|
||
| 285 | {core}.draw STACK"""
|
||
| 286 | |||
| 287 | ############################################################################# |
||
| 288 | # Graph managment |
||
| 289 | CHECKS_DEFINED = [ |
||
| 290 | 'numdocs', |
||
| 291 | 'qps', |
||
| 292 | 'indexsize', |
||
| 293 | 'requesttimes', |
||
| 294 | 'documentcache', |
||
| 295 | 'fieldvaluecache', |
||
| 296 | 'filtercache', |
||
| 297 | 'queryresultcache' |
||
| 298 | ] |
||
| 299 | |||
| 300 | class SolrMuninGraph: |
||
| 301 | def __init__(self, hostport, solrmbean): |
||
| 302 | self.solrcoresadmin = SolrCoresAdmin(hostport) |
||
| 303 | self.hostport = hostport |
||
| 304 | self.params = params |
||
| 305 | |||
| 306 | def _getMBean(self, core): |
||
| 307 | return SolrCoreMBean(self.hostport, core) |
||
| 308 | |||
| 309 | def _cacheConfig(self, cacheType, cacheName): |
||
| 310 | return CACHE_GRAPH_TPL.format(core=self.params['core'], cacheType=cacheType, cacheName=cacheName) |
||
| 311 | |||
| 312 | def _cacheFetch(self, cacheType, fields = None): |
||
| 313 | fields = fields or ['size', 'lookups', 'hits', 'inserts', 'evictions'] |
||
| 314 | hits_fields = ['lookups', 'hits', 'inserts'] |
||
| 315 | size_fields = ['size', 'evictions'] |
||
| 316 | results = [] |
||
| 317 | solrmbean = self._getMBean(self.params['core']) |
||
| 318 | data = getattr(solrmbean, cacheType)() |
||
| 319 | results.append('multigraph solr_{core}_{cacheType}_hit_rates'.format(core=self.params['core'], cacheType=cacheType))
|
||
| 320 | for label in hits_fields: |
||
| 321 | results.append("%s.value %s" % (label, data[label]))
|
||
| 322 | results.append('multigraph solr_{core}_{cacheType}_size'.format(core=self.params['core'], cacheType=cacheType))
|
||
| 323 | for label in size_fields: |
||
| 324 | results.append("%s.value %s" % (label, data[label]))
|
||
| 325 | return "\n".join(results) |
||
| 326 | |||
| 327 | def config(self, mtype): |
||
| 328 | if not mtype: |
||
| 329 | raise CheckException("""Check missing. Available checks: \n\t%s""" % '\n\t'.join(CHECKS_DEFINED))
|
||
| 330 | if not hasattr(self, '%sConfig' % mtype): |
||
| 331 | raise CheckException("Unknown check %s" % mtype)
|
||
| 332 | return getattr(self, '%sConfig' % mtype)() |
||
| 333 | |||
| 334 | def fetch(self, mtype): |
||
| 335 | if not hasattr(self, params['type']): |
||
| 336 | return None |
||
| 337 | return getattr(self, params['type'])() |
||
| 338 | |||
| 339 | def _getCores(self): |
||
| 340 | if self.params['core']: |
||
| 341 | cores = [self.params['core']] |
||
| 342 | else: |
||
| 343 | cores = sorted(self.solrcoresadmin.getCores()) |
||
| 344 | return cores |
||
| 345 | |||
| 346 | def qpsConfig(self): |
||
| 347 | cores = self._getCores() |
||
| 348 | graph = [QPSCORE_GRAPH_TPL.format(core=c) for c in cores ] |
||
| 349 | return QPSMAIN_GRAPH_TPL.format( |
||
| 350 | cores_qps_graphs='\n'.join(graph), |
||
| 351 | handler=self.params['params']['handler'], |
||
| 352 | core=self.params['core'], |
||
| 353 | cores_qps_cdefs='%s,%s' % (','.join(map(lambda x: 'qps_%s' % x, cores)),','.join(['+']*(len(cores)-1)))
|
||
| 354 | ) |
||
| 355 | |||
| 356 | def qps(self): |
||
| 357 | results = [] |
||
| 358 | cores = self._getCores() |
||
| 359 | for c in cores: |
||
| 360 | mbean = self._getMBean(c) |
||
| 361 | results.append('qps_%s.value %s' % (c, mbean.qps(self.params['params']['handler'])))
|
||
| 362 | return '\n'.join(results) |
||
| 363 | |||
| 364 | def requesttimesConfig(self): |
||
| 365 | cores = self._getCores() |
||
| 366 | graphs = [REQUESTTIMES_GRAPH_TPL.format(core=c, handler=self.params['params']['handler']) for c in cores ] |
||
| 367 | return '\n'.join(graphs) |
||
| 368 | |||
| 369 | def requesttimes(self): |
||
| 370 | cores = self._getCores() |
||
| 371 | results = [] |
||
| 372 | for c in cores: |
||
| 373 | mbean = self._getMBean(c) |
||
| 374 | results.append('multigraph {core}_requesttimes'.format(core=c))
|
||
| 375 | for k, time in mbean.requesttimes(self.params['params']['handler']).items(): |
||
| 376 | results.append('s%s_%s.value %s' % (k.lower(), c, time))
|
||
| 377 | return '\n'.join(results) |
||
| 378 | |||
| 379 | def numdocsConfig(self): |
||
| 380 | return NUMDOCS_GRAPH_TPL % self.params['core'] |
||
| 381 | |||
| 382 | def numdocs(self): |
||
| 383 | mbean = self._getMBean(self.params['core']) |
||
| 384 | return 'docs.value %s' % mbean.numdocs(**self.params['params']) |
||
| 385 | |||
| 386 | def indexsizeConfig(self): |
||
| 387 | cores = self._getCores() |
||
| 388 | availableram = os.environ.get('availableram', 16868532224)
|
||
| 389 | graph = [ INDEXSIZECORE_GRAPH_TPL.format(core=c) for c in cores] |
||
| 390 | return INDEXSIZE_GRAPH_TPL.format(cores=" ".join(cores), cores_config="\n".join(graph), availableram=availableram) |
||
| 391 | |||
| 392 | def indexsize(self): |
||
| 393 | results = [] |
||
| 394 | for c, size in self.solrcoresadmin.indexsize(**self.params['params']).items(): |
||
| 395 | results.append("%s.value %s" % (c, size))
|
||
| 396 | return "\n".join(results) |
||
| 397 | |||
| 398 | def documentcacheConfig(self): |
||
| 399 | return self._cacheConfig('documentcache', 'Document Cache')
|
||
| 400 | |||
| 401 | def documentcache(self): |
||
| 402 | return self._cacheFetch('documentcache')
|
||
| 403 | |||
| 404 | def filtercacheConfig(self): |
||
| 405 | return self._cacheConfig('filtercache', 'Filter Cache')
|
||
| 406 | |||
| 407 | def filtercache(self): |
||
| 408 | return self._cacheFetch('filtercache')
|
||
| 409 | |||
| 410 | def fieldvaluecacheConfig(self): |
||
| 411 | return self._cacheConfig('fieldvaluecache', 'Field Value Cache')
|
||
| 412 | |||
| 413 | def fieldvaluecache(self): |
||
| 414 | return self._cacheFetch('fieldvaluecache')
|
||
| 415 | |||
| 416 | def queryresultcacheConfig(self): |
||
| 417 | return self._cacheConfig('queryresultcache', 'Query Cache')
|
||
| 418 | |||
| 419 | def queryresultcache(self): |
||
| 420 | return self._cacheFetch('queryresultcache')
|
||
| 421 | |||
| 422 | if __name__ == '__main__': |
||
| 423 | params = parse_params() |
||
| 424 | SOLR_HOST_PORT = os.environ.get('host_port', 'localhost:8080').replace('http://', '')
|
||
| 425 | mb = SolrMuninGraph(SOLR_HOST_PORT, params) |
||
| 426 | try: |
||
| 427 | if hasattr(mb, params['op']): |
||
| 428 | print getattr(mb, params['op'])(params['type']) |
||
| 429 | except Exception, ex: |
||
| 430 | print "ERROR: %s" % ex |
||
| 431 | exit(1) |
