diff --git a/.gitignore b/.gitignore index 8d77f88..c3fcb62 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ nbactions.xml *.log MANIFEST python/jmxquery.egg-info/ +.vscode/ +venv/ +venv3/ +*.pyc diff --git a/python/jmxquery/__init__.py b/python/jmxquery/__init__.py index 795d97b..3e82538 100644 --- a/python/jmxquery/__init__.py +++ b/python/jmxquery/__init__.py @@ -1,46 +1,56 @@ #!/usr/bin/env python3 """ - Python interface to JMX. Uses local jar to pass commands to JMX and read JSON - results returned. + Python interface to JMX. Uses local jar to pass commands to JMX. + Results returned. """ - -import subprocess +try: + import subprocess32 as subprocess +except: + import subprocess import os import json -from typing import List from enum import Enum import logging # Full Path to Jar -JAR_PATH = os.path.dirname(os.path.realpath(__file__)) + '/JMXQuery-0.1.8.jar' -# Default Java path +JAR_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'JMXQuery-0.1.8.jar' + ) + +# Default Java path and default timeout in seconds DEFAULT_JAVA_PATH = 'java' -# Default timeout for running jar in seconds DEFAULT_JAR_TIMEOUT = 10 -logger = logging.getLogger(__name__) +log = logging.getLogger('jmxQuery') + class MetricType(Enum): COUNTER = 'counter' GAUGE = 'gauge' + class JMXQuery: """ - A JMX Query which is used to fetch specific MBean attributes/values from the JVM. The object_name can support wildcards - to pull multiple metrics at once, for example '*:*' will bring back all MBeans and attributes in the JVM with their values. - - You can set a metric name if you want to override the generated metric name created from the MBean path + A JMX Query which is used to fetch specific MBean + attributes/values from the JVM. The object_name + can support wildcards to pull multiple metrics + at once, for example '*:*' will bring back all + MBeans and attributes in the JVM with their values. + + You can set a metric name if you want to override + the generated metric name created from the MBean path. """ def __init__(self, - mBeanName: str, - attribute: str = None, - attributeKey: str = None, - value: object = None, - value_type: str = None, - metric_name: str = None, - metric_labels: dict = None): + mBeanName=None, + attribute=None, + attributeKey=None, + value=None, + value_type=None, + metric_name=None, + metric_labels=None): self.mBeanName = mBeanName self.attribute = attribute @@ -48,127 +58,145 @@ def __init__(self, self.value = value self.value_type = value_type self.metric_name = metric_name - self.metric_labels = metric_labels + self.metric_labels = metric_labels or {} - def to_query_string(self) -> str: + def to_query_string(self): """ Build a query string to pass via command line to JMXQuery Jar - :return: The query string to find the MBean in format: - + {mBeanName}/{attribute}/{attributeKey} - - Example: java.lang:type=Memory/HeapMemoryUsage/init + + Example: java.lang:type=Memory/HeapMemoryUsage/init """ - query = "" + query = [] if self.metric_name: - query += self.metric_name - - if ((self.metric_labels != None) and (len(self.metric_labels) > 0)): - query += "<" - keyCount = 0 - for key, value in self.metric_labels.items(): - query += key + "=" + value - keyCount += 1 - if keyCount < len(self.metric_labels): - query += "," - query += ">" - query += "==" - - query += self.mBeanName + query.append(self.metric_name) + if self.metric_labels: + query.append("<") + total_count = len(self.metric_labels) + for count, (key, value) in enumerate( + self.metric_labels.items(), + 1 + ): + query.append("{} = {}".format( + key, + value) + ) + if count < total_count: + query.append(",") + query.append(">") + query.append("==") + + query.append(self.mBeanName) if self.attribute: - query += "/" + self.attribute + query.append("/%s" % self.attribute) if self.attributeKey: - query += "/" + self.attributeKey + query.append("/%s" % self.attributeKey) - return query + return ''.join(query) def to_string(self): - - string = "" + string = [] if self.metric_name: - string += self.metric_name - - if ((self.metric_labels != None) and (len(self.metric_labels) > 0)): - string += " {" - keyCount = 0 - for key, value in self.metric_labels.items(): - string += key + "=" + value - keyCount += 1 - if keyCount < len(self.metric_labels): - string += "," - string += "}" + string.append(self.metric_name) + if self.metric_labels: + string.append(" {") + total_count = len(self.metric_labels) + for count, (key, value) in enumerate( + self.metric_labels.items(), + 1 + ): + string.append("{} = {}".format(key, value)) + if count < total_count: + string.append(",") + string.append("}") else: - string += self.mBeanName + string.append(self.mBeanName) if self.attribute: - string += "/" + self.attribute + string.append("/%s" % self.attribute) if self.attributeKey: - string += "/" + self.attributeKey + string.append("/%s" % self.attributeKey) - string += " = " - string += str(self.value) + " (" + self.value_type + ")" + string.append(" = ") + string.append("%s ( %s )" % ( + self.value, + self.value_type + )) - return string + return ''.join(string) class JMXConnection(object): """ - The main class that connects to the JMX endpoint via a local JAR to run queries + The main class that connects to the JMX endpoint via a local JAR + to run queries """ - - def __init__(self, connection_uri: str, jmx_username: str = None, jmx_password: str = None, java_path: str = DEFAULT_JAVA_PATH): + def __init__(self, + uri=None, + user=None, + passwd=None, + jpath=DEFAULT_JAVA_PATH): """ - Creates instance of JMXQuery set to a specific connection uri for the JMX endpoint + Creates instance of JMXQuery set to a specific connection uri + for the JMX endpoint. - :param connection_uri: The JMX connection URL. E.g. service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi - :param jmx_username: (Optional) Username if JMX endpoint is secured - :param jmx_password: (Optional) Password if JMX endpoint is secured - :param java_path: (Optional) Provide an alternative Java path on the machine to run the JAR. - Default is 'java' which will use the machines default JVM + :param uri: The JMX connection URL. E.g. + + service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi + + :param user: (Optional) Username if JMX endpoint is secured + :param passwd: (Optional) Password if JMX endpoint is secured + :param jpath: (Optional) Java path. Default is 'java' """ - self.connection_uri = connection_uri - self.jmx_username = jmx_username - self.jmx_password = jmx_password - self.java_path = java_path - def __run_jar(self, queries: List[JMXQuery], timeout) -> List[JMXQuery]: + self.connection_uri = uri + self.jmx_username = user + self.jmx_password = passwd + self.java_path = jpath + + def run_jar(self, queries, timeout): """ Run the JAR and return the results - :param query: The query - :return: The full command array to run via subprocess + :param query: The query + :return: The full command array to run via subprocess """ - command = [self.java_path, '-jar', JAR_PATH, '-url', self.connection_uri, "-json"] - if (self.jmx_username): - command.extend(["-u", self.jmx_username, "-p", self.jmx_password]) - - queryString = "" + command = [ + self.java_path, + '-jar', + JAR_PATH, + '-url', + self.connection_uri, + "-json"] + + if self.jmx_username: + command.extend([ + "-u", + self.jmx_username, + "-p", + self.jmx_password]) + + queryString = [] for query in queries: - queryString += query.to_query_string() + ";" + queryString.append("%s;" % query.to_query_string()) - command.extend(["-q", queryString]) - logger.debug("Running command: " + str(command)) + command.extend(["-q", ''.join(queryString)]) jsonOutput = "[]" - try: - output = subprocess.run(command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=timeout, - check=True) - - jsonOutput = output.stdout.decode('utf-8') - except subprocess.TimeoutExpired as err: - logger.error("Error calling JMX, Timeout of " + str(err.timeout) + " Expired: " + err.output.decode('utf-8')) - except subprocess.CalledProcessError as err: - logger.error("Error calling JMX: " + err.output.decode('utf-8')) - raise err - - logger.debug("JSON Output Received: " + jsonOutput) - metrics = self.__load_from_json(jsonOutput) + output = subprocess.run(command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + check=True) + + jsonOutput = output.stdout.decode('utf-8') + + metrics = self.load_from_json(jsonOutput) + return metrics - def __load_from_json(self, jsonOutput: str) -> List[JMXQuery]: + def load_from_json(self, jsonOutput): """ Loads the list of returned metrics from JSON response @@ -177,31 +205,41 @@ def __load_from_json(self, jsonOutput: str) -> List[JMXQuery]: """ jsonMetrics = json.loads(jsonOutput) metrics = [] - for jsonMetric in jsonMetrics: - mBeanName = jsonMetric['mBeanName'] - attribute = jsonMetric['attribute'] - attributeType = jsonMetric['attributeType'] + + for jm in jsonMetrics: + mBeanName = jm['mBeanName'] + attribute = jm['attribute'] + attributeType = jm['attributeType'] metric_name = None - if 'metricName' in jsonMetric: - metric_name = jsonMetric['metricName'] + if 'metricName' in jm: + metric_name = jm['metricName'] metric_labels = None - if 'metricLabels' in jsonMetric: - metric_labels = jsonMetric['metricLabels'] + if 'metricLabels' in jm: + metric_labels = jm['metricLabels'] attributeKey = None - if 'attributeKey' in jsonMetric: - attributeKey = jsonMetric['attributeKey'] + if 'attributeKey' in jm: + attributeKey = jm['attributeKey'] value = None - if 'value' in jsonMetric: - value = jsonMetric['value'] + if 'value' in jm: + value = jm['value'] + + metrics.append(JMXQuery( + mBeanName, + attribute, + attributeKey, + value, + attributeType, + metric_name, + metric_labels)) - metrics.append(JMXQuery(mBeanName, attribute, attributeKey, value, attributeType, metric_name, metric_labels)) return metrics - def query(self, queries: List[JMXQuery], timeout=DEFAULT_JAR_TIMEOUT) -> List[JMXQuery]: + def query(self, queries, timeout=DEFAULT_JAR_TIMEOUT): """ Run a list of JMX Queries against the JVM and get the results - :param queries: A list of JMXQuerys to query the JVM for - :return: A list of JMXQuerys found in the JVM with their current values + :param queries: A list of JMXQuerys to query the JVM for + :return: list of query results with their current values """ - return self.__run_jar(queries, timeout) + + return self.run_jar(queries, timeout) diff --git a/python/setup.py b/python/setup.py index f012212..423d103 100644 --- a/python/setup.py +++ b/python/setup.py @@ -21,6 +21,10 @@ setup( name = 'jmxquery', packages = ['jmxquery'], + install_requires = [ + 'subprocess32; python_version < "3.5"', + 'enum34; python_version < "3.4"' + ], version = '0.6.0', description = 'A JMX Interface for Python to Query runtime metrics in a JVM', long_description=long_description, diff --git a/python/tests/test_JMXQuery.py b/python/tests/test_JMXQuery.py index 7d2bd7f..64678e3 100644 --- a/python/tests/test_JMXQuery.py +++ b/python/tests/test_JMXQuery.py @@ -5,6 +5,7 @@ docker-compose -f docker-compose-kafka.yaml up """ +from __future__ import print_function import logging, sys import threading from nose.tools import assert_greater_equal @@ -126,8 +127,16 @@ def test_threading(): def printMetrics(metrics): for metric in metrics: if metric.metric_name: - print(f"{metric.metric_name}<{metric.metric_labels}> == {metric.value}") + print("{%s}<{%s}> == {%s}" % ( + metric.metric_name, + metric.metric_labels, + metric.value + )) else: - print(f"{metric.to_query_string()} ({metric.value_type}) = {metric.value}") + print("{%s} ({%s}) = {%s}" % ( + metric.to_query_string(), + metric.value_type, + metric.value + )) - print("===================\nTotal Metrics: " + str(len(metrics))) \ No newline at end of file + print("===================\nTotal Metrics: " + str(len(metrics)))