# pylint: disable=too-many-public-methods, too-many-instance-attributes # pylint: disable=too-many-arguments ''' Performance Metrics Domain Agent exporting systemtap JSON metrics. ''' import json import jsonpointer import collections from pcp.pmda import PMDA, pmdaMetric import cpmapi as c_api from pcp.pmapi import pmUnits from ctypes import c_int, POINTER, cast import os import traceback class Metric(object): ''' Metric information class ''' def __init__(self, name_prefix, cluster, idx, pmda): self.__name = '' self.name_prefix = name_prefix self.full_name = '' self.cluster = cluster self.idx = idx self.__pmda = pmda self.desc = '' self.type = c_api.PM_TYPE_UNKNOWN self.sem = c_api.PM_SEM_INSTANT self.pointer = None self.pmid = None self.obj = None self.indom = None self.index_pointer = None # Make sure when setting 'name', 'full_name' also gets updated. @property def name(self): return self.__name @name.setter def name(self, name): self.__name = name self.full_name = "%s.%s" % (self.name_prefix, name) def valid(self): return (self.__name != '' and self.type != c_api.PM_TYPE_UNKNOWN) def create(self): ''' Create the metric. Note that the metric will still need to be added to the PMDA. ''' if not self.valid(): self.__pmda.log("Invalid metric") raise RuntimeError("Invalid metric") self.pmid = self.__pmda.pmid(self.cluster, self.idx) # FIXME: we'll need to handle units/scale at some point... if self.indom != None: self.obj = pmdaMetric(self.pmid, self.type, self.indom.obj, self.sem, pmUnits(0, 0, 0, 0, 0, 0)) else: self.obj = pmdaMetric(self.pmid, self.type, c_api.PM_INDOM_NULL, self.sem, pmUnits(0, 0, 0, 0, 0, 0)) # Note that you can't delete individual metrics. The # pmda.{clear,reset}_metrics() functions clear out *all* metrics. class Indom(object): ''' Indom (instance domain) information class ''' def __init__(self, idx, pmda): self.__pmda = pmda self.idx = idx self.obj = pmda.indom(self.idx) self.values = {} def add_value(self, name, value): ''' Add a value to the indom ''' # PMDA.replace_indom() wants a dictionary, indexed by # indom string value. PMDA.replace_indom() doesn't really # care what is stored at that string value. We're storing the # array index there. self.values[name] = c_int(value) def lookup_inst(self, inst): ''' Lookup an array index based on the instance ID ''' voidp = self.__pmda.inst_lookup(self.obj, inst) if voidp == None: return None valuep = cast(voidp, POINTER(c_int)) return valuep.contents.value class SystemtapModule(object): ''' Systemtap Module class. Contains all metrics and data needed by a single systemtap module. ''' def __init__(self, path, cluster, pmda): self.__path = path self.cluster = cluster self.__pmda = pmda self.__root_name = os.path.basename(path) self.__metadata_path = "%s/metadata" % path self.__metadata = {} self.__data_path = "%s/data" % path self.__json_data = {} self.__metrics = {} self.__metrics_by_name = {} self.__metric_idx = 0 self.__indoms = {} def __load_json_metadata(self): ''' Load the JSON metadata file for this systemtap module. ''' self.__metadata = {} try: fobj = open(self.__metadata_path) except IOError: self.__pmda.log("Couldn't open JSON metadata file: %s" % self.__metadata_path) self.__pmda.log("%s" % traceback.format_exc()) return try: self.__metadata = json.load(fobj, object_pairs_hook=collections.OrderedDict) except ValueError: self.__pmda.log("Couldn't parse JSON metadata from %s" % self.__metadata_path) self.__pmda.log("%s" % traceback.format_exc()) fobj.close() def __load_json_data(self): ''' Load the JSON data file for this systemtap module. ''' self.__json_data = {} try: fobj = open(self.__data_path) except IOError: self.__pmda.log("Couldn't open JSON data file: %s" % self.__data_path) self.__pmda.log("%s" % traceback.format_exc()) return try: self.__json_data \ = json.load(fobj, object_pairs_hook=collections.OrderedDict) except ValueError: self.__pmda.log("Couldn't parse JSON data from %s" % self.__data_path) self.__pmda.log("%s" % traceback.format_exc()) fobj.close() def load(self): ''' Load the JSON metadata and data files for this systemtap module, then create metrics based on the JSON. ''' if self.__pmda.debug: self.__pmda.log("Loading module %s" % self.__root_name) self.__load_json_metadata() self.__load_json_data() # If either loading the metadata or data failed, quit. if self.__metadata == {} or self.__json_data == {}: self.cleanup() return # Update the indom list. self.__refresh_indoms() # Parse the metadata, creating metrics as needed. try: self.__parse_metadata() except TypeError: self.__pmda.log("Couldn't parse JSON metadata") self.__pmda.log("%s" % traceback.format_exc()) def refresh_json_data(self): ''' Reload the JSON data and update indoms. ''' # Just loading the JSON data. self.__load_json_data() # Update the indom list. self.__refresh_indoms() def cleanup(self): ''' Cleanup module data. ''' self.__metadata = {} self.__json_data = {} self.__metrics = {} def __refresh_indoms(self): ''' Refresh the list of indoms. ''' # Notice we never delete indoms, we just keep adding. for array_name in self.__indoms.keys(): index = 0 try: metric_info = self.__metrics_by_name[array_name] metrics_array = metric_info.pointer.resolve(self.__json_data) # Loop through all the array items, updating the indom # list with the array's index pointer values. for item in metrics_array: indom_value = metric_info.index_pointer.resolve(item) self.__indoms[array_name].add_value(indom_value, index) index += 1 except KeyError: self.__pmda.log("Error while refreshing indom for array %s" % array_name) self.__pmda.log("%s" % traceback.format_exc()) continue self.__pmda.replace_indom(self.__indoms[array_name].obj, self.__indoms[array_name].values) def __add_metric(self, metric_info): ''' Create and add a metric to the pmda. ''' metric_info.create() # Add the metric to the pmda (unless it is an array metric). if metric_info.type != c_api.PM_TYPE_NOSUPPORT: self.__pmda.add_metric(metric_info.full_name, metric_info.obj, metric_info.desc) self.__metrics[metric_info.idx] = metric_info self.__metrics_by_name[metric_info.name] = metric_info def __parse_array_metadata(self, array_name, metrics_array): ''' Parse a JSON array metadata. ''' if not self.__indoms.has_key(array_name): # Note that we're creating an indom here, but we don't # know any values for it yet. We'll get those on a data # read. self.__indoms[array_name] = Indom(self.__pmda.indom_idx, self.__pmda) self.__pmda.indom_idx += 1 # Process the array's metrics array. for item in metrics_array: metric_info = Metric("%s.%s" % (self.__pmda.pmda_name, self.__root_name), self.cluster, self.__metric_idx, self.__pmda) self.__metric_idx += 1 metric_info.indom = self.__indoms[array_name] for (key, value) in item.iteritems(): # 'name' (required): Sanity check it and save it. if key == 'name': if not isinstance(value, unicode): self.__pmda.log("Invalid metadata 'name' value") raise TypeError("Invalid metadata 'name' value") metric_info.name = "%s.%s" % (array_name, value) # 'type' (required): Sanity check it and save it. elif key == 'type': if not isinstance(value, unicode): self.__pmda.log("Invalid metadata 'type' value") raise TypeError("Invalid metadata 'type' value") if value == 'string': metric_info.type = c_api.PM_TYPE_STRING metric_info.sem = c_api.PM_SEM_INSTANT elif value == 'integer': metric_info.type = c_api.PM_TYPE_64 metric_info.sem = c_api.PM_SEM_COUNTER elif value == 'array': # We don't allow arrays to have sub-arrays. self.__pmda.log("Arrays can't contain arrays") raise TypeError("Arrays can't contain arrays") else: self.__pmda.log("Type attribute has unknown value '%s'" % value) raise TypeError("Type attribute has unknown value '%s'" % value) # 'pointer' (required): Sanity check it and save it. elif key == 'pointer': if not isinstance(value, unicode): self.__pmda.log("Invalid metadata 'pointer' value") raise TypeError("Invalid metadata 'pointer' value") metric_info.pointer = jsonpointer.JsonPointer(value) # 'description' (optional): Type check it and save it. elif key == 'description': if not isinstance(value, unicode): self.__pmda.log("Invalid schema 'description' value") raise TypeError("Invalid schema 'description' value") metric_info.desc = value # For everything else, raise an error. # FIXME: or just ignore it? else: self.__pmda.log("Metadata has an unknown attribute '%s'" % (key)) raise TypeError("Metadata has an unknown attribute '%s'" % (key)) # Make sure we have everything we need. if not metric_info.valid() or metric_info.pointer == None: self.__pmda.log("Metadata doesn't have required" " information for the following entry: %s" % metric_info.name) raise TypeError("Metadata doesn't have required" " information for the following entry: %s" % metric_info.name) # We have all the required information. Add the metric. self.__add_metric(metric_info) def __parse_metadata(self): ''' Go through the meta, looking for information we can use to create the pcp representation of the metadata. ''' # Look for the "metrics" array. metrics_array = None for (key, value) in self.__metadata.items(): # 'metrics' (required): Save it. if key == 'metrics': metrics_array = value # 'prefix' (optional): Sanity check it and save it. elif key == 'prefix': if not isinstance(value, unicode): self.__pmda.log("Invalid metadata 'prefix' value") raise TypeError("Invalid metadata 'prefix' value") # FIXME: need to validate name and make sure it # doesn't already exist. self.__root_name = value # For everything else, raise an error. # FIXME: or just ignore it? else: self.__pmda.log("Metadata has an unknown attribute '%s'" % key) raise TypeError("Metadata has an unknown attribute '%s'" % key) if not metrics_array: self.__pmda.log("Metadata has no 'metrics' array") raise TypeError("Metadata has no 'metrics' array") # Process the metrics array. for item in metrics_array: metric_info = Metric("%s.%s" % (self.__pmda.pmda_name, self.__root_name), self.cluster, self.__metric_idx, self.__pmda) self.__metric_idx += 1 for (key, value) in item.iteritems(): # 'name' (required): Sanity check it and save it. if key == 'name': if not isinstance(value, unicode): self.__pmda.log("Invalid metadata 'name' value") raise TypeError("Invalid metadata 'name' value") metric_info.name = value # 'type' (required): Sanity check it and save it. elif key == 'type': if not isinstance(value, unicode): self.__pmda.log("Invalid metadata 'type' value") raise TypeError("Invalid metadata 'type' value") if value == 'string': metric_info.type = c_api.PM_TYPE_STRING metric_info.sem = c_api.PM_SEM_INSTANT elif value == 'integer': metric_info.type = c_api.PM_TYPE_64 metric_info.sem = c_api.PM_SEM_COUNTER elif value == 'array': # For arrays, we have to create metrics for # each subitem in the array, using the same # indom. This happens in the 'metrics' handling # below. metric_info.type = c_api.PM_TYPE_NOSUPPORT else: self.__pmda.log("Type attribute has unknown value '%s'" % value) raise \ TypeError("Type attribute has unknown value '%s'" % value) # 'pointer' (required): Sanity check it and save it. elif key == 'pointer': if not isinstance(value, unicode): self.__pmda.log("Invalid metadata 'pointer' value") raise TypeError("Invalid metadata 'pointer' value") metric_info.pointer = jsonpointer.JsonPointer(value) # 'description' (optional): Type check it and save it. elif key == 'description': if not isinstance(value, unicode): self.__pmda.log("Invalid schema 'description' value") raise TypeError("Invalid schema 'description' value") metric_info.desc = value # 'index' (required for arrays): Type check and save it. elif key == 'index': if not isinstance(value, unicode): self.__pmda.log("Invalid schema 'index' value") raise TypeError("Invalid schema 'index' value") if metric_info.type != c_api.PM_TYPE_NOSUPPORT: self.__pmda.log("Metadata has an 'index' item" " for non-array '%s'" % metric_info.name) raise TypeError("Metadata has an 'index' item" " for non-array '%s'" % metric_info.name) # If we're here, we're processing an array's # metadata metric_info.index_pointer = jsonpointer.JsonPointer(value) # 'metrics' (required for arrays): Process it. elif key == 'metrics': if metric_info.type != c_api.PM_TYPE_NOSUPPORT: self.__pmda.log("Metadata has an 'metrics' item" " for non-array '%s'" % metric_info.name) raise TypeError("Metadata has an 'metrics' item" " for non-array '%s'" % metric_info.name) # If we're here, we're processing an array's # metadata. For arrays, we have to create metrics # for each subitem in the array, using the same # indom. self.__parse_array_metadata(metric_info.name, value) # For everything else, raise an error. # FIXME: or just ignore it? else: self.__pmda.log("Metadata has an unknown attribute '%s'" % key) raise TypeError("Metadata has an unknown attribute '%s'" % key) # Make sure we have everything we need. if not metric_info.valid() or metric_info.pointer == None: self.__pmda.log("Metadata doesn't have required" " information for the following entry: %s" % item) raise TypeError("Metadata doesn't have required" " information for the following entry: %s" % item) # We have all the required information. Add the metric. if self.__pmda.debug: self.__pmda.log("Adding metric '%s'" % metric_info.name) self.__add_metric(metric_info) def fetch(self, item, inst): ''' Fetch value for this item and instance. ''' if not self.__metrics.has_key(item): self.__pmda.log("Module '%s' has no item %d instance %d" % (self.__root_name, item, inst)) return [c_api.PM_ERR_PMID, 0] metric_info = self.__metrics[item] # Handle array metrics. if metric_info.indom != None: # Get the array index from the indom. array_index = metric_info.indom.lookup_inst(inst) if array_index == None: self.__pmda.log("Module %s has no indom %d" % (self.__root_name, inst)) return [c_api.PM_ERR_INST, 0] # Split the full name into the array name and metric (array, metric) = metric_info.name.split('.', 2) if not self.__metrics_by_name.has_key(array): self.__pmda.log("Module '%s' has no item '%s'" % (self.__root_name, array)) return [c_api.PM_ERR_PMID, 0] array_info = self.__metrics_by_name[array] try: # Get the entire array. metrics_array = array_info.pointer.resolve(self.__json_data) # Get the appropriate item from within the desired array entry. return [metric_info.pointer.resolve(metrics_array[array_index]), 1] except KeyError: self.__pmda.log("Error while fetching metrics for array %s" % array_info.name) self.__pmda.log("%s" % traceback.format_exc()) pass # Handle single-valued metrics. else: try: if metric_info.type == c_api.PM_TYPE_STRING: type_str = 'string' elif metric_info.type == c_api.PM_TYPE_64: type_str = 'int' else: type_str = 'unknown' return [metric_info.pointer.resolve(self.__json_data), 1] except KeyError: self.__pmda.log("Error while fetching metric %s" % metric_info.name) self.__pmda.log("%s" % traceback.format_exc()) pass self.__pmda.log("Module %s couldn't fetch value for item %d instance %d" % (self.__root_name, item, inst)) return [c_api.PM_ERR_TYPE, 0] def refresh_metrics(self): ''' Refresh metrics by re-adding all metrics for this module to the PMDA. ''' for (dummy, metric) in self.__metrics.items(): self.__pmda.add_metric(metric.full_name, metric.obj, metric.desc) class StapJsonPMDA(PMDA): ''' systemtap JSON PMDA class ''' def __init__(self, pmda_name, domain): self.pmda_name = pmda_name PMDA.__init__(self, self.pmda_name, domain) self.indom_idx = 0 # cluster 0 is reserved for the static metrics self.cluster_idx = 1 self.__metrics = {} # FIXME: python doesn't have a __pmParseDebug() wrapper. So, # if PCP_PYTHON_DEBUG has any value, turn debugging on. self.debug = os.environ.has_key('PCP_PYTHON_DEBUG') self.__add_static_metrics() # Load all the metadata files and json data. self.modules_by_root = {} self.modules_by_cluster = {} self.__load_all_json() self.set_fetch_callback(self.__fetch_callback) self.set_refresh_metrics(self.__refresh_metrics) if self.debug: self.log("__init__ finished") def __add_static_metrics(self): ''' Create all the static metrics (not from a systemtap module). ''' # Create our 'nmodules' metric. metric_info = Metric(self.pmda_name, 0, 0, self) metric_info.name = 'nmodules' metric_info.type = c_api.PM_TYPE_64 metric_info.sem = c_api.PM_SEM_COUNTER metric_info.desc = 'Number of modules' metric_info.create() self.add_metric(metric_info.full_name, metric_info.obj, metric_info.desc) self.__metrics[metric_info.idx] = metric_info # Create out 'debug' metric. metric_info = Metric(self.pmda_name, 0, 1, self) metric_info.name = 'debug' metric_info.type = c_api.PM_TYPE_64 metric_info.sem = c_api.PM_SEM_INSTANT metric_info.desc = 'Debug logging state' metric_info.create() self.add_metric(metric_info.full_name, metric_info.obj, metric_info.desc) self.__metrics[metric_info.idx] = metric_info def __remove_modules(self, removed_modules): ''' Clean up a list of removed modules. ''' if len(removed_modules): for root in removed_modules: if self.debug: self.log("Removing systemtap module '%s'" % os.path.basename(root)) self.modules_by_root[root].cleanup() cluster = self.modules_by_root[root].cluster del self.modules_by_root[root] del self.modules_by_cluster[cluster] def __load_all_json(self): ''' Walk the filesystem and load the JSON metadata/data for every module found. ''' if self.debug: self.log("load_all_json entry") modules_seen = {} for root, dummy, files in os.walk('/proc/systemtap'): # Make sure we have both of the files we're looking for if 'metadata' in files and 'data' in files: if not self.modules_by_root.has_key(root): self.modules_by_root[root] \ = SystemtapModule(root, self.cluster_idx, self) self.modules_by_cluster[self.cluster_idx] \ = self.modules_by_root[root] self.cluster_idx += 1 self.modules_by_root[root].load() modules_seen[root] = 1 # Cleanup all removed modules. removed_modules = [k for k in self.modules_by_root \ if k not in modules_seen] self.__remove_modules(removed_modules) if self.debug: self.log("load_all_json exit") def __load_all_json_data(self): ''' Walk the filesystem and refresh the JSON data for every module found. ''' if self.debug: self.log("load_all_json_data entry") # FIXME: why do we need 'load_all_json()' and 'load_all_json_data()'? modules_seen = {} refresh_metrics = 0 for root, dummy, files in os.walk('/proc/systemtap'): # Make sure we have both of the files we're looking for if 'metadata' in files and 'data' in files: if not self.modules_by_root.has_key(root): self.modules_by_root[root] \ = SystemtapModule(root, self.cluster_idx, self) self.modules_by_cluster[self.cluster_idx] \ = self.modules_by_root[root] self.cluster_idx += 1 self.modules_by_root[root].load() if self.debug: self.log("load_all_json_data: adding module '%s'" % root) else: self.modules_by_root[root].refresh_json_data() modules_seen[root] = 1 # Cleanup all removed modules. removed_modules = [k for k in self.modules_by_root \ if k not in modules_seen] if len(removed_modules) > 0: if self.debug: self.log("load_all_json_data: removed modules found") refresh_metrics = 1 self.__remove_modules(removed_modules) # If we've got a new or removed module, we need to recreate # the metrics. if refresh_metrics == 1: if self.debug: self.log("*** refreshing metrics ***") self.clear_metrics() # First refresh static metrics. for (dummy, metric) in self.__metrics.items(): self.add_metric(metric.full_name, metric.obj, metric.desc) # Now ask each systemtap module to refresh its metrics. for (root, module) in self.modules_by_root.items(): module.refresh_metrics() if self.debug: self.log("load_all_json_data exit") def __refresh_metrics(self): ''' Called before callbacks. This allows us to update the list of metrics if needed. ''' if self.debug: self.log("__refresh_metrics entry") self.__load_all_json_data() if self.debug: self.log("__refresh_metrics exit") return def __fetch_callback(self, cluster, item, inst): ''' Main fetch callback. Returns a list of value,status (single pair) for requested pmid/inst. ''' if self.debug: self.log("**** fetch_callback: %d, %d, %d ****" % (cluster, item, inst)) if not self.modules_by_cluster.has_key(cluster): # Handle our static metrics. if cluster == 0: if item == 0: return [len(self.modules_by_cluster), 1] elif item == 1: return [self.debug, 1] if self.debug: self.log("Invalid cluster %d" % cluster) return [c_api.PM_ERR_PMID, 0] module = self.modules_by_cluster[cluster] return module.fetch(item, inst) if __name__ == '__main__': #os.environ["PCP_PYTHON_DEBUG"] = "ALL" #os.environ["PCP_PYTHON_DEBUG"] = "APPL0|LIBPMDA" StapJsonPMDA('stap_json', 132).run()