From: Mark Donnelly Date: Mon, 15 May 2023 16:35:41 +0000 (-0400) Subject: Import metrics package from a different repository (#4988) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5278d0622077b2f3c25cefb1b142807b646f48c4;p=thirdparty%2Ffreeradius-server.git Import metrics package from a different repository (#4988) --- diff --git a/scripts/stackdriver/radsniff_metrics.py b/scripts/stackdriver/radsniff_metrics.py new file mode 100644 index 00000000000..4bab51f7a0c --- /dev/null +++ b/scripts/stackdriver/radsniff_metrics.py @@ -0,0 +1,219 @@ +#!python3 +import atexit +import csv +import logging +import math +import os +import pty +import re +import subprocess + +import opencensus.stats.stats + +import opencensus.ext.stackdriver.stats_exporter +import yaml + +from opencensus.ext import prometheus +from opencensus.ext.prometheus import stats_exporter +from opencensus.stats.stats import stats +from opencensus.stats import view, measure, aggregation +# from opencensus.tags import tag_key, tag_map, tag_value + +# Make up for broken code in the Prometheus exporter +import opencensus.stats.aggregation_data +opencensus.stats.aggregation_data.SumAggregationDataFloat = opencensus.stats.aggregation_data.SumAggregationData + +SUPPORTED_EXPORTERS = ['Prometheus', 'Stackdriver'] + + +class BaseStatistic: + def __init__(self, config=None, tag_keys=None): + logging.info(f"Entering BaseStatistic.__init__({config}, {tag_keys}") + if config is None: + config = {} + if tag_keys is None: + tag_keys = [] + + self.name = config.get('name', '') + self.description = config.get('description', 'unspecified') + self.unit = config.get('unit', '1') + + logging.debug(f"Creating a measurement for {self.name}, {self.description}, {self.unit}") + self.measure = measure.MeasureFloat( + name=self.name, + description=self.description, + unit=self.unit + ) + logging.debug(f"Creating a new view for {self.name}") + self.view = view.View( + name=self.name, + description=self.description, + columns=tag_keys, + aggregation=aggregation.LastValueAggregation(), + measure=self.measure + ) + stats.view_manager.register_view(self.view) + + def display_name(self): + return self.name + + def collect(self, measurement_map=None, value=0.0): + logging.debug(f" Entering collect({measurement_map}, {value}) for statistic {self.name}") + if measurement_map is None: + logging.error(f"INTERNAL ERROR: Failing to collect statistic {self.display_name()} " + f"because no measurement map was supplied.") + raise ValueError("The measurement map must be supplied to LdapStatistic.collect") + if math.isnan(value): + raise ValueError("The value to measure must be a number greater than zero.") + if value > 0: + print(f"{self.measure.name}: {value}") + measurement_map.measure_float_put(self.measure, value) + + +class RadiusStatistic(BaseStatistic): + def __init__(self, config=None, tag_keys=None): + sub_config = { + 'name': config.get('name', ''), + 'description': config.get('description', config.get("label")), + 'unit': config.get('unit', self.guess_unit(config.get('label'))) + } + super().__init__(sub_config, tag_keys) + + @staticmethod + def guess_unit(label): + if re.search(r'/s$', label) or re.search(r'PPS$', label): + return 's' + if re.search(r'\(ms\)$', label): + return 'ms' + print(f"Could not determine a unit for {label}, using 'By'.") + return 'By' + + +def create_exporter(config=None): + if config is None: + raise ValueError("Cannot create an exporter with no configuration!") + + name = config.get('name') + if name not in SUPPORTED_EXPORTERS: + logging.error( + f"Requested exporter named {name}, which is not supported. Choose from:{', '.join(SUPPORTED_EXPORTERS)}" + ) + raise ValueError( + f"Requested exporter named {name}, which is not supported. Choose from:{', '.join(SUPPORTED_EXPORTERS)}" + ) + + exporter = None + + options = config.get('options', {}) + if "Prometheus" == name: + if 'options' not in config: + logging.error("The Prometheus exporter requires options configuration.") + raise ValueError("The Prometheus exporter requires options configuration.") + final_options = {'namespace': 'radius', 'port': 8001, 'address': '0.0.0.0'} + final_options.update(options) + exporter = stats_exporter.new_stats_exporter( + prometheus.stats_exporter.Options(**final_options) + ) + + elif "Stackdriver" == name: + exporter = opencensus.ext.stackdriver.stats_exporter.new_stats_exporter(interval=5) + print(f"Exporting stats to this project {exporter.options.project_id}") + + return exporter + + +class Configuration: + def __init__(self, configuration_filename='radsniff_metrics.yml'): + if configuration_filename is None: + raise ValueError("Configuration filename must be supplied.") + self._configuration_filename = configuration_filename + self._config = {} + self.read_configuration() + + def exporters(self): + return {'name': 'Prometheus', 'options': {}} + + def read_configuration(self): + with open(self._configuration_filename, 'r') as file: + ret_val = yaml.safe_load(file) + self._config = ret_val + + +def main(): + config = Configuration() + exporter = create_exporter(config.exporters()) + stats.view_manager.register_exporter(exporter) + master_fd, slave_fd = pty.openpty() + command_line = ['./radsniff', '-W', '5', '-E'] + process = subprocess.Popen( + command_line, + stdout=slave_fd, + bufsize=1, + text=True + ) + atexit.register(exit_handler, process) + radsniff_output = os.fdopen(master_fd) + reader = csv.DictReader(radsniff_output) + + statistics = None + for row in reader: + logging.info("Read a new set of data from radsniff.") + measurement_map = stats.stats_recorder.new_measurement_map() + if statistics is None: + statistics = {} + updates = { + 'access-': 'access/', + 'accounting-': 'accounting/', + 'status-': 'status/', + 'disconnect-': 'disconnect/', + 'coa-': 'coa/', + 'request ': 'request/', + 'accept ': 'accept/', + 'reject ': 'reject/', + 'response ': 'response/', + 'challenge ': 'challenge/', + 'server ': 'server/', + 'client ': 'client/', + 'nak ': 'nak/', + 'ack ': 'ack/', + r'rtx \(([1-5].?)\)': r'rtx/\1', + ' ': '_', + '\+': 'plus', + '/s$': '', + } + for label in row.keys(): + name = label.lower() + for match, replacement in updates.items(): + name = re.sub(match, replacement, name) + statistics[label] = RadiusStatistic( + { + 'label': label.lower(), + 'name': name, + 'description': label, + }, + [] + ) + for label, measurement in row.items(): + measurement_value = float(measurement) + if math.isnan(measurement_value): + continue + + statistic = statistics.get(label, None) + if statistic is None: + logging.error("Tried to collect a value for a name ({label}) that has not been seen before. Skipping.") + continue + + statistic.collect( + measurement_map=measurement_map, + value=measurement_value + ) + measurement_map.record() + + +def exit_handler(process): + result = process.terminate() + + +if __name__ == '__main__': + logging.getLogger().setLevel('WARN') + main() diff --git a/scripts/stackdriver/radsniff_metrics.yml b/scripts/stackdriver/radsniff_metrics.yml new file mode 100644 index 00000000000..8ab437a2708 --- /dev/null +++ b/scripts/stackdriver/radsniff_metrics.yml @@ -0,0 +1,6 @@ +exporters: + - name: Prometheus + options: + namespace: freeradius + port: 8000 + address: 0.0.0.0 diff --git a/scripts/stackdriver/requirements.txt b/scripts/stackdriver/requirements.txt new file mode 100644 index 00000000000..2b7f306af4d --- /dev/null +++ b/scripts/stackdriver/requirements.txt @@ -0,0 +1,6 @@ +grpcio +opencensus-ext-stackdriver==0.8.0 +opencensus-ext-prometheus +opencensus==0.10.0 +python-ldap +pyyaml