Cleanup config.nodes_of
[check_mk.git] / agents / plugins / mk_jolokia.py
blob6e6ec35c6b2b891f10dc28e82472b4e694319fe6
1 #!/usr/bin/env python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 import os
28 import socket
29 import sys
30 import urllib2
32 try:
33 try:
34 from simplejson import json
35 except ImportError:
36 import json
37 except ImportError as import_error:
38 sys.stdout.write(
39 "<<<jolokia_info>>>\n"
40 "Error: mk_jolokia requires either the json or simplejson library."
41 " Please either use a Python version that contains the json library or install the"
42 " simplejson library on the monitored system.")
43 sys.exit(1)
45 try:
46 import requests
47 from requests.auth import HTTPDigestAuth
48 from requests.packages import urllib3
49 except ImportError as import_error:
50 sys.stdout.write("<<<jolokia_info>>>\n"
51 "Error: mk_jolokia requires the requests library."
52 " Please install it on the monitored system.")
53 sys.exit(1)
55 VERBOSE = sys.argv.count('--verbose') + sys.argv.count('-v') + 2 * sys.argv.count('-vv')
56 DEBUG = sys.argv.count('--debug')
58 MBEAN_SECTIONS = {
59 'jvm_threading': ("java.lang:type=Threading",),
62 MBEAN_SECTIONS_SPECIFIC = {
63 'tomcat': {
64 'jvm_threading': (
65 "*:name=*,type=ThreadPool/maxThreads,currentThreadCount,currentThreadsBusy/",),
69 QUERY_SPECS_LEGACY = [
70 ("java.lang:type=Memory", "NonHeapMemoryUsage/used", "NonHeapMemoryUsage", [], False),
71 ("java.lang:type=Memory", "NonHeapMemoryUsage/max", "NonHeapMemoryMax", [], False),
72 ("java.lang:type=Memory", "HeapMemoryUsage/used", "HeapMemoryUsage", [], False),
73 ("java.lang:type=Memory", "HeapMemoryUsage/max", "HeapMemoryMax", [], False),
74 ("java.lang:type=Runtime", "Uptime", "Uptime", [], False),
75 ("java.lang:type=GarbageCollector,name=*", "CollectionCount", "", [], False),
76 ("java.lang:type=GarbageCollector,name=*", "CollectionTime", "", [], False),
77 ("java.lang:name=CMS%20Perm%20Gen,type=MemoryPool", "Usage/used", "PermGenUsage", [], False),
78 ("java.lang:name=CMS%20Perm%20Gen,type=MemoryPool", "Usage/max", "PermGenMax", [], False),
79 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "OffHeapHits",
80 "", [], True),
81 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "OnDiskHits",
82 "", [], True),
83 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
84 "InMemoryHitPercentage", "", [], True),
85 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "CacheMisses",
86 "", [], True),
87 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
88 "OnDiskHitPercentage", "", [], True),
89 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
90 "MemoryStoreObjectCount", "", [], True),
91 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
92 "DiskStoreObjectCount", "", [], True),
93 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
94 "CacheMissPercentage", "", [], True),
95 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
96 "CacheHitPercentage", "", [], True),
97 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
98 "OffHeapHitPercentage", "", [], True),
99 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
100 "InMemoryMisses", "", [], True),
101 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
102 "OffHeapStoreObjectCount", "", [], True),
103 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
104 "WriterQueueLength", "", [], True),
105 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
106 "WriterMaxQueueSize", "", [], True),
107 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "OffHeapMisses",
108 "", [], True),
109 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "InMemoryHits",
110 "", [], True),
111 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
112 "AssociatedCacheName", "", [], True),
113 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "ObjectCount",
114 "", [], True),
115 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "OnDiskMisses",
116 "", [], True),
117 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "CacheHits", "",
118 [], True),
121 QUERY_SPECS_SPECIFIC_LEGACY = {
122 "weblogic": [
123 ("*:*", "CompletedRequestCount", None, ["ServerRuntime"], False),
124 ("*:*", "QueueLength", None, ["ServerRuntime"], False),
125 ("*:*", "StandbyThreadCount", None, ["ServerRuntime"], False),
126 ("*:*", "PendingUserRequestCount", None, ["ServerRuntime"], False),
127 ("*:Name=ThreadPoolRuntime,*", "ExecuteThreadTotalCount", None, ["ServerRuntime"], False),
128 ("*:*", "ExecuteThreadIdleCount", None, ["ServerRuntime"], False),
129 ("*:*", "HoggingThreadCount", None, ["ServerRuntime"], False),
130 ("*:Type=WebAppComponentRuntime,*", "OpenSessionsCurrentCount", None,
131 ["ServerRuntime", "ApplicationRuntime"], False),
133 "tomcat": [
134 ("*:type=Manager,*", "activeSessions,maxActiveSessions", None, ["path", "context"], False),
135 ("*:j2eeType=Servlet,name=default,*", "stateName", None, ["WebModule"], False),
136 # Check not yet working
137 ("*:j2eeType=Servlet,name=default,*", "requestCount", None, ["WebModule"], False),
138 # too wide location for addressing the right info
139 # ( "*:j2eeType=Servlet,*", "requestCount", None, [ "WebModule" ] , False),
141 "jboss": [("*:type=Manager,*", "activeSessions,maxActiveSessions", None, ["path", "context"],
142 False),],
145 AVAILABLE_PRODUCTS = sorted(
146 set(QUERY_SPECS_SPECIFIC_LEGACY.keys() + MBEAN_SECTIONS_SPECIFIC.keys()))
148 # Default global configuration: key, value [, help]
149 DEFAULT_CONFIG_TUPLES = (
150 ("protocol", "http", "Protocol to use (http/https)."),
151 ("server", "localhost", "Host name or IP address of the Jolokia server."),
152 ("port", 8080, "TCP Port of the Jolokia server."),
153 ("suburi", "jolokia", "Path-component of the URI to query."),
154 ("user", "monitoring", "Username to use for connecting."),
155 ("password", None, "Password to use for connecting."),
156 ("mode", "digest", "Authentication mode. Can be \"basic\", \"digest\" or \"https\"."),
157 ("instance", None, "Name of the instance in the monitoring. Defaults to port."),
158 ("verify", None),
159 ("client_cert", None, "Path to client cert for https authentication."),
160 ("client_key", None, "Client cert secret for https authentication."),
161 ("service_url", None),
162 ("service_user", None),
163 ("service_password", None),
164 ("product", None, "Product description. Available: %s. If not provided," \
165 " we try to detect the product from the jolokia info section." % \
166 ", ".join(AVAILABLE_PRODUCTS)),
167 ("timeout", 1.0, "Connection/read timeout for requests."),
168 ("custom_vars", []),
169 # List of instances to monitor. Each instance is a dict where
170 # the global configuration values can be overridden.
171 ("instances", [{}]),
175 class SkipInstance(RuntimeError):
176 pass
179 class SkipMBean(RuntimeError):
180 pass
183 def get_default_config_dict():
184 return {t[0]: t[1] for t in DEFAULT_CONFIG_TUPLES}
187 def write_section(name, iterable):
188 sys.stdout.write('<<<%s:sep(0)>>>\n' % name)
189 for line in iterable:
190 sys.stdout.write(chr(0).join(map(str, line)) + '\n')
193 def cached(function):
194 cache = {}
196 def cached_function(*args):
197 key = repr(args)
198 try:
199 return cache[key]
200 except KeyError:
201 return cache.setdefault(key, function(*args))
203 return cached_function
206 class JolokiaInstance(object):
207 @staticmethod
208 def _sanitize_config(config):
209 instance = config.get("instance")
210 err_msg = "%s in configuration"
211 if instance:
212 err_msg += " for %s" % instance
214 required_keys = {"protocol", "server", "port", "suburi", "timeout"}
215 auth_mode = config.get("mode")
216 if auth_mode in ("digest", "basic", "basic_preemtive"):
217 required_keys |= {"user", "password"}
218 elif auth_mode == "https":
219 required_keys |= {"client_cert", "client_key"}
220 if config.get("service_url") is not None and config.get("service_user") is not None:
221 required_keys.add("service_password")
222 missing_keys = required_keys - set(config.keys())
223 if missing_keys:
224 raise ValueError(err_msg % ("Missing key(s): %s" % ", ".join(sorted(missing_keys))))
226 if not instance:
227 instance = str(config["port"])
228 config["instance"] = instance.replace(" ", "_")
230 # port must be (or look like) an integer, timeout like float
231 for key, type_ in (("port", int), ("timeout", float)):
232 val = config[key]
233 try:
234 config[key] = type_(val)
235 except ValueError:
236 raise ValueError(err_msg % ("Invalid %s %r" % (key, val)))
238 if config.get("server") == "use fqdn":
239 config["server"] = socket.getfqdn()
241 # if "verify" was not set to bool/string
242 if config.get("verify") is None:
243 # handle legacy "cert_path"
244 cert_path = config.get("cert_path")
245 if cert_path not in ("_default", None):
246 # The '_default' was the default value
247 # up to cmk version 1.5.0p8. It broke things.
248 config["verify"] = cert_path
249 else:
250 # this is default, but be explicit
251 config["verify"] = True
253 return config
255 def __init__(self, config):
256 super(JolokiaInstance, self).__init__()
257 self._config = self._sanitize_config(config)
259 self.name = self._config["instance"]
260 self.product = self._config.get("product")
261 self.custom_vars = self._config.get("custom_vars", [])
263 self.base_url = self._get_base_url()
264 self.target = self._get_target()
265 self._session = self._initialize_http_session()
267 def _get_base_url(self):
268 return "%s://%s:%d/%s/" % (
269 self._config["protocol"].strip('/'),
270 self._config["server"].strip('/'),
271 self._config["port"],
272 self._config["suburi"],
275 def _get_target(self):
276 url = self._config.get("service_url")
277 if url is None:
278 return {}
279 user = self._config.get("service_user")
280 if user is None:
281 return {"url": url}
282 return {
283 "url": url,
284 "user": user,
285 "password": self._config["service_password"],
288 def _initialize_http_session(self):
289 session = requests.Session()
290 session.verify = self._config["verify"]
291 if session.verify is False:
292 urllib3.disable_warnings(category=urllib3.exceptions.InsecureRequestWarning)
293 session.timeout = self._config["timeout"]
295 auth_method = self._config.get("mode")
296 if auth_method is None:
297 return session
299 # initialize authentication
300 if auth_method == "https":
301 session.cert = (
302 self._config["client_cert"],
303 self._config["client_key"],
305 elif auth_method == 'digest':
306 session.auth = HTTPDigestAuth(
307 self._config["user"],
308 self._config["password"],
310 elif auth_method in ("basic", "basic_preemptive"):
311 session.auth = (
312 self._config["user"],
313 self._config["password"],
315 else:
316 raise NotImplementedError("Authentication method %r" % auth_method)
318 return session
320 def get_post_data(self, path, function, use_target):
321 segments = path.strip("/").split("/")
322 # we may have one to three segments:
323 data = dict(zip(("mbean", "attribute", "path"), segments))
325 data["type"] = function
326 if use_target and self.target:
327 data["target"] = self.target
328 return data
330 def post(self, data):
331 post_data = json.dumps(data)
332 if VERBOSE:
333 sys.stderr.write("\nDEBUG: POST data: %r\n" % post_data)
334 try:
335 raw_response = self._session.post(self.base_url, data=post_data)
336 except () if DEBUG else Exception, exc:
337 sys.stderr.write("ERROR: %s\n" % exc)
338 raise SkipMBean(exc)
340 return validate_response(raw_response)
343 def validate_response(raw):
344 '''return loaded response or raise exception'''
345 if VERBOSE > 1:
346 sys.stderr.write("DEBUG: %r:\n"
347 "DEBUG: headers: %r\n"
348 "DEBUG: content: %r\n\n" % (raw, raw.headers, raw.content))
350 # check the status of the http server
351 if not 200 <= raw.status_code < 300:
352 sys.stderr.write("ERROR: HTTP STATUS: %d\n" % raw.status_code)
353 # Unauthorized, Forbidden, Bad Gateway
354 if raw.status_code in (401, 403, 502):
355 raise SkipInstance("HTTP STATUS", raw.status_code)
356 raise SkipMBean("HTTP STATUS", raw.status_code)
358 response = raw.json()
359 # check the status of the jolokia response
360 if response.get("status") != 200:
361 errmsg = response.get("error", "unkown error")
362 sys.stderr.write("ERROR: JAVA: %s\n" % errmsg)
363 raise SkipMBean("JAVA", errmsg)
365 if "value" not in response:
366 sys.stderr.write("ERROR: missing 'value': %r\n" % response)
367 raise SkipMBean("ERROR", "missing 'value'")
369 if VERBOSE:
370 sys.stderr.write("\nDEBUG: RESPONSE: %r\n" % response)
372 return response
375 def fetch_var(inst, function, path, use_target=False):
376 data = inst.get_post_data(path, function, use_target=use_target)
377 obj = inst.post(data)
378 return obj['value']
381 # convert single values into lists of items in
382 # case value is a 1-levelled or 2-levelled dict
383 def make_item_list(path, value, itemspec):
384 if not isinstance(value, dict):
385 if isinstance(value, str):
386 value = value.replace(r'\/', '/')
387 return [(path, value)]
389 result = []
390 for key, subvalue in value.items():
391 # Handle filtering via itemspec
392 miss = False
393 while itemspec and '=' in itemspec[0]:
394 if itemspec[0] not in key:
395 miss = True
396 break
397 itemspec = itemspec[1:]
398 if miss:
399 continue
400 item = extract_item(key, itemspec)
401 if not item:
402 item = (key,)
403 result += make_item_list(path + item, subvalue, [])
404 return result
407 # Example:
408 # key = 'Catalina:host=localhost,path=\\/,type=Manager'
409 # itemsepc = [ "path" ]
410 # --> "/"
411 def extract_item(key, itemspec):
412 if not itemspec:
413 return ()
415 path = key.split(":", 1)[-1]
416 components = path.split(",")
417 comp_dict = dict(c.split('=') for c in components if c.count('=') == 1)
419 item = ()
420 for pathkey in itemspec:
421 if pathkey in comp_dict:
422 right = comp_dict[pathkey]
423 if '/' in right:
424 right = '/' + right.split('/')[-1]
425 item = item + (right,)
426 return item
429 def fetch_metric(inst, path, title, itemspec, inst_add=None):
430 values = fetch_var(inst, "read", path, use_target=True)
431 item_list = make_item_list((), values, itemspec)
433 for subinstance, value in item_list:
434 if not subinstance and not title:
435 sys.stderr.write("INTERNAL ERROR: %s\n" % value)
436 continue
438 if "threadStatus" in subinstance or "threadParam" in subinstance:
439 continue
441 if len(subinstance) > 1:
442 item = ",".join((inst.name,) + subinstance[:-1])
443 elif inst_add is not None:
444 item = ",".join((inst.name, inst_add))
445 else:
446 item = inst.name
448 if title:
449 if subinstance:
450 tit = title + "." + subinstance[-1]
451 else:
452 tit = title
453 else:
454 tit = subinstance[-1]
456 yield (item.replace(" ", "_"), tit, value)
459 @cached
460 def _get_queries(do_search, inst, itemspec, title, path, mbean):
461 if not do_search:
462 return [(mbean + "/" + path, title, itemspec)]
464 value = fetch_var(inst, "search", mbean)
465 try:
466 paths = make_item_list((), value, "")[0][1]
467 except IndexError:
468 return []
470 return [("%s/%s" % (urllib2.quote(mbean_exp), path), path, itemspec) for mbean_exp in paths]
473 def _process_queries(inst, queries):
474 for mbean_path, title, itemspec in queries:
475 try:
476 for item, out_title, value in fetch_metric(inst, mbean_path, title, itemspec):
477 yield item, out_title, value
478 except (IOError, socket.timeout):
479 raise SkipInstance()
480 except SkipMBean:
481 continue
482 except () if DEBUG else Exception:
483 continue
486 def query_instance(inst):
487 write_section('jolokia_info', generate_jolokia_info(inst))
489 # now (after jolokia_info) we're sure about the product
490 specs_specific = QUERY_SPECS_SPECIFIC_LEGACY.get(inst.product, [])
491 write_section('jolokia_metrics', generate_values(inst, specs_specific))
492 write_section('jolokia_metrics', generate_values(inst, QUERY_SPECS_LEGACY))
494 sections_specific = MBEAN_SECTIONS_SPECIFIC.get(inst.product, {})
495 for section_name, mbeans in sections_specific.iteritems():
496 write_section('jolokia_%s' % section_name, generate_json(inst, mbeans))
497 for section_name, mbeans in MBEAN_SECTIONS.iteritems():
498 write_section('jolokia_%s' % section_name, generate_json(inst, mbeans))
500 write_section('jolokia_generic', generate_values(inst, inst.custom_vars))
503 def generate_jolokia_info(inst):
504 # Determine type of server
505 try:
506 data = fetch_var(inst, "version", "")
507 except (SkipInstance, SkipMBean) as exc:
508 yield inst.name, "ERROR", str(exc)
509 raise SkipInstance(exc)
511 info = data.get('info', {})
512 version = info.get('version', "unknown")
513 product = info.get('product', "unknown")
514 if inst.product is not None:
515 product = inst.product
516 else:
517 inst.product = product
519 agentversion = data.get('agent', "unknown")
520 yield inst.name, product, version, agentversion
523 def generate_values(inst, var_list):
524 for var in var_list:
525 mbean, path, title, itemspec, do_search = var[:5]
526 value_type = var[5] if len(var) >= 6 else None
528 queries = _get_queries(do_search, inst, itemspec, title, path, mbean)
530 for item, title, value in _process_queries(inst, queries):
531 if value_type:
532 yield item, title, value, value_type
533 else:
534 yield item, title, value
537 def generate_json(inst, mbeans):
538 for mbean in mbeans:
539 try:
540 data = inst.get_post_data(mbean, "read", use_target=True)
541 obj = inst.post(data)
542 yield inst.name, mbean, json.dumps(obj['value'])
543 except (IOError, socket.timeout):
544 raise SkipInstance()
545 except SkipMBean if DEBUG else Exception:
546 pass
549 def yield_configured_instances(custom_config=None):
551 if custom_config is None:
552 custom_config = get_default_config_dict()
554 conffile = os.path.join(os.getenv("MK_CONFDIR", "/etc/check_mk"), "jolokia.cfg")
555 if os.path.exists(conffile):
556 execfile(conffile, {}, custom_config)
558 # Generate list of instances to monitor. If the user has defined
559 # instances in his configuration, we will use this (a list of dicts).
560 individual_configs = custom_config.pop("instances", [{}])
561 for cfg in individual_configs:
562 keys = set(cfg.keys() + custom_config.keys())
563 conf_dict = {k: cfg.get(k, custom_config.get(k)) for k in keys}
564 if VERBOSE:
565 sys.stderr.write("DEBUG: configuration: %r\n" % conf_dict)
566 yield conf_dict
569 def main(configs_iterable=None):
570 if configs_iterable is None:
571 configs_iterable = yield_configured_instances()
573 for config in configs_iterable:
574 instance = JolokiaInstance(config)
575 try:
576 query_instance(instance)
577 except SkipInstance:
578 pass
581 if __name__ == "__main__":
582 main()