]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
Import metrics package from a different repository (#4988)
authorMark Donnelly <mark@painless-security.com>
Mon, 15 May 2023 16:35:41 +0000 (12:35 -0400)
committerGitHub <noreply@github.com>
Mon, 15 May 2023 16:35:41 +0000 (12:35 -0400)
scripts/stackdriver/radsniff_metrics.py [new file with mode: 0644]
scripts/stackdriver/radsniff_metrics.yml [new file with mode: 0644]
scripts/stackdriver/requirements.txt [new file with mode: 0644]

diff --git a/scripts/stackdriver/radsniff_metrics.py b/scripts/stackdriver/radsniff_metrics.py
new file mode 100644 (file)
index 0000000..4bab51f
--- /dev/null
@@ -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 (file)
index 0000000..8ab437a
--- /dev/null
@@ -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 (file)
index 0000000..2b7f306
--- /dev/null
@@ -0,0 +1,6 @@
+grpcio
+opencensus-ext-stackdriver==0.8.0
+opencensus-ext-prometheus
+opencensus==0.10.0
+python-ldap
+pyyaml