|
| 1 | +""" |
| 2 | +Basic exporter class for prometheus metrics. |
| 3 | +Runs a ThreadingHTTPServer with a PrometheusRequest handler. |
| 4 | +The PrometheusRequest handler processes requests from Promotheus, by default returns server.export(). |
| 5 | +The server.export() method goes through all defined metrics and returns them as a string. |
| 6 | +If a dict is passed to the export method, it will be used to filter by that label. |
| 7 | +""" |
| 8 | + |
| 9 | +from http.server import ThreadingHTTPServer |
| 10 | +from pathlib import Path |
| 11 | + |
| 12 | +from zenlib.logging import loggify |
| 13 | + |
| 14 | +from .labels import Labels |
| 15 | +from .metric import Metric |
| 16 | +from .prometheus_request import PrometheusRequest |
| 17 | + |
| 18 | +DEFAULT_IP = '127.0.0.1' |
| 19 | +DEFAULT_PORT = 9809 |
| 20 | + |
| 21 | + |
| 22 | +@loggify |
| 23 | +class Exporter(ThreadingHTTPServer): |
| 24 | + """ |
| 25 | + Basic prometheus metric exporter class. |
| 26 | + Extends the ThreadingHTTPServer class. |
| 27 | + Forces use of the PrometheusRequest RequestHandlerClass. |
| 28 | + Reads a config.toml file to read the server port and ip. |
| 29 | + If 'ip' and 'port' are passed as kwargs, they will override the config file. |
| 30 | + """ |
| 31 | + def __init__(self, config_file='config.toml', labels=Labels(), *args, **kwargs): |
| 32 | + self.labels = Labels(dict_items=labels, logger=self.logger, _log_init=False) |
| 33 | + self.metrics = [] |
| 34 | + self.config_file = Path(config_file) |
| 35 | + self.read_config() |
| 36 | + |
| 37 | + kwargs['RequestHandlerClass'] = PrometheusRequest |
| 38 | + ip = self.config['listen_ip'] if 'ip' not in kwargs else kwargs.pop('ip', DEFAULT_IP) |
| 39 | + port = self.config['listen_port'] if 'port' not in kwargs else kwargs.pop('port', DEFAULT_PORT) |
| 40 | + kwargs['server_address'] = (ip, port) |
| 41 | + |
| 42 | + super().__init__(*args, **kwargs) |
| 43 | + |
| 44 | + def __setattr__(self, name, value): |
| 45 | + if name == 'labels' and not isinstance(value, Labels): |
| 46 | + raise ValueError("Labels must be a dict.") |
| 47 | + super().__setattr__(name, value) |
| 48 | + |
| 49 | + def read_config(self): |
| 50 | + """ Reads the config file defined in self.config_file """ |
| 51 | + from tomllib import load |
| 52 | + with open(self.config_file, 'rb') as config: |
| 53 | + self.config = load(config) |
| 54 | + |
| 55 | + self.logger.info("Read config file: %s", self.config_file) |
| 56 | + self.labels.update(self.config.get('labels', {})) |
| 57 | + |
| 58 | + self.add_metrics() |
| 59 | + |
| 60 | + def add_metrics(self): |
| 61 | + """ Adds all defined metrics to the exporter. """ |
| 62 | + for name, values in self.config.get('metrics', {}).items(): |
| 63 | + kwargs = {'metric_type': values.pop('type'), 'labels': self.labels.copy(), |
| 64 | + 'logger': self.logger, '_log_init': False} |
| 65 | + |
| 66 | + # Add labels specified under the metric to ones in the exporter |
| 67 | + if labels := values.pop('labels', None): |
| 68 | + kwargs['labels'].update(labels) |
| 69 | + |
| 70 | + self.logger.info("Adding metric: %s", name) |
| 71 | + self.metrics.append(Metric(name=name, **kwargs, **values)) |
| 72 | + |
| 73 | + def get_metrics(self): |
| 74 | + """ |
| 75 | + Gets all defined metrics. |
| 76 | + """ |
| 77 | + return [metric for metric in Metric.metrics] |
| 78 | + |
| 79 | + def _filter_metrics(self, metrics, label_filter): |
| 80 | + """ |
| 81 | + Filters a list of metrics by a label_filter. |
| 82 | + """ |
| 83 | + for label_name, label_value in label_filter.items(): |
| 84 | + if label_name not in self.labels.global_labels: |
| 85 | + raise ValueError("label_filter contains unknown label: %s", label_name) |
| 86 | + if label_value not in self.labels.global_labels[label_name]: |
| 87 | + raise ValueError("[%s] label_filter contains unknown label value: %s" % (label_name, label_value)) |
| 88 | + return [metric for metric in metrics if metric.labels[label_name] == label_value] |
| 89 | + |
| 90 | + def export(self, label_filter=None): |
| 91 | + """ |
| 92 | + Go through ALL DEFINED metrics, turn them into a metric string for prometheus. |
| 93 | + If a label_filter is passed, only return metrics that match the label_filter. |
| 94 | + """ |
| 95 | + metrics = self.get_metrics() |
| 96 | + if label_filter: |
| 97 | + metrics = self._filter_metrics(metrics, label_filter) |
| 98 | + return "\n".join([str(metric) for metric in metrics]) |
| 99 | + |
| 100 | + def handle_error(self, request, client_address): |
| 101 | + """ Handle errors in the request handler. """ |
| 102 | + from sys import exc_info |
| 103 | + from traceback import format_exception |
| 104 | + self.logger.warning("[%s:%d] Error in request: %s" % (*client_address, exc_info()[1])) |
| 105 | + exc = format_exception(*exc_info()) |
| 106 | + self.logger.debug(''.join(exc).replace(r'\n', '\n')) |
0 commit comments