File: //lib/fm-agent/plugins/tomcat_jmx.py
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