import logging
import agent_util
import jpype
from jpype import java, javax
logger = logging.getLogger(__name__)
class TomcatJMXPlugin(agent_util.Plugin):
"""Tomcat Plugin for the FortiMonitor Agent using JMX to collect data."""
textkey = 'tomcat_jmx'
label = 'Tomcat (JMX)'
JMX_MAPPING = {
# Memory
'memory.heap': ('Heap Memory Usage Used', 'java.lang', 'Memory', None, 'HeapMemoryUsage', 'used', 'bytes'),
'memory.heap.committed': ('Heap Memory Usage Committed', 'java.lang', 'Memory', None, 'HeapMemoryUsage', 'committed', 'bytes'),
'memory.heap.init': ('Heap Memory Usage Init', 'java.lang', 'Memory', None, 'HeapMemoryUsage', 'init', 'bytes'),
'memory.heap.max': ('Heap Memory Usage Max', 'java.lang', 'Memory', None, 'HeapMemoryUsage', 'max', 'bytes'),
'memory.non_heap': ('Non-Heap Memory Usage Used', 'java.lang', 'Memory', None, 'NonHeapMemoryUsage', 'used', 'bytes'),
'memory.non_heap.committed': ('Non-Heap Memory Usage Committed', 'java.lang', 'Memory', None, 'NonHeapMemoryUsage', 'committed', 'bytes'),
'memory.non_heap.init': ('Non-Heap Memory Usage Init', 'java.lang', 'Memory', None, 'NonHeapMemoryUsage', 'init', 'bytes'),
'memory.non_heap.max': ('Non-Heap Memory Usage Max', 'java.lang', 'Memory', None, 'NonHeapMemoryUsage', 'max', 'bytes'),
# Threading
'threading.count': ('Thread Count', 'java.lang', 'Threading', None, 'ThreadCount', None, 'count'),
# OS
'os.cpu_load.process': ('OS Process CPU Load', 'java.lang', 'OperatingSystem', None, 'ProcessCpuLoad', None, 'percent'),
'os.cpu_load.system': ('OS System CPU Load', 'java.lang', 'OperatingSystem', None, 'SystemCpuLoad', None, 'percent'),
'os.open_file_descriptors': ('OS Open File Descriptor Count', 'java.lang', 'OperatingSystem', None, 'OpenFileDescriptorCount', None, 'count'),
# Class loading
'class_loading.loaded_classes': ('Loaded Class Count', 'java.lang', 'ClassLoading', None, 'LoadedClassCount', None, 'count'),
# MemoryPool
'memory_pool.eden': ('Eden Space', 'java.lang', 'MemoryPool', 'Eden Space', 'Usage', 'used', 'bytes'),
'memory_pool.eden.ps': ('PS Eden Space', 'java.lang', 'MemoryPool', 'PS Eden Space', 'Usage', 'used', 'bytes'),
'memory_pool.eden.par': ('Par Eden Space', 'java.lang', 'MemoryPool', 'Par Eden Space', 'Usage', 'used', 'bytes'),
'memory_pool.eden.g1': ('G1 Eden Space', 'java.lang', 'MemoryPool', 'G1 Eden Space', 'Usage', 'used', 'bytes'),
'memory_pool.survivor': ('Survivor Space', 'java.lang', 'MemoryPool', 'Survivor Space', 'Usage', 'used', 'bytes'),
'memory_pool.survivor.ps': ('PS Survivor Space', 'java.lang', 'MemoryPool', 'PS Survivor Space', 'Usage', 'used', 'bytes'),
'memory_pool.survivor.par': ('Par Survivor Space', 'java.lang', 'MemoryPool', 'Par Survivor Space', 'Usage', 'used', 'bytes'),
'memory_pool.survivor.g1': ('G1 Survivor Space', 'java.lang', 'MemoryPool', 'G1 Survivor Space', 'Usage', 'used', 'bytes'),
'memory_pool.old.ps': ('PS Old Gen', 'java.lang', 'MemoryPool', 'PS Old Gen', 'Usage', 'used', 'bytes'),
'memory_pool.old.cms': ('CMS Old Gen', 'java.lang', 'MemoryPool', 'CMS Old Gen', 'Usage', 'used', 'bytes'),
'memory_pool.old.g1': ('G1 Old Gen', 'java.lang', 'MemoryPool', 'G1 Old Gen', 'Usage', 'used', 'bytes'),
# Garbage Collector
'gc.young.copy': ('Copy', 'java.lang', 'GarbageCollector', 'Copy', 'CollectionCount', None, 'count'),
'gc.young.ps_scavenge': ('PS Scavenge', 'java.lang', 'MemoryPool', 'PS Scavenge', 'CollectionCount', None, 'count'),
'gc.young.par_new': ('ParNew', 'java.lang', 'GarbageCollector', 'ParNew', 'CollectionCount', None, 'count'),
'gc.young.g1_generation': ('G1 Young Generation', 'java.lang', 'GarbageCollector', 'G1 Young Generation', 'CollectionCount', None, 'count'),
'gc.mixed.g1_generation': ('G1 Mixed Generation', 'java.lang', 'GarbageCollector', 'G1 Mixed Generation', 'CollectionCount', None, 'count'),
'gc.old.mark_sweep_compact': ('MarkSweepCompact', 'java.lang', 'GarbageCollector', 'MarkSweepCompact', 'CollectionCount', None, 'count'),
'gc.old.ps_mark_sweep': ('PS MarkSweep', 'java.lang', 'GarbageCollector', 'PS MarkSweep', 'CollectionCount', None, 'count'),
'gc.old.concurrent_mark_sweep': ('ConcurrentMarkSweep', 'java.lang', 'GarbageCollector', 'ConcurrentMarkSweep', 'CollectionCount', None, 'count'),
'gc.old.g1_generation': ('G1 Old Generation', 'java.lang', 'GarbageCollector', 'G1 Old Generation', 'CollectionCount', None, 'count'),
}
@staticmethod
def __get_object_name_from_tuple(tuple_):
"""returns a constructed ObjectName.
:type tuple_: tuple (label, domain, type, bean_name, attribute_name,
composite_data_key, unit)
:param tuple_: A tuple with all the information for an ObjectName. A
string that represents the label, a string that represents the domain,
and so on and so forth.
:rtype: javax.management.ObjectName
:return: An ObjectName object that can be used to lookup a MBean.
"""
domain, type_, bean_name = tuple_[1], tuple_[2], tuple_[3]
canonical_name = "%s:" % domain
if bean_name:
canonical_name += "name=%s," % bean_name
if type_:
canonical_name += "type=%s" % type_
return javax.management.ObjectName(canonical_name)
@classmethod
def get_connections_from_config(cls, config):
"""
Parse the config object to build a structure of connections parameters
based on the number of entries that are in each key. The main parameter we base on
to split off is host.
:type config: dict (host, port, username, password, jvm_path)
:param config: Dictionary with the information stored in the config file.
:rtype: Dict
:return: Dictionary with connection information split up in multiple if needed.
"""
keys = ['host', 'port', 'username', 'password', 'jvm_path']
data = {}
for key in keys:
key_value = config.get(key)
if not key_value and key not in ('username', 'password'):
raise ValueError('Missing %s information from config' % key)
elif not key_value and key in ('username', 'password'):
# Username and password are not required
continue
else:
values = [value.strip(' ') for value in key_value.split(',')]
data[key] = values
connections = {}
hosts = data['host']
for index, host in enumerate(hosts):
connections[index] = {
'host': host,
}
for key in ['port', 'username', 'password', 'jvm_path']:
if len(data.get(key, [])) > 1:
# Multiple entries in this config, use the index to apply.
value = data[key][index]
elif len(data.get(key, [])) == 1:
value = data[key][0]
elif key not in ('username', 'password'):
raise ValueError('Missing %s information from config' % (key))
else:
# Username and password can be skipped
continue
connections[index][key] = value
return connections
@classmethod
def __get_connection(cls, config):
"""
returns a list of connections from the jpype library - a python interface to
the Java Native Interface. Wheter there are 1 or many connections depends on the
number of entries in the host, port and optionally username/password/jvm entries.
:type config: dict
:param config: Mapping of information under the application block for
this plugin.
:rtype: tuple (status, connection, error_message)
:return: A tuple containing a numeric value corresponding to the
agent_util status'. A MBeanServerConnection object. And, a string with
an error message if any.
"""
status = agent_util.SUPPORTED
msg = None
# Check that we have an agent config
if not config:
msg = "No JMX configuration found"
cls.log.info(msg)
status = agent_util.MISCONFIGURED
# Make sure at least host and port are in the config
if "host" not in config or "port" not in config:
msg = ("Missing value in the [%s] block of the agent config file"
" (e.g host, port).") % cls.textkey
cls.log.info(msg)
status = agent_util.MISCONFIGURED
# Try and get the jvm path from the config
jvm_path = config.get('jvm_path')
# If we can't find it then try and use the default
if not jvm_path:
try:
jvm_path = jpype.getDefaultJVMPath()
if not jvm_path:
status = agent_util.MISCONFIGURED
msg = ("Unable to find JVM, please specify 'jvm_path' in"
" the [%s] block of the agent config file.") % cls.textkey
cls.log.info(msg)
except:
status = agent_util.MISCONFIGURED
msg = ("Unable to find JVM, please specify 'jvm_path' in the"
" [%s] block of the agent config file.") % cls.textkey
cls.log.info(msg)
try:
# If the JVM has not been started try and start it
if status is agent_util.SUPPORTED and not jpype.isJVMStarted():
jpype.startJVM(jvm_path)
except:
status = agent_util.MISCONFIGURED
msg = "Unable to access JMX metrics because JVM cannot be started."
cls.log.info(msg)
if status is agent_util.SUPPORTED:
try:
# Start the JVM if its not started
# XXX: Redundant logic - is this necessary?
if not jpype.isJVMStarted():
jpype.startJVM(jvm_path)
j_hash = java.util.HashMap()
# If we have a username and password use it as our credentials
if config.get('username') and config.get('password'):
j_array = jpype.JArray(java.lang.String)(
[config.get('username'), config.get('password')])
j_hash.put(
javax.management.remote.JMXConnector.CREDENTIALS,
j_array)
url = "service:jmx:rmi:///jndi/rmi://%s:%s/jmxrmi" % (
config.get('host'), int(config.get('port')))
jmx_url = javax.management.remote.JMXServiceURL(url)
jmx_soc = javax.management.remote.JMXConnectorFactory.connect(
jmx_url, j_hash)
connection = jmx_soc.getMBeanServerConnection()
return status, connection, None
except Exception:
msg = ("Unable to access JMX metrics, JMX is not running or"
" not installed.")
cls.log.exception(msg)
return status, None, msg
@classmethod
def get_metadata(cls, config):
"""returns a json object who's textkeys correspond to a given metric
available on the JVM.
:type config: dict
:param config: Mapping of information under the application block for
this plugin.
:return: JSON Object for all metrics
"""
result = {}
configs = cls.get_connections_from_config(config)
connections = {}
errors = []
for entry in configs.values():
status, connection, msg = cls.__get_connection(entry)
connection_key = "%s:%s" % (entry['host'], entry['port'])
if msg:
errors.append('%s %s' % (connection_key, msg))
else:
connections[connection_key] = connection
if not connections.keys():
cls.log.info('Unable to connect to any connection')
for msg in errors:
cls.log.error(msg)
return result
else:
status = agent_util.SUPPORTED
msg = ''
for error in errors:
cls.log.warning(error)
for key, tuple_ in cls.JMX_MAPPING.items():
object_name = cls.__get_object_name_from_tuple(tuple_)
# Check to see if the object exists, if it doesnt we will throw
# an error which will be handled silently by continuing through
# the for loop.
options = []
for connection_key, connection in connections.items():
try:
connection.getObjectInstance(object_name)
options.append(connection_key)
except Exception:
cls.log.exception("Tomcat (JMX) plugin - %s bean not found at %s." % (object_name, connection_key))
continue
if len(connections.keys()) >= 1 and not options:
# No connection was able to get this value. Set it to unsupported.
options = None
status = agent_util.UNSUPPORTED
msg = 'Unreachable %s at any connection' % key
else:
# We found options. Is supported.
msg = ''
status = agent_util.SUPPORTED
label, unit = tuple_[0], tuple_[6]
result[key] = {
"label": label,
"options": options,
"status": status,
"error_message": msg,
"unit": unit
}
return result
@classmethod
def check(cls, textkey, data, config):
"""returns a value for the metric.
:type textkey: string
:param textkey: Canonical name for a metric.
:type data: string
:param data: Specific option to check for.
:type config: dict
:param config: Mapping of information under the application block for
this plugin.
:rtype: double
:return: Value for a specific metric
"""
entries = cls.get_connections_from_config(config)
if data:
for entry in entries.values():
possible_match = "%s:%s" % (entry['host'], entry['port'])
if possible_match == data:
config = entry
else:
# Default to the first configuration
config = entries[0]
status, connection, msg = cls.__get_connection(config)
if msg:
cls.log.info("Failed to get a connection: %s" % msg)
return None
tuple_ = cls.JMX_MAPPING.get(textkey)
attribute_name, composite_data_key = tuple_[4], tuple_[5]
# Create an ObjectName object to lookup
object_name = cls.__get_object_name_from_tuple(tuple_)
try:
object_instance = connection.getObjectInstance(object_name)
attribute_value = connection.getAttribute(
object_instance.getObjectName(),
attribute_name)
attribute_class_name = attribute_value.__class__.__name__
# If the object returned is just a numeric value
if "CompositeDataSupport" not in attribute_class_name:
return attribute_value.floatValue()
# If the attribute object does not have the composite data key
# return none
if not attribute_value.containsKey(composite_data_key):
return None
# If the object returned is of type CompositeDataSupport return the
# correct value from that object
check_result = attribute_value.get(composite_data_key)
return check_result.floatValue()
except:
cls.log.info(
"Tomcat (JMX) plugin - %s bean not found." % object_name)
return 0