2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
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.
34 from simplejson
import json
37 except ImportError as import_error
:
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.")
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.")
55 VERBOSE
= sys
.argv
.count('--verbose') + sys
.argv
.count('-v') + 2 * sys
.argv
.count('-vv')
56 DEBUG
= sys
.argv
.count('--debug')
59 'jvm_threading': ("java.lang:type=Threading",),
62 MBEAN_SECTIONS_SPECIFIC
= {
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",
81 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "OnDiskHits",
83 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
84 "InMemoryHitPercentage", "", [], True),
85 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "CacheMisses",
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",
109 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "InMemoryHits",
111 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics",
112 "AssociatedCacheName", "", [], True),
113 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "ObjectCount",
115 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "OnDiskMisses",
117 ("net.sf.ehcache:CacheManager=CacheManagerApplication*,*,type=CacheStatistics", "CacheHits", "",
121 QUERY_SPECS_SPECIFIC_LEGACY
= {
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),
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"],
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."),
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."),
169 # List of instances to monitor. Each instance is a dict where
170 # the global configuration values can be overridden.
175 class SkipInstance(RuntimeError):
179 class SkipMBean(RuntimeError):
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
):
196 def cached_function(*args
):
201 return cache
.setdefault(key
, function(*args
))
203 return cached_function
206 class JolokiaInstance(object):
208 def _sanitize_config(config
):
209 instance
= config
.get("instance")
210 err_msg
= "%s in configuration"
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())
224 raise ValueError(err_msg
% ("Missing key(s): %s" % ", ".join(sorted(missing_keys
))))
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)):
234 config
[key
] = type_(val
)
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
250 # this is default, but be explicit
251 config
["verify"] = True
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")
279 user
= self
._config
.get("service_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:
299 # initialize authentication
300 if auth_method
== "https":
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"):
312 self
._config
["user"],
313 self
._config
["password"],
316 raise NotImplementedError("Authentication method %r" % auth_method
)
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
330 def post(self
, data
):
331 post_data
= json
.dumps(data
)
333 sys
.stderr
.write("\nDEBUG: POST data: %r\n" % post_data
)
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
)
340 return validate_response(raw_response
)
343 def validate_response(raw
):
344 '''return loaded response or raise exception'''
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'")
370 sys
.stderr
.write("\nDEBUG: RESPONSE: %r\n" % 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
)
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
)]
390 for key
, subvalue
in value
.items():
391 # Handle filtering via itemspec
393 while itemspec
and '=' in itemspec
[0]:
394 if itemspec
[0] not in key
:
397 itemspec
= itemspec
[1:]
400 item
= extract_item(key
, itemspec
)
403 result
+= make_item_list(path
+ item
, subvalue
, [])
408 # key = 'Catalina:host=localhost,path=\\/,type=Manager'
409 # itemsepc = [ "path" ]
411 def extract_item(key
, itemspec
):
415 path
= key
.split(":", 1)[-1]
416 components
= path
.split(",")
417 comp_dict
= dict(c
.split('=') for c
in components
if c
.count('=') == 1)
420 for pathkey
in itemspec
:
421 if pathkey
in comp_dict
:
422 right
= comp_dict
[pathkey
]
424 right
= '/' + right
.split('/')[-1]
425 item
= item
+ (right
,)
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
)
438 if "threadStatus" in subinstance
or "threadParam" in subinstance
:
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
))
450 tit
= title
+ "." + subinstance
[-1]
454 tit
= subinstance
[-1]
456 yield (item
.replace(" ", "_"), tit
, value
)
460 def _get_queries(do_search
, inst
, itemspec
, title
, path
, mbean
):
462 return [(mbean
+ "/" + path
, title
, itemspec
)]
464 value
= fetch_var(inst
, "search", mbean
)
466 paths
= make_item_list((), value
, "")[0][1]
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
:
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
):
482 except () if DEBUG
else Exception:
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
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
517 inst
.product
= product
519 agentversion
= data
.get('agent', "unknown")
520 yield inst
.name
, product
, version
, agentversion
523 def generate_values(inst
, 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
):
532 yield item
, title
, value
, value_type
534 yield item
, title
, value
537 def generate_json(inst
, mbeans
):
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
):
545 except SkipMBean
if DEBUG
else Exception:
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
}
565 sys
.stderr
.write("DEBUG: configuration: %r\n" % 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
)
576 query_instance(instance
)
581 if __name__
== "__main__":