From 12cb6812439efb563902c390fb99d703a0495144 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 21 Aug 2018 19:04:29 +0200 Subject: [PATCH] Drop protobuf support for OpenMetrics (text only) and optimize for text support --- .../checks/openmetrics/__init__.py | 3 - .../checks/openmetrics/mixins.py | 450 +++------- .../tests/fixtures/prometheus/metrics.txt | 6 +- datadog_checks_base/tests/test_openmetrics.py | 821 +++++------------- .../tests/test_openmetrics_base_check.py | 3 +- 5 files changed, 348 insertions(+), 935 deletions(-) diff --git a/datadog_checks_base/datadog_checks/checks/openmetrics/__init__.py b/datadog_checks_base/datadog_checks/checks/openmetrics/__init__.py index 4b3e4d6e915406..e53575769246ff 100644 --- a/datadog_checks_base/datadog_checks/checks/openmetrics/__init__.py +++ b/datadog_checks_base/datadog_checks/checks/openmetrics/__init__.py @@ -3,11 +3,8 @@ # Licensed under a 3-clause BSD style license (see LICENSE) -from .mixins import PrometheusFormat, UnknownFormatError from .base_check import OpenMetricsBaseCheck __all__ = [ - 'PrometheusFormat', - 'UnknownFormatError', 'OpenMetricsBaseCheck', ] diff --git a/datadog_checks_base/datadog_checks/checks/openmetrics/mixins.py b/datadog_checks_base/datadog_checks/checks/openmetrics/mixins.py index 6c3567bc9839f0..0033644f6fd309 100644 --- a/datadog_checks_base/datadog_checks/checks/openmetrics/mixins.py +++ b/datadog_checks_base/datadog_checks/checks/openmetrics/mixins.py @@ -7,9 +7,6 @@ import requests from urllib3 import disable_warnings from urllib3.exceptions import InsecureRequestWarning -from collections import defaultdict -from google.protobuf.internal.decoder import _DecodeVarint32 # pylint: disable=E0611,E0401 -from ...utils.prometheus import metrics_pb2 from math import isnan, isinf from prometheus_client.parser import text_fd_to_metric_families @@ -22,33 +19,23 @@ if PY3: long = int -class PrometheusFormat: - """ - Used to specify if you want to use the protobuf format or the text format when - querying prometheus metrics - """ - PROTOBUF = "PROTOBUF" - TEXT = "TEXT" - -class UnknownFormatError(TypeError): - pass - class OpenMetricsScraperMixin(object): # pylint: disable=E1101 # This class is not supposed to be used by itself, it provides scraping behavior but # need to be within a check in the end - UNWANTED_LABELS = ['le', 'quantile'] # are specifics keys for prometheus itself REQUESTS_CHUNK_SIZE = 1024 * 10 # use 10kb as chunk size when using the Stream feature in requests.get + # indexes in the sample tuple of core.Metric + SAMPLE_NAME = 0 + SAMPLE_LABELS = 1 + SAMPLE_VALUE = 2 + + METRIC_TYPES = ['counter', 'gauge', 'summary', 'histogram'] def __init__(self, *args, **kwargs): # Initialize AgentCheck's base class super(OpenMetricsScraperMixin, self).__init__(*args, **kwargs) - # message.type is the index in this array - # see: https://github.com/prometheus/client_model/blob/master/ruby/lib/prometheus/client/model/metrics.pb.rb - self.METRIC_TYPES = ['counter', 'gauge', 'summary', 'untyped', 'histogram'] - def create_scraper_configuration(self, instance=None): # We can choose to create a default mixin configuration for an empty instance @@ -96,10 +83,6 @@ def create_scraper_configuration(self, instance=None): config['metrics_mapper'] = metrics_mapper - # `rate_metrics` contains the metrics that should be sent as rates - config['rate_metrics'] = self._extract_rate_metrics(default_instance.get('type_overrides', {})) - config['rate_metrics'].extend(self._extract_rate_metrics(instance.get('type_overrides', {}))) - # `_metrics_wildcards` holds the potential wildcards to match for metrics config['_metrics_wildcards'] = None @@ -163,6 +146,8 @@ def create_scraper_configuration(self, instance=None): # when sending the gauges. config['labels_mapper'] = default_instance.get('labels_mapper', {}) config['labels_mapper'].update(instance.get('labels_mapper', {})) + # Rename bucket "le" label to "upper_bound" + config['labels_mapper']['le'] = 'upper_bound' # `exclude_labels` is an array of labels names to exclude. Those labels # will just not be added as tags when submitting the metric. @@ -221,73 +206,20 @@ def create_scraper_configuration(self, instance=None): def parse_metric_family(self, response, scraper_config): """ Parse the MetricFamily from a valid requests.Response object to provide a MetricFamily object (see [0]) - The text format uses iter_lines() generator. - - The protobuf format directly parse the response.content property searching for Prometheus messages of type - MetricFamily [0] delimited by a varint32 [1] when the content-type is a `application/vnd.google.protobuf`. - - [0] https://github.com/prometheus/client_model/blob/086fe7ca28bde6cec2acd5223423c1475a362858/metrics.proto#L76-%20%20L81 - [1] https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/AbstractMessageLite#writeDelimitedTo(java.io.OutputStream) - :param response: requests.Response - :return: metrics_pb2.MetricFamily() + :return: core.Metric """ - if 'application/vnd.google.protobuf' in response.headers['Content-Type']: - n = 0 - buf = response.content - while n < len(buf): - msg_len, new_pos = _DecodeVarint32(buf, n) - n = new_pos - msg_buf = buf[n:n+msg_len] - n += msg_len - - message = metrics_pb2.MetricFamily() - message.ParseFromString(msg_buf) - message.name = self._remove_metric_prefix(message.name, scraper_config) - - # Lookup type overrides: - if scraper_config['type_overrides'] and message.name in scraper_config['type_overrides']: - new_type = scraper_config['type_overrides'][message.name] - if new_type in self.METRIC_TYPES: - message.type = self.METRIC_TYPES.index(new_type) - else: - self.log.debug("type override %s for %s is not a valid type name" % (new_type, message.name)) - yield message - - elif 'text/plain' in response.headers['Content-Type']: - input_gen = response.iter_lines(chunk_size=self.REQUESTS_CHUNK_SIZE) - if scraper_config['_text_filter_blacklist']: - input_gen = self._text_filter_input(input_gen, scraper_config) - - messages = defaultdict(list) # map with the name of the element (before the labels) - # and the list of occurrences with labels and values - - obj_map = {} # map of the types of each metrics - obj_help = {} # help for the metrics - for metric in text_fd_to_metric_families(input_gen): - metric.name = self._remove_metric_prefix(metric.name, scraper_config) - metric_name = '{}_bucket'.format(metric.name) if metric.type == 'histogram' else metric.name - metric_type = scraper_config['type_overrides'].get(metric_name, metric.type) - if metric_type == 'untyped' or metric_type not in self.METRIC_TYPES: - continue + input_gen = response.iter_lines(chunk_size=self.REQUESTS_CHUNK_SIZE) + if scraper_config['_text_filter_blacklist']: + input_gen = self._text_filter_input(input_gen, scraper_config) - for sample in metric.samples: - if (sample[0].endswith('_sum') or sample[0].endswith('_count')) and \ - metric_type in ['histogram', 'summary']: - messages[sample[0]].append({'labels': sample[1], 'value': sample[2]}) - else: - messages[metric_name].append({'labels': sample[1], 'value': sample[2]}) - - obj_map[metric.name] = metric_type - obj_help[metric.name] = metric.documentation - - for _m in obj_map: - if _m in messages or (obj_map[_m] == 'histogram' and ('{}_bucket'.format(_m) in messages)): - yield self._extract_metric_from_map(_m, messages, obj_map, obj_help) - else: - raise UnknownFormatError('Unsupported content-type provided: {}'.format( - response.headers['Content-Type'])) + for metric in text_fd_to_metric_families(input_gen): + metric.type = scraper_config['type_overrides'].get(metric.name, metric.type) + if metric.type not in self.METRIC_TYPES: + continue + metric.name = self._remove_metric_prefix(metric.name, scraper_config) + yield metric def _text_filter_input(self, input_gen, scraper_config): """ @@ -309,116 +241,6 @@ def _remove_metric_prefix(self, metric, scraper_config): prometheus_metrics_prefix = scraper_config['prometheus_metrics_prefix'] return metric[len(prometheus_metrics_prefix):] if metric.startswith(prometheus_metrics_prefix) else metric - @staticmethod - def get_metric_value_by_labels(messages, _metric, _m, metric_suffix): - """ - :param messages: dictionary as metric_name: {labels: {}, value: 10} - :param _metric: dictionary as {labels: {le: '0.001', 'custom': 'value'}} - :param _m: str as metric name - :param metric_suffix: str must be in (count or sum) - :return: value of the metric_name matched by the labels - """ - metric_name = '{}_{}'.format(_m, metric_suffix) - expected_labels = set( - (k, v) for k, v in iteritems(_metric["labels"]) - if k not in OpenMetricsScraperMixin.UNWANTED_LABELS - ) - - for elt in messages[metric_name]: - current_labels = set( - (k, v) for k, v in iteritems(elt["labels"]) - if k not in OpenMetricsScraperMixin.UNWANTED_LABELS - ) - - # As we have two hashable objects we can compare them without any side effects - if current_labels == expected_labels: - return float(elt['value']) - - raise AttributeError("cannot find expected labels for metric {} with suffix {}".format(metric_name, metric_suffix)) - - def _extract_rate_metrics(self, type_overrides): - rate_metrics = [] - for metric, value in iteritems(type_overrides): - if value == 'rate': - rate_metrics.append(metric) - type_overrides[metric] = 'gauge' - return rate_metrics - - def _extract_metric_from_map(self, _m, messages, obj_map, obj_help): - """ - Extracts MetricFamily objects from the maps generated by parsing the - strings in _extract_metrics_from_string - """ - _obj = metrics_pb2.MetricFamily() - _obj.name = _m - _obj.type = self.METRIC_TYPES.index(obj_map[_m]) - if _m in obj_help: - _obj.help = obj_help[_m] - # trick for histograms - _newlbl = _m - if obj_map[_m] == 'histogram': - _newlbl = '{}_bucket'.format(_m) - # Loop through the array of metrics ({labels, value}) built earlier - for _metric in messages[_newlbl]: - # in the case of quantiles and buckets, they need to be grouped by labels - if obj_map[_m] in ['summary', 'histogram'] and len(_obj.metric) > 0: - _label_exists = False - _metric_minus = {k: v for k, v in list(iteritems(_metric['labels'])) if k not in ['quantile', 'le']} - _metric_idx = 0 - for mls in _obj.metric: - _tmp_lbl = {idx.name: idx.value for idx in mls.label} - if _metric_minus == _tmp_lbl: - _label_exists = True - break - _metric_idx = _metric_idx + 1 - if _label_exists: - _g = _obj.metric[_metric_idx] - else: - _g = _obj.metric.add() - else: - _g = _obj.metric.add() - if obj_map[_m] == 'counter': - _g.counter.value = float(_metric['value']) - elif obj_map[_m] == 'gauge': - _g.gauge.value = float(_metric['value']) - elif obj_map[_m] == 'summary': - if '{}_count'.format(_m) in messages: - _g.summary.sample_count = long(self.get_metric_value_by_labels(messages, _metric, _m, 'count')) - if '{}_sum'.format(_m) in messages: - _g.summary.sample_sum = self.get_metric_value_by_labels(messages, _metric, _m, 'sum') - # TODO: see what can be done with the untyped metrics - elif obj_map[_m] == 'histogram': - if '{}_count'.format(_m) in messages: - _g.histogram.sample_count = long(self.get_metric_value_by_labels(messages, _metric, _m, 'count')) - if '{}_sum'.format(_m) in messages: - _g.histogram.sample_sum = self.get_metric_value_by_labels(messages, _metric, _m, 'sum') - # last_metric = len(_obj.metric) - 1 - # if last_metric >= 0: - for lbl in _metric['labels']: - # In the string format, the quantiles are in the labels - if lbl == 'quantile': - # _q = _obj.metric[last_metric].summary.quantile.add() - _q = _g.summary.quantile.add() - _q.quantile = float(_metric['labels'][lbl]) - _q.value = float(_metric['value']) - # The upper_bounds are stored as "le" labels on string format - elif obj_map[_m] == 'histogram' and lbl == 'le': - # _q = _obj.metric[last_metric].histogram.bucket.add() - _q = _g.histogram.bucket.add() - _q.upper_bound = float(_metric['labels'][lbl]) - _q.cumulative_count = long(float(_metric['value'])) - else: - # labels deduplication - is_in_labels = False - for _existing_lbl in _g.label: - if lbl == _existing_lbl.name: - is_in_labels = True - if not is_in_labels: - _l = _g.label.add() - _l.name = lbl - _l.value = _metric['labels'][lbl] - return _obj - def scrape_metrics(self, scraper_config): """ Poll the data from prometheus and return the metrics as a generator. @@ -458,45 +280,44 @@ def process(self, scraper_config, metric_transformers=None): for metric in self.scrape_metrics(scraper_config): self.process_metric(metric, scraper_config, metric_transformers=metric_transformers) - def _store_labels(self, message, scraper_config): + def _store_labels(self, metric, scraper_config): + scraper_config['label_joins'] # If targeted metric, store labels - if message.name in scraper_config['label_joins']: - matching_label = scraper_config['label_joins'][message.name]['label_to_match'] - for metric in message.metric: + if metric.name in scraper_config['label_joins']: + matching_label = scraper_config['label_joins'][metric.name]['label_to_match'] + for sample in metric.samples: labels_list = [] matching_value = None - for label in metric.label: - if label.name == matching_label: - matching_value = label.value - elif label.name in scraper_config['label_joins'][message.name]['labels_to_get']: - labels_list.append((label.name, label.value)) + for label_name, label_value in iteritems(sample[self.SAMPLE_LABELS]): + if label_name == matching_label: + matching_value = label_value + elif label_name in scraper_config['label_joins'][metric.name]['labels_to_get']: + labels_list.append((label_name, label_value)) try: scraper_config['_label_mapping'][matching_label][matching_value] = labels_list except KeyError: if matching_value is not None: scraper_config['_label_mapping'][matching_label] = {matching_value: labels_list} - def _join_labels(self, message, scraper_config): + def _join_labels(self, metric, scraper_config): # Filter metric to see if we can enrich with joined labels if scraper_config['label_joins']: - for metric in message.metric: - for label in metric.label: - if label.name in scraper_config['_watched_labels']: - # Set this label value as active - if label.name not in scraper_config['_active_label_mapping']: - scraper_config['_active_label_mapping'][label.name] = {} - scraper_config['_active_label_mapping'][label.name][label.value] = True - # If mapping found add corresponding labels - try: - for label_tuple in scraper_config['_label_mapping'][label.name][label.value]: - extra_label = metric.label.add() - extra_label.name, extra_label.value = label_tuple - except KeyError: - pass - - def process_metric(self, message, scraper_config, metric_transformers=None): + for sample in metric.samples: + for label_name in scraper_config['_watched_labels'].intersection(set(sample[self.SAMPLE_LABELS].keys())): + # Set this label value as active + if label_name not in scraper_config['_active_label_mapping']: + scraper_config['_active_label_mapping'][label_name] = {} + scraper_config['_active_label_mapping'][label_name][sample[self.SAMPLE_LABELS][label_name]] = True + # If mapping found add corresponding labels + try: + for label_tuple in scraper_config['_label_mapping'][label_name][sample[self.SAMPLE_LABELS][label_name]]: + sample[self.SAMPLE_LABELS][label_tuple[0]] = label_tuple[1] + except KeyError: + pass + + def process_metric(self, metric, scraper_config, metric_transformers=None): """ - Handle a prometheus metric message according to the following flow: + Handle a prometheus metric according to the following flow: - search scraper_config['metrics_mapper'] for a prometheus.metric <--> datadog.metric mapping - call check method with the same name as the metric - log some info if none of the above worked @@ -504,31 +325,30 @@ def process_metric(self, message, scraper_config, metric_transformers=None): `metric_transformers` is a dict of : """ # If targeted metric, store labels - self._store_labels(message, scraper_config) + self._store_labels(metric, scraper_config) - if message.name in scraper_config['ignore_metrics']: + if metric.name in scraper_config['ignore_metrics']: return # Ignore the metric # Filter metric to see if we can enrich with joined labels - self._join_labels(message, scraper_config) + self._join_labels(metric, scraper_config) if scraper_config['_dry_run']: return try: - metric = scraper_config['metrics_mapper'][message.name] - self._submit(metric, message, scraper_config) + self._submit(scraper_config['metrics_mapper'][metric.name], metric, scraper_config) except KeyError: if metric_transformers is not None: - if message.name in metric_transformers: + if metric.name in metric_transformers: try: # Get the transformer function for this specific metric - transformer = metric_transformers[message.name] - transformer(message, scraper_config) + transformer = metric_transformers[metric.name] + transformer(metric, scraper_config) except Exception as err: - self.log.warning("Error handling metric: {} - error: {}".format(message.name, err)) + self.log.warning("Error handling metric: {} - error: {}".format(metric.name, err)) else: - self.log.debug("Unable to handle metric: {0} - error: No handler function named '{0}' defined".format(message.name)) + self.log.debug("Unable to handle metric: {0} - error: No handler function named '{0}' defined".format(metric.name)) else: # build the wildcard list if first pass if scraper_config['_metrics_wildcards'] is None: @@ -536,14 +356,11 @@ def process_metric(self, message, scraper_config, metric_transformers=None): # try matching wildcard (generic check) for wildcard in scraper_config['_metrics_wildcards']: - if fnmatchcase(message.name, wildcard): - self._submit(message.name, message, scraper_config) + if fnmatchcase(metric.name, wildcard): + self._submit(metric.name, metric, scraper_config) - def poll(self, scraper_config, pFormat=PrometheusFormat.PROTOBUF, headers=None): + def poll(self, scraper_config, headers=None): """ - Polls the metrics from the prometheus metrics endpoint provided. - Defaults to the protobuf format, but can use the formats specified by - the PrometheusFormat class. Custom headers can be added to the default headers. Returns a valid requests.Response, raise requests.HTTPError if the status code of the requests.Response @@ -552,7 +369,6 @@ def poll(self, scraper_config, pFormat=PrometheusFormat.PROTOBUF, headers=None): The caller needs to close the requests.Response :param endpoint: string url endpoint - :param pFormat: the preferred format defined in PrometheusFormat :param headers: extra headers :return: requests.Response """ @@ -563,7 +379,7 @@ def poll(self, scraper_config, pFormat=PrometheusFormat.PROTOBUF, headers=None): service_check_name = '{}{}'.format(scraper_config['namespace'], '.prometheus.health') service_check_tags = scraper_config['custom_tags'] + ['endpoint:' + endpoint] try: - response = self.send_request(endpoint, scraper_config, pFormat, headers) + response = self.send_request(endpoint, scraper_config, headers) except requests.exceptions.SSLError: self.log.error("Invalid SSL settings for requesting {} endpoint".format(endpoint)) raise @@ -594,16 +410,12 @@ def poll(self, scraper_config, pFormat=PrometheusFormat.PROTOBUF, headers=None): ) raise - def send_request(self, endpoint, scraper_config, pFormat=PrometheusFormat.PROTOBUF, headers=None): + def send_request(self, endpoint, scraper_config, headers=None): # Determine the headers if headers is None: headers = {} if 'accept-encoding' not in headers: headers['accept-encoding'] = 'gzip' - if pFormat == PrometheusFormat.PROTOBUF: - headers['accept'] = 'application/vnd.google.protobuf; ' \ - 'proto=io.prometheus.client.MetricFamily; ' \ - 'encoding=delimited' headers.update(scraper_config['extra_headers']) # Determine the SSL verification settings @@ -625,12 +437,12 @@ def send_request(self, endpoint, scraper_config, pFormat=PrometheusFormat.PROTOB password = scraper_config['password'] auth = (username, password) if username is not None and password is not None else None - return requests.get(endpoint, headers=headers, stream=False, timeout=scraper_config['prometheus_timeout'], + return requests.get(endpoint, headers=headers, stream=True, timeout=scraper_config['prometheus_timeout'], cert=cert, verify=verify, auth=auth) - def _submit(self, metric_name, message, scraper_config, hostname=None): + def _submit(self, metric_name, metric, scraper_config, hostname=None): """ - For each metric in the message, report it as a gauge with all labels as tags + For each sample in the metric, report it as a gauge with all labels as tags except if a labels dict is passed, in which case keys are label names we'll extract and corresponding values are tag names we'll use (eg: {'node': 'node'}). @@ -641,47 +453,36 @@ def _submit(self, metric_name, message, scraper_config, hostname=None): `custom_tags` is an array of 'tag:value' that will be added to the metric when sending the gauge to Datadog. """ - if message.type < len(self.METRIC_TYPES): - for metric in message.metric: - custom_hostname = self._get_hostname(hostname, metric, scraper_config) - if message.type == 0: - val = getattr(metric, self.METRIC_TYPES[message.type]).value - if self._is_value_valid(val): - # Determine the tags to send - tags = self._metric_tags(metric_name, val, metric, scraper_config, hostname=custom_hostname) - metric_name_with_namespace = '{}.{}'.format(scraper_config['namespace'], metric_name) - if scraper_config['send_monotonic_counter']: - self.monotonic_count(metric_name_with_namespace, val, tags=tags, hostname=custom_hostname) - else: - self.gauge(metric_name_with_namespace, val, tags=tags, hostname=custom_hostname) - else: - self.log.debug("Metric value is not supported for metric {}.".format(metric_name)) - elif message.type == 4: - self._submit_gauges_from_histogram(metric_name, metric, scraper_config, hostname=custom_hostname) - elif message.type == 2: - self._submit_gauges_from_summary(metric_name, metric, scraper_config, hostname=custom_hostname) + if metric.type in ["gauge", "counter", "rate"]: + metric_name_with_namespace = '{}.{}'.format(scraper_config['namespace'], metric_name) + for sample in metric.samples: + val = sample[self.SAMPLE_VALUE] + if not self._is_value_valid(val): + self.log.debug("Metric value is not supported for metric {}".format(sample[self.SAMPLE_NAME])) + continue + custom_hostname = self._get_hostname(hostname, sample, scraper_config) + # Determine the tags to send + tags = self._metric_tags(metric_name, val, sample, scraper_config, hostname=custom_hostname) + if metric.type == "counter" and scraper_config['send_monotonic_counter']: + self.monotonic_count(metric_name_with_namespace, val, tags=tags, hostname=custom_hostname) + elif metric.type == "rate": + self.rate(metric_name_with_namespace, val, tags=tags, hostname=custom_hostname) else: - val = getattr(metric, self.METRIC_TYPES[message.type]).value - if self._is_value_valid(val): - tags = self._metric_tags(metric_name, val, metric, scraper_config, hostname=custom_hostname) - metric_name_with_namespace = '{}.{}'.format(scraper_config['namespace'], metric_name) - if message.name in scraper_config['rate_metrics']: - self.rate(metric_name_with_namespace, val, tags=tags, hostname=custom_hostname) - else: - self.gauge(metric_name_with_namespace, val, tags=tags, hostname=custom_hostname) - else: - self.log.debug("Metric value is not supported for metric {}.".format(metric_name)) + self.gauge(metric_name_with_namespace, val, tags=tags, hostname=custom_hostname) + elif metric.type == "histogram": + self._submit_gauges_from_histogram(metric_name, metric, scraper_config) + elif metric.type == "summary": + self._submit_gauges_from_summary(metric_name, metric, scraper_config) else: - self.log.error("Metric type {} unsupported for metric {}.".format(message.type, message.name)) + self.log.error("Metric type {} unsupported for metric {}.".format(metric.type, metric_name)) - def _get_hostname(self, hostname, metric, scraper_config): + def _get_hostname(self, hostname, sample, scraper_config): """ If hostname is None, look at label_to_hostname setting """ - if hostname is None and scraper_config['label_to_hostname'] is not None: - for label in metric.label: - if label.name == scraper_config['label_to_hostname']: - return label.value + if (hostname is None and scraper_config['label_to_hostname'] is not None and + scraper_config['label_to_hostname'] in sample[self.SAMPLE_LABELS]): + return sample[self.SAMPLE_LABELS][scraper_config['label_to_hostname']] return hostname @@ -689,65 +490,44 @@ def _submit_gauges_from_summary(self, metric_name, metric, scraper_config, hostn """ Extracts metrics from a prometheus summary metric and sends them as gauges """ - # summaries do not have a value attribute - val = getattr(metric, self.METRIC_TYPES[2]).sample_count - if self._is_value_valid(val): - tags = self._metric_tags(metric_name, val, metric, scraper_config, hostname) - self.gauge('{}.{}.count'.format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=hostname) - else: - self.log.debug("Metric value is not supported for metric {}.count.".format(metric_name)) - val = getattr(metric, self.METRIC_TYPES[2]).sample_sum - if self._is_value_valid(val): - tags = self._metric_tags(metric_name, val, metric, scraper_config, hostname=hostname) - self.gauge('{}.{}.sum'.format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=hostname) - else: - self.log.debug("Metric value is not supported for metric {}.sum.".format(metric_name)) - for quantile in getattr(metric, self.METRIC_TYPES[2]).quantile: - val = quantile.value - limit = quantile.quantile - if self._is_value_valid(val): - tags = self._metric_tags(metric_name, val, metric, scraper_config, hostname=hostname) + ['quantile:{}'.format(limit)] - self.gauge('{}.{}.quantile'.format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=hostname) + for sample in metric.samples: + val = sample[self.SAMPLE_VALUE] + if not self._is_value_valid(val): + self.log.debug("Metric value is not supported for metric {}".format(sample[self.SAMPLE_NAME])) + continue + custom_hostname = self._get_hostname(hostname, sample, scraper_config) + tags = self._metric_tags(metric_name, val, sample, scraper_config, hostname=custom_hostname) + if sample[self.SAMPLE_NAME].endswith("_sum"): + self.gauge("{}.{}.sum".format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=custom_hostname) + if sample[self.SAMPLE_NAME].endswith("_count"): + self.gauge("{}.{}.count".format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=custom_hostname) else: - self.log.debug("Metric value is not supported for metric {}.quantile.".format(metric_name)) + self.gauge("{}.{}.quantile".format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=custom_hostname) def _submit_gauges_from_histogram(self, metric_name, metric, scraper_config, hostname=None): """ Extracts metrics from a prometheus histogram and sends them as gauges """ - # histograms do not have a value attribute - val = getattr(metric, self.METRIC_TYPES[4]).sample_count - if self._is_value_valid(val): - tags = self._metric_tags(metric_name, val, metric, scraper_config, hostname=hostname) - self.gauge('{}.{}.count'.format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=hostname) - else: - self.log.debug("Metric value is not supported for metric {}.count.".format(metric_name)) - val = getattr(metric, self.METRIC_TYPES[4]).sample_sum - if self._is_value_valid(val): - tags = self._metric_tags(metric_name, val, metric, scraper_config, hostname=hostname) - self.gauge('{}.{}.sum'.format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=hostname) - else: - self.log.debug("Metric value is not supported for metric {}.sum.".format(metric_name)) - if scraper_config['send_histograms_buckets']: - for bucket in getattr(metric, self.METRIC_TYPES[4]).bucket: - val = bucket.cumulative_count - limit = bucket.upper_bound - if self._is_value_valid(val): - tags = self._metric_tags(metric_name, val, metric, scraper_config, hostname=hostname) + ['upper_bound:{}'.format(limit)] - self.gauge('{}.{}.count'.format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=hostname) - else: - self.log.debug("Metric value is not supported for metric {}.count.".format(metric_name)) - - def _metric_tags(self, metric_name, val, metric, scraper_config, hostname=None): + for sample in metric.samples: + val = sample[self.SAMPLE_VALUE] + if not self._is_value_valid(val): + self.log.debug("Metric value is not supported for metric {}".format(sample[self.SAMPLE_NAME])) + continue + custom_hostname = self._get_hostname(hostname, sample, scraper_config) + tags = self._metric_tags(metric_name, val, sample, scraper_config, hostname) + if sample[self.SAMPLE_NAME].endswith("_sum"): + self.gauge("{}.{}.sum".format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=custom_hostname) + elif sample[self.SAMPLE_NAME].endswith("_count") or (sample[self.SAMPLE_NAME].endswith("_bucket") and scraper_config['send_histograms_buckets']): + self.gauge("{}.{}.count".format(scraper_config['namespace'], metric_name), val, tags=tags, hostname=custom_hostname) + + def _metric_tags(self, metric_name, val, sample, scraper_config, hostname=None): custom_tags = scraper_config['custom_tags'] _tags = list(custom_tags) - for label in metric.label: - if label.name not in scraper_config['exclude_labels']: - tag_name = label.name - if label.name in scraper_config['labels_mapper']: - tag_name = scraper_config['labels_mapper'][label.name] - _tags.append('{}:{}'.format(tag_name, label.value)) - return self._finalize_tags_to_submit(_tags, metric_name, val, metric, custom_tags=custom_tags, hostname=hostname) + for label_name, label_value in iteritems(sample[self.SAMPLE_LABELS]): + if label_name not in scraper_config['exclude_labels']: + tag_name = scraper_config['labels_mapper'].get(label_name, label_name) + _tags.append('{}:{}'.format(tag_name, label_value)) + return self._finalize_tags_to_submit(_tags, metric_name, val, sample, custom_tags=custom_tags, hostname=hostname) def _is_value_valid(self, val): return not (isnan(val) or isinf(val)) diff --git a/datadog_checks_base/tests/fixtures/prometheus/metrics.txt b/datadog_checks_base/tests/fixtures/prometheus/metrics.txt index f9e10f8a6a5ff7..a7aed043251c91 100644 --- a/datadog_checks_base/tests/fixtures/prometheus/metrics.txt +++ b/datadog_checks_base/tests/fixtures/prometheus/metrics.txt @@ -121,9 +121,6 @@ process_resident_memory_bytes 3.4304e+07 # HELP process_start_time_seconds Start time of the process since unix epoch in seconds. # TYPE process_start_time_seconds gauge process_start_time_seconds 1.49340832351e+09 -# HELP process_virtual_memory_bytes Virtual memory size in bytes. -# TYPE process_virtual_memory_bytes gauge -process_virtual_memory_bytes 5.492736e+07 # HELP skydns_skydns_dns_cachemiss_count_total Counter of DNS requests that result in a cache miss. # TYPE skydns_skydns_dns_cachemiss_count_total counter skydns_skydns_dns_cachemiss_count_total{cache="response"} 1.359194e+06 @@ -213,3 +210,6 @@ skydns_skydns_dns_response_size_bytes_bucket{system="reverse",le="2048"} 67648 skydns_skydns_dns_response_size_bytes_bucket{system="reverse",le="+Inf"} 67648 skydns_skydns_dns_response_size_bytes_sum{system="reverse"} 6.075182e+06 skydns_skydns_dns_response_size_bytes_count{system="reverse"} 67648 +# HELP process_virtual_memory_bytes Virtual memory size in bytes. +# TYPE process_virtual_memory_bytes gauge +process_virtual_memory_bytes 5.492736e+07 diff --git a/datadog_checks_base/tests/test_openmetrics.py b/datadog_checks_base/tests/test_openmetrics.py index 72fe9c9e948afe..778f84bae580ca 100644 --- a/datadog_checks_base/tests/test_openmetrics.py +++ b/datadog_checks_base/tests/test_openmetrics.py @@ -4,17 +4,17 @@ import logging import os +import math import mock import pytest import requests -from six import iteritems, iterkeys -from six.moves import range -from datadog_checks.checks.openmetrics import OpenMetricsBaseCheck, UnknownFormatError -from datadog_checks.utils.prometheus import parse_metric_family, metrics_pb2 +from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, SummaryMetricFamily, HistogramMetricFamily +from datadog_checks.checks.openmetrics import OpenMetricsBaseCheck -protobuf_content_type = 'application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited' + +text_content_type = 'text/plain; version=0.0.4' class MockResponse: @@ -69,26 +69,12 @@ def p_check(): @pytest.fixture def ref_gauge(): - ref_gauge = metrics_pb2.MetricFamily() - ref_gauge.name = 'process_virtual_memory_bytes' - ref_gauge.help = 'Virtual memory size in bytes.' - ref_gauge.type = 1 # GAUGE - _m = ref_gauge.metric.add() - _m.gauge.value = 39211008.0 + ref_gauge = GaugeMetricFamily('process_virtual_memory_bytes', 'Virtual memory size in bytes.') + ref_gauge.add_metric([], 54927360.0) return ref_gauge -@pytest.fixture -def bin_data(): - f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'prometheus', 'protobuf.bin') - with open(f_name, 'rb') as f: - bin_data = f.read() - assert len(bin_data) == 51855 - - return bin_data - - @pytest.fixture def text_data(): # Loading test text data @@ -109,159 +95,15 @@ def mock_get(): with mock.patch( 'requests.get', return_value=mock.MagicMock( - status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={'Content-Type': "text/plain"} + status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={'Content-Type': text_content_type} ), ): yield text_data -def test_parse_metric_family(): - f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'prometheus', 'protobuf.bin') - with open(f_name, 'rb') as f: - data = f.read() - assert len(data) == 51855 - messages = list(parse_metric_family(data)) - assert len(messages) == 61 - assert messages[-1].name == 'process_virtual_memory_bytes' - - -def test_parse_metric_family_protobuf(bin_data, mocked_prometheus_check, mocked_prometheus_scraper_config): - response = MockResponse(bin_data, protobuf_content_type) - check = mocked_prometheus_check - - messages = list(check.parse_metric_family(response, mocked_prometheus_scraper_config)) - - assert len(messages) == 61 - assert messages[-1].name == 'process_virtual_memory_bytes' - - # check type overriding is working - # original type: - assert messages[1].name == 'go_goroutines' - assert messages[1].type == 1 # gauge - - # override the type: - mocked_prometheus_scraper_config['type_overrides'] = {"go_goroutines": "summary"} - - response = MockResponse(bin_data, protobuf_content_type) - - messages = list(check.parse_metric_family(response, mocked_prometheus_scraper_config)) - - assert len(messages) == 61 - assert messages[1].name == 'go_goroutines' - assert messages[1].type == 2 # summary - - -def test_parse_metric_family_text(text_data, mocked_prometheus_check, mocked_prometheus_scraper_config): - """ Test the high level method for loading metrics from text format """ - check = mocked_prometheus_check - response = MockResponse(text_data, 'text/plain; version=0.0.4') - - messages = list(check.parse_metric_family(response, mocked_prometheus_scraper_config)) - # total metrics are 41 but one is typeless and we expect it not to be - # parsed... - assert len(messages) == 40 - # ...unless the check ovverrides the type manually - mocked_prometheus_scraper_config['type_overrides'] = {"go_goroutines": "gauge"} - response = MockResponse(text_data, 'text/plain; version=0.0.4') - messages = list(check.parse_metric_family(response, mocked_prometheus_scraper_config)) - assert len(messages) == 41 - # Tests correct parsing of counters - _counter = metrics_pb2.MetricFamily() - _counter.name = 'skydns_skydns_dns_cachemiss_count_total' - _counter.help = 'Counter of DNS requests that result in a cache miss.' - _counter.type = 0 # COUNTER - _c = _counter.metric.add() - _c.counter.value = 1359194.0 - _lc = _c.label.add() - _lc.name = 'cache' - _lc.value = 'response' - assert _counter in messages - # Tests correct parsing of gauges - _gauge = metrics_pb2.MetricFamily() - _gauge.name = 'go_memstats_heap_alloc_bytes' - _gauge.help = 'Number of heap bytes allocated and still in use.' - _gauge.type = 1 # GAUGE - _gauge.metric.add().gauge.value = 6396288.0 - assert _gauge in messages - # Tests correct parsing of summaries - _summary = metrics_pb2.MetricFamily() - _summary.name = 'http_response_size_bytes' - _summary.help = 'The HTTP response sizes in bytes.' - _summary.type = 2 # SUMMARY - _sm = _summary.metric.add() - _lsm = _sm.label.add() - _lsm.name = 'handler' - _lsm.value = 'prometheus' - _sm.summary.sample_count = 25 - _sm.summary.sample_sum = 147728.0 - _sq1 = _sm.summary.quantile.add() - _sq1.quantile = 0.5 - _sq1.value = 21470.0 - _sq2 = _sm.summary.quantile.add() - _sq2.quantile = 0.9 - _sq2.value = 21470.0 - _sq3 = _sm.summary.quantile.add() - _sq3.quantile = 0.99 - _sq3.value = 21470.0 - assert _summary in messages - # Tests correct parsing of histograms - _histo = metrics_pb2.MetricFamily() - _histo.name = 'skydns_skydns_dns_response_size_bytes' - _histo.help = 'Size of the returns response in bytes.' - _histo.type = 4 # HISTOGRAM - _sample_data = [ - { - 'ct': 1359194, - 'sum': 199427281.0, - 'lbl': {'system': 'auth'}, - 'buckets': { - 0.0: 0, - 512.0: 1359194, - 1024.0: 1359194, - 1500.0: 1359194, - 2048.0: 1359194, - float('+Inf'): 1359194, - }, - }, - { - 'ct': 520924, - 'sum': 41527128.0, - 'lbl': {'system': 'recursive'}, - 'buckets': {0.0: 0, 512.0: 520924, 1024.0: 520924, 1500.0: 520924, 2048.0: 520924, float('+Inf'): 520924}, - }, - { - 'ct': 67648, - 'sum': 6075182.0, - 'lbl': {'system': 'reverse'}, - 'buckets': {0.0: 0, 512.0: 67648, 1024.0: 67648, 1500.0: 67648, 2048.0: 67648, float('+Inf'): 67648}, - }, - ] - for _data in _sample_data: - _h = _histo.metric.add() - _h.histogram.sample_count = _data['ct'] - _h.histogram.sample_sum = _data['sum'] - for k, v in list(iteritems(_data['lbl'])): - _lh = _h.label.add() - _lh.name = k - _lh.value = v - for _b in sorted(iterkeys(_data['buckets'])): - _subh = _h.histogram.bucket.add() - _subh.upper_bound = _b - _subh.cumulative_count = _data['buckets'][_b] - assert _histo in messages - - -def test_parse_metric_family_unsupported(bin_data, mocked_prometheus_check, mocked_prometheus_scraper_config): +def test_process(text_data, mocked_prometheus_check, mocked_prometheus_scraper_config, ref_gauge): check = mocked_prometheus_check - - with pytest.raises(UnknownFormatError): - response = MockResponse(bin_data, 'application/json') - list(check.parse_metric_family(response, mocked_prometheus_scraper_config)) - - -def test_process(bin_data, mocked_prometheus_check, mocked_prometheus_scraper_config, ref_gauge): - check = mocked_prometheus_check - check.poll = mock.MagicMock(return_value=MockResponse(bin_data, protobuf_content_type)) + check.poll = mock.MagicMock(return_value=MockResponse(text_data, text_content_type)) check.process_metric = mock.MagicMock() check.process(mocked_prometheus_scraper_config) check.poll.assert_called_with(mocked_prometheus_scraper_config) @@ -274,17 +116,13 @@ def test_process_metric_gauge(aggregator, mocked_prometheus_check, mocked_promet mocked_prometheus_scraper_config['_dry_run'] = False check.process_metric(ref_gauge, mocked_prometheus_scraper_config) - aggregator.assert_metric('prometheus.process.vm.bytes', 39211008.0, tags=[], count=1) + aggregator.assert_metric('prometheus.process.vm.bytes', 54927360.0, tags=[], count=1) def test_process_metric_filtered(aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config): """ Metric absent from the metrics_mapper """ - filtered_gauge = metrics_pb2.MetricFamily() - filtered_gauge.name = "process_start_time_seconds" - filtered_gauge.help = "Start time of the process since unix epoch in seconds." - filtered_gauge.type = 1 # GAUGE - _m = filtered_gauge.metric.add() - _m.gauge.value = 39211008.0 + filtered_gauge = GaugeMetricFamily('process_start_time_seconds', 'Start time of the process since unix epoch in seconds.') + filtered_gauge.add_metric([], 123456789.0) mocked_prometheus_scraper_config['_dry_run'] = False check = mocked_prometheus_check @@ -295,27 +133,13 @@ def test_process_metric_filtered(aggregator, mocked_prometheus_check, mocked_pro aggregator.assert_all_metrics_covered() -def test_poll_protobuf(bin_data, mocked_prometheus_check, mocked_prometheus_scraper_config): - """ Tests poll using the protobuf format """ - check = mocked_prometheus_check - mock_response = mock.MagicMock( - status_code=200, - content=bin_data, - headers={'Content-Type': protobuf_content_type}) - with mock.patch('requests.get', return_value=mock_response, __name__="get"): - response = check.poll(mocked_prometheus_scraper_config) - messages = list(check.parse_metric_family(response, mocked_prometheus_scraper_config)) - assert len(messages) == 61 - assert messages[-1].name == 'process_virtual_memory_bytes' - - def test_poll_text_plain(mocked_prometheus_check, mocked_prometheus_scraper_config, text_data): """Tests poll using the text format""" check = mocked_prometheus_check mock_response = mock.MagicMock( status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), - headers={'Content-Type': "text/plain"}) + headers={'Content-Type': text_content_type}) with mock.patch('requests.get', return_value=mock_response, __name__="get"): response = check.poll(mocked_prometheus_scraper_config) messages = list(check.parse_metric_family(response, mocked_prometheus_scraper_config)) @@ -324,32 +148,28 @@ def test_poll_text_plain(mocked_prometheus_check, mocked_prometheus_scraper_conf assert messages[-1].name == 'skydns_skydns_dns_response_size_bytes' -def test_submit_gauge_with_labels(aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config, ref_gauge): +def test_submit_gauge_with_labels(aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config): """ submitting metrics that contain labels should result in tags on the gauge call """ - _l1 = ref_gauge.metric[0].label.add() - _l1.name, _l1.value = ('my_1st_label', 'my_1st_label_value') - _l2 = ref_gauge.metric[0].label.add() - _l2.name, _l2.value = ('my_2nd_label', 'my_2nd_label_value') + ref_gauge = GaugeMetricFamily('process_virtual_memory_bytes', 'Virtual memory size in bytes.', labels=['my_1st_label', 'my_2nd_label']) + ref_gauge.add_metric(['my_1st_label_value', 'my_2nd_label_value'], 54927360.0) check = mocked_prometheus_check metric_name = mocked_prometheus_scraper_config['metrics_mapper'][ref_gauge.name] check._submit(metric_name, ref_gauge, mocked_prometheus_scraper_config) aggregator.assert_metric( 'prometheus.process.vm.bytes', - 39211008.0, + 54927360.0, tags=['my_1st_label:my_1st_label_value', 'my_2nd_label:my_2nd_label_value'], count=1, ) def test_submit_gauge_with_labels_and_hostname_override( - aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config, ref_gauge + aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config ): """ submitting metrics that contain labels should result in tags on the gauge call """ - _l1 = ref_gauge.metric[0].label.add() - _l1.name, _l1.value = ('my_1st_label', 'my_1st_label_value') - _l2 = ref_gauge.metric[0].label.add() - _l2.name, _l2.value = ('node', 'foo') + ref_gauge = GaugeMetricFamily('process_virtual_memory_bytes', 'Virtual memory size in bytes.', labels=['my_1st_label', 'node']) + ref_gauge.add_metric(['my_1st_label_value', 'foo'], 54927360.0) check = mocked_prometheus_check mocked_prometheus_scraper_config['label_to_hostname'] = 'node' @@ -357,7 +177,7 @@ def test_submit_gauge_with_labels_and_hostname_override( check._submit(metric_name, ref_gauge, mocked_prometheus_scraper_config) aggregator.assert_metric( 'prometheus.process.vm.bytes', - 39211008.0, + 54927360.0, tags=['my_1st_label:my_1st_label_value', 'node:foo'], hostname="foo", count=1, @@ -365,13 +185,11 @@ def test_submit_gauge_with_labels_and_hostname_override( def test_submit_gauge_with_labels_and_hostname_already_overridden( - aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config, ref_gauge + aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config ): """ submitting metrics that contain labels should result in tags on the gauge call """ - _l1 = ref_gauge.metric[0].label.add() - _l1.name, _l1.value = ('my_1st_label', 'my_1st_label_value') - _l2 = ref_gauge.metric[0].label.add() - _l2.name, _l2.value = ('node', 'foo') + ref_gauge = GaugeMetricFamily('process_virtual_memory_bytes', 'Virtual memory size in bytes.', labels=['my_1st_label', 'node']) + ref_gauge.add_metric(['my_1st_label_value', 'foo'], 54927360.0) check = mocked_prometheus_check mocked_prometheus_scraper_config['label_to_hostname'] = 'node' @@ -379,7 +197,7 @@ def test_submit_gauge_with_labels_and_hostname_already_overridden( check._submit(metric_name, ref_gauge, mocked_prometheus_scraper_config, hostname='bar') aggregator.assert_metric( 'prometheus.process.vm.bytes', - 39211008.0, + 54927360.0, tags=['my_1st_label:my_1st_label_value', 'node:foo'], hostname="bar", count=1, @@ -389,10 +207,8 @@ def test_submit_gauge_with_labels_and_hostname_already_overridden( def test_labels_not_added_as_tag_once_for_each_metric( aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config, ref_gauge ): - _l1 = ref_gauge.metric[0].label.add() - _l1.name, _l1.value = ('my_1st_label', 'my_1st_label_value') - _l2 = ref_gauge.metric[0].label.add() - _l2.name, _l2.value = ('my_2nd_label', 'my_2nd_label_value') + ref_gauge = GaugeMetricFamily('process_virtual_memory_bytes', 'Virtual memory size in bytes.', labels=['my_1st_label', 'my_2nd_label']) + ref_gauge.add_metric(['my_1st_label_value', 'my_2nd_label_value'], 54927360.0) check = mocked_prometheus_check mocked_prometheus_scraper_config['custom_tags'] = ['test'] @@ -404,7 +220,7 @@ def test_labels_not_added_as_tag_once_for_each_metric( aggregator.assert_metric( 'prometheus.process.vm.bytes', - 39211008.0, + 54927360.0, tags=['test', 'my_1st_label:my_1st_label_value', 'my_2nd_label:my_2nd_label_value'], count=2, ) @@ -419,20 +235,18 @@ def test_submit_gauge_with_custom_tags( mocked_prometheus_scraper_config['custom_tags'] = tags metric = mocked_prometheus_scraper_config['metrics_mapper'][ref_gauge.name] check._submit(metric, ref_gauge, mocked_prometheus_scraper_config) - aggregator.assert_metric('prometheus.process.vm.bytes', 39211008.0, tags=tags, count=1) + aggregator.assert_metric('prometheus.process.vm.bytes', 54927360.0, tags=tags, count=1) def test_submit_gauge_with_labels_mapper( - aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config, ref_gauge + aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config ): """ Submitting metrics that contain labels mappers should result in tags on the gauge call with transformed tag names """ - _l1 = ref_gauge.metric[0].label.add() - _l1.name, _l1.value = ('my_1st_label', 'my_1st_label_value') - _l2 = ref_gauge.metric[0].label.add() - _l2.name, _l2.value = ('my_2nd_label', 'my_2nd_label_value') + ref_gauge = GaugeMetricFamily('process_virtual_memory_bytes', 'Virtual memory size in bytes.', labels=['my_1st_label', 'my_2nd_label']) + ref_gauge.add_metric(['my_1st_label_value', 'my_2nd_label_value'], 54927360.0) check = mocked_prometheus_check mocked_prometheus_scraper_config['labels_mapper'] = { @@ -445,23 +259,21 @@ def test_submit_gauge_with_labels_mapper( check._submit(metric, ref_gauge, mocked_prometheus_scraper_config) aggregator.assert_metric( 'prometheus.process.vm.bytes', - 39211008.0, + 54927360.0, tags=['env:dev', 'app:my_pretty_app', 'transformed_1st:my_1st_label_value', 'my_2nd_label:my_2nd_label_value'], count=1, ) def test_submit_gauge_with_exclude_labels( - aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config, ref_gauge + aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config ): """ Submitting metrics when filtering with exclude_labels should end up with a filtered tags list """ - _l1 = ref_gauge.metric[0].label.add() - _l1.name, _l1.value = ('my_1st_label', 'my_1st_label_value') - _l2 = ref_gauge.metric[0].label.add() - _l2.name, _l2.value = ('my_2nd_label', 'my_2nd_label_value') + ref_gauge = GaugeMetricFamily('process_virtual_memory_bytes', 'Virtual memory size in bytes.', labels=['my_1st_label', 'my_2nd_label']) + ref_gauge.add_metric(['my_1st_label_value', 'my_2nd_label_value'], 54927360.0) check = mocked_prometheus_check mocked_prometheus_scraper_config['labels_mapper'] = { @@ -479,79 +291,52 @@ def test_submit_gauge_with_exclude_labels( check._submit(metric, ref_gauge, mocked_prometheus_scraper_config) aggregator.assert_metric( 'prometheus.process.vm.bytes', - 39211008.0, + 54927360.0, tags=['env:dev', 'app:my_pretty_app', 'transformed_1st:my_1st_label_value'], count=1, ) def test_submit_counter(aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config): - _counter = metrics_pb2.MetricFamily() - _counter.name = 'my_counter' - _counter.help = 'Random counter' - _counter.type = 0 # COUNTER - _met = _counter.metric.add() - _met.counter.value = 42 + _counter = CounterMetricFamily('my_counter', 'Random counter') + _counter.add_metric([], 42) check = mocked_prometheus_check check._submit('custom.counter', _counter, mocked_prometheus_scraper_config) aggregator.assert_metric('prometheus.custom.counter', 42, tags=[], count=1) aggregator.assert_all_metrics_covered() -def test_submits_summary(aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config): - _sum = metrics_pb2.MetricFamily() - _sum.name = 'my_summary' - _sum.help = 'Random summary' - _sum.type = 2 # SUMMARY - _met = _sum.metric.add() - _met.summary.sample_count = 42 - _met.summary.sample_sum = 3.14 - _q1 = _met.summary.quantile.add() - _q1.quantile = 10.0 - _q1.value = 3 - _q2 = _met.summary.quantile.add() - _q2.quantile = 4.0 - _q2.value = 5 +def test_submit_summary(aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config): + _sum = SummaryMetricFamily('my_summary', 'Random summary') + _sum.add_metric([], 5.0, 120512.0) + _sum.add_sample("my_summary", {"quantile": "0.5"}, 24547.0) + _sum.add_sample("my_summary", {"quantile": "0.9"}, 25763.0) + _sum.add_sample("my_summary", {"quantile": "0.99"}, 25763.0) check = mocked_prometheus_check check._submit('custom.summary', _sum, mocked_prometheus_scraper_config) - - aggregator.assert_metric('prometheus.custom.summary.count', 42, tags=[], count=1) - aggregator.assert_metric('prometheus.custom.summary.sum', 3.14, tags=[], count=1) - aggregator.assert_metric('prometheus.custom.summary.quantile', 3, tags=['quantile:10.0'], count=1) - aggregator.assert_metric('prometheus.custom.summary.quantile', 5, tags=['quantile:4.0'], count=1) + aggregator.assert_metric('prometheus.custom.summary.count', 5.0, tags=[], count=1) + aggregator.assert_metric('prometheus.custom.summary.sum', 120512.0, tags=[], count=1) + aggregator.assert_metric('prometheus.custom.summary.quantile', 24547.0, tags=['quantile:0.5'], count=1) + aggregator.assert_metric('prometheus.custom.summary.quantile', 25763.0, tags=['quantile:0.9'], count=1) + aggregator.assert_metric('prometheus.custom.summary.quantile', 25763.0, tags=['quantile:0.99'], count=1) aggregator.assert_all_metrics_covered() def test_submit_histogram(aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config): - _histo = metrics_pb2.MetricFamily() - _histo.name = 'my_histogram' - _histo.help = 'Random histogram' - _histo.type = 4 # HISTOGRAM - _met = _histo.metric.add() - _met.histogram.sample_count = 42 - _met.histogram.sample_sum = 3.14 - _b1 = _met.histogram.bucket.add() - _b1.upper_bound = 12.7 - _b1.cumulative_count = 33 - _b2 = _met.histogram.bucket.add() - _b2.upper_bound = 18.2 - _b2.cumulative_count = 666 + _histo = HistogramMetricFamily('my_histogram', 'my_histogram') + _histo.add_metric([], buckets=[('0', 1), ('+Inf', 2)], sum_value=3) check = mocked_prometheus_check check._submit('custom.histogram', _histo, mocked_prometheus_scraper_config) - aggregator.assert_metric('prometheus.custom.histogram.count', 42, tags=[], count=1) - aggregator.assert_metric('prometheus.custom.histogram.sum', 3.14, tags=[], count=1) - aggregator.assert_metric('prometheus.custom.histogram.count', 33, tags=['upper_bound:12.7'], count=1) - aggregator.assert_metric('prometheus.custom.histogram.count', 666, tags=['upper_bound:18.2'], count=1) + aggregator.assert_metric('prometheus.custom.histogram.sum', 3, tags=[], count=1) + aggregator.assert_metric('prometheus.custom.histogram.count', 2, tags=[]) + aggregator.assert_metric('prometheus.custom.histogram.count', 1, tags=['upper_bound:0'], count=1) + aggregator.assert_metric('prometheus.custom.histogram.count', 2, tags=['upper_bound:+Inf'], count=1) aggregator.assert_all_metrics_covered() def test_submit_rate(aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config): - _rate = metrics_pb2.MetricFamily() - _rate.name = 'my_rate' - _rate.help = 'Random rate' - _rate.type = 1 # GAUGE - _met = _rate.metric.add() - _met.gauge.value = 42 + _rate = GaugeMetricFamily('my_rate', 'Random rate') + _rate.add_metric([], 42) check = mocked_prometheus_check mocked_prometheus_scraper_config['rate_metrics'] = ["my_rate"] check._submit('custom.rate', _rate, mocked_prometheus_scraper_config) @@ -570,25 +355,12 @@ def test_filter_sample_on_gauge(p_check, mocked_prometheus_scraper_config): 'kube_deployment_status_replicas{deployment="heapster-v1.4.3"} 1\n' 'kube_deployment_status_replicas{deployment="kube-dns"} 2\n') - expected_metric = metrics_pb2.MetricFamily() - expected_metric.help = "The number of replicas per deployment." - expected_metric.name = "kube_deployment_status_replicas" - expected_metric.type = 1 - - gauge1 = expected_metric.metric.add() - gauge1.gauge.value = 1 - label1 = gauge1.label.add() - label1.name = "deployment" - label1.value = "event-exporter-v0.1.7" - - gauge2 = expected_metric.metric.add() - gauge2.gauge.value = 1 - label2 = gauge2.label.add() - label2.name = "deployment" - label2.value = "heapster-v1.4.3" + expected_metric = GaugeMetricFamily('kube_deployment_status_replicas', 'The number of replicas per deployment.', labels=['deployment']) + expected_metric.add_metric(['event-exporter-v0.1.7'], 1) + expected_metric.add_metric(['heapster-v1.4.3'], 1) # Iter on the generator to get all metrics - response = MockResponse(text_data, 'text/plain; version=0.0.4') + response = MockResponse(text_data, text_content_type) check = p_check mocked_prometheus_scraper_config['_text_filter_blacklist'] = ["deployment=\"kube-dns\""] metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] @@ -614,14 +386,11 @@ def test_parse_one_gauge(p_check, mocked_prometheus_scraper_config): "etcd_server_has_leader 1\n" ) - expected_etcd_metric = metrics_pb2.MetricFamily() - expected_etcd_metric.help = "Whether or not a leader exists. 1 is existence, 0 is not." - expected_etcd_metric.name = "etcd_server_has_leader" - expected_etcd_metric.type = 1 - expected_etcd_metric.metric.add().gauge.value = 1 + expected_etcd_metric = GaugeMetricFamily('etcd_server_has_leader', 'Whether or not a leader exists. 1 is existence, 0 is not.') + expected_etcd_metric.add_metric([], 1) # Iter on the generator to get all metrics - response = MockResponse(text_data, 'text/plain; version=0.0.4') + response = MockResponse(text_data, text_content_type) check = p_check metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] @@ -629,16 +398,6 @@ def test_parse_one_gauge(p_check, mocked_prometheus_scraper_config): current_metric = metrics[0] assert expected_etcd_metric == current_metric - # Remove the old metric and add a new one with a different value - expected_etcd_metric.metric.pop() - expected_etcd_metric.metric.add().gauge.value = 0 - assert expected_etcd_metric != current_metric - - # Re-add the expected value but as different type: it should works - expected_etcd_metric.metric.pop() - expected_etcd_metric.metric.add().gauge.value = 1.0 - assert expected_etcd_metric == current_metric - def test_parse_one_counter(p_check, mocked_prometheus_scraper_config): """ @@ -657,14 +416,11 @@ def test_parse_one_counter(p_check, mocked_prometheus_scraper_config): "go_memstats_mallocs_total 18713\n" ) - expected_etcd_metric = metrics_pb2.MetricFamily() - expected_etcd_metric.help = "Total number of mallocs." - expected_etcd_metric.name = "go_memstats_mallocs_total" - expected_etcd_metric.type = 0 - expected_etcd_metric.metric.add().counter.value = 18713 + expected_etcd_metric = CounterMetricFamily('go_memstats_mallocs_total', 'Total number of mallocs.') + expected_etcd_metric.add_metric([], 18713) # Iter on the generator to get all metrics - response = MockResponse(text_data, 'text/plain; version=0.0.4') + response = MockResponse(text_data, text_content_type) check = p_check metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] @@ -672,11 +428,6 @@ def test_parse_one_counter(p_check, mocked_prometheus_scraper_config): current_metric = metrics[0] assert expected_etcd_metric == current_metric - # Remove the old metric and add a new one with a different value - expected_etcd_metric.metric.pop() - expected_etcd_metric.metric.add().counter.value = 18714 - assert expected_etcd_metric != current_metric - def test_parse_one_histograms_with_label(p_check, mocked_prometheus_scraper_config): text_data = ( @@ -701,50 +452,39 @@ def test_parse_one_histograms_with_label(p_check, mocked_prometheus_scraper_conf 'etcd_disk_wal_fsync_duration_seconds_count{app="vault"} 4\n' ) - expected_etcd_vault_metric = metrics_pb2.MetricFamily() - expected_etcd_vault_metric.help = "The latency distributions of fsync called by wal." - expected_etcd_vault_metric.name = "etcd_disk_wal_fsync_duration_seconds" - expected_etcd_vault_metric.type = 4 - - histogram_metric = expected_etcd_vault_metric.metric.add() - - # Label for app vault - summary_label = histogram_metric.label.add() - summary_label.name, summary_label.value = "app", "vault" - - for upper_bound, cumulative_count in [ - (0.001, 2), - (0.002, 2), - (0.004, 2), - (0.008, 2), - (0.016, 4), - (0.032, 4), - (0.064, 4), - (0.128, 4), - (0.256, 4), - (0.512, 4), - (1.024, 4), - (2.048, 4), - (4.096, 4), - (8.192, 4), - (float('inf'), 4), - ]: - bucket = histogram_metric.histogram.bucket.add() - bucket.upper_bound = upper_bound - bucket.cumulative_count = cumulative_count - - # Root histogram sample - histogram_metric.histogram.sample_count = 4 - histogram_metric.histogram.sample_sum = 0.026131671 + expected_etcd_vault_metric = HistogramMetricFamily('etcd_disk_wal_fsync_duration_seconds', + 'The latency distributions of fsync called by wal.', + labels=['app'] + ) + expected_etcd_vault_metric.add_metric(['vault'], buckets=[ + ('0.001', 2.0), + ('0.002', 2.0), + ('0.004', 2.0), + ('0.008', 2.0), + ('0.016', 4.0), + ('0.032', 4.0), + ('0.064', 4.0), + ('0.128', 4.0), + ('0.256', 4.0), + ('0.512', 4.0), + ('1.024', 4.0), + ('2.048', 4.0), + ('4.096', 4.0), + ('8.192', 4.0), + ('+Inf', 4.0) + ], sum_value=0.026131671) # Iter on the generator to get all metrics - response = MockResponse(text_data, 'text/plain; version=0.0.4') + response = MockResponse(text_data, text_content_type) check = p_check metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] assert 1 == len(metrics) current_metric = metrics[0] - assert expected_etcd_vault_metric == current_metric + assert expected_etcd_vault_metric.documentation == current_metric.documentation + assert expected_etcd_vault_metric.name == current_metric.name + assert expected_etcd_vault_metric.type == current_metric.type + assert sorted(expected_etcd_vault_metric.samples, key=lambda i: i[0]) == sorted(current_metric.samples, key=lambda i: i[0]) def test_parse_one_histogram(p_check, mocked_prometheus_scraper_config): @@ -841,46 +581,35 @@ def test_parse_one_histogram(p_check, mocked_prometheus_scraper_config): 'etcd_disk_wal_fsync_duration_seconds_count 4\n' ) - expected_etcd_metric = metrics_pb2.MetricFamily() - expected_etcd_metric.help = "The latency distributions of fsync called by wal." - expected_etcd_metric.name = "etcd_disk_wal_fsync_duration_seconds" - expected_etcd_metric.type = 4 - - histogram_metric = expected_etcd_metric.metric.add() - for upper_bound, cumulative_count in [ - (0.001, 2), - (0.002, 2), - (0.004, 2), - (0.008, 2), - (0.016, 4), - (0.032, 4), - (0.064, 4), - (0.128, 4), - (0.256, 4), - (0.512, 4), - (1.024, 4), - (2.048, 4), - (4.096, 4), - (8.192, 4), - (float('inf'), 4), - ]: - bucket = histogram_metric.histogram.bucket.add() - bucket.upper_bound = upper_bound - bucket.cumulative_count = cumulative_count - - # Root histogram sample - histogram_metric.histogram.sample_count = 4 - histogram_metric.histogram.sample_sum = 0.026131671 + expected_etcd_metric = HistogramMetricFamily('etcd_disk_wal_fsync_duration_seconds', 'The latency distributions of fsync called by wal.') + expected_etcd_metric.add_metric([], buckets=[ + ('0.001', 2.0), + ('0.002', 2.0), + ('0.004', 2.0), + ('0.008', 2.0), + ('0.016', 4.0), + ('0.032', 4.0), + ('0.064', 4.0), + ('0.128', 4.0), + ('0.256', 4.0), + ('0.512', 4.0), + ('1.024', 4.0), + ('2.048', 4.0), + ('4.096', 4.0), + ('8.192', 4.0), + ('+Inf', 4.0) + ], sum_value=0.026131671) # Iter on the generator to get all metrics - response = MockResponse(text_data, 'text/plain; version=0.0.4') + response = MockResponse(text_data, text_content_type) check = p_check metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] - assert 1 == len(metrics) current_metric = metrics[0] - assert expected_etcd_metric == current_metric - + assert expected_etcd_metric.documentation == current_metric.documentation + assert expected_etcd_metric.name == current_metric.name + assert expected_etcd_metric.type == current_metric.type + assert sorted(expected_etcd_metric.samples, key=lambda i: i[0]) == sorted(current_metric.samples, key=lambda i: i[0]) def test_parse_two_histograms_with_label(p_check, mocked_prometheus_scraper_config): text_data = ( @@ -922,81 +651,48 @@ def test_parse_two_histograms_with_label(p_check, mocked_prometheus_scraper_conf 'etcd_disk_wal_fsync_duration_seconds_count{kind="fs",app="kubernetes"} 751\n' ) - expected_etcd_metric = metrics_pb2.MetricFamily() - expected_etcd_metric.help = "The latency distributions of fsync called by wal." - expected_etcd_metric.name = "etcd_disk_wal_fsync_duration_seconds" - expected_etcd_metric.type = 4 - - # Vault - histogram_metric = expected_etcd_metric.metric.add() - - # Label for app vault - summary_label = histogram_metric.label.add() - summary_label.name, summary_label.value = "kind", "fs" - summary_label = histogram_metric.label.add() - summary_label.name, summary_label.value = "app", "vault" - - for upper_bound, cumulative_count in [ - (0.001, 2), - (0.002, 2), - (0.004, 2), - (0.008, 2), - (0.016, 4), - (0.032, 4), - (0.064, 4), - (0.128, 4), - (0.256, 4), - (0.512, 4), - (1.024, 4), - (2.048, 4), - (4.096, 4), - (8.192, 4), - (float('inf'), 4), - ]: - bucket = histogram_metric.histogram.bucket.add() - bucket.upper_bound = upper_bound - bucket.cumulative_count = cumulative_count - - # Root histogram sample - histogram_metric.histogram.sample_count = 4 - histogram_metric.histogram.sample_sum = 0.026131671 - - # Kubernetes - histogram_metric = expected_etcd_metric.metric.add() - - # Label for app kubernetes - summary_label = histogram_metric.label.add() - summary_label.name, summary_label.value = "kind", "fs" - summary_label = histogram_metric.label.add() - summary_label.name, summary_label.value = "app", "kubernetes" - - for upper_bound, cumulative_count in [ - (0.001, 718), - (0.002, 740), - (0.004, 743), - (0.008, 748), - (0.016, 751), - (0.032, 751), - (0.064, 751), - (0.128, 751), - (0.256, 751), - (0.512, 751), - (1.024, 751), - (2.048, 751), - (4.096, 751), - (8.192, 751), - (float('inf'), 751), - ]: - bucket = histogram_metric.histogram.bucket.add() - bucket.upper_bound = upper_bound - bucket.cumulative_count = cumulative_count - - # Root histogram sample - histogram_metric.histogram.sample_count = 751 - histogram_metric.histogram.sample_sum = 0.3097010759999998 + expected_etcd_metric = HistogramMetricFamily( + 'etcd_disk_wal_fsync_duration_seconds', + 'The latency distributions of fsync called by wal.', + labels=['kind', 'app'] + ) + expected_etcd_metric.add_metric(['fs', 'vault'], buckets=[ + ('0.001', 2.0), + ('0.002', 2.0), + ('0.004', 2.0), + ('0.008', 2.0), + ('0.016', 4.0), + ('0.032', 4.0), + ('0.064', 4.0), + ('0.128', 4.0), + ('0.256', 4.0), + ('0.512', 4.0), + ('1.024', 4.0), + ('2.048', 4.0), + ('4.096', 4.0), + ('8.192', 4.0), + ('+Inf', 4.0) + ], sum_value=0.026131671) + expected_etcd_metric.add_metric(['fs', 'kubernetes'], buckets=[ + ('0.001', 718.0), + ('0.002', 740.0), + ('0.004', 743.0), + ('0.008', 748.0), + ('0.016', 751.0), + ('0.032', 751.0), + ('0.064', 751.0), + ('0.128', 751.0), + ('0.256', 751.0), + ('0.512', 751.0), + ('1.024', 751.0), + ('2.048', 751.0), + ('4.096', 751.0), + ('8.192', 751.0), + ('+Inf', 751.0) + ], sum_value=0.3097010759999998) # Iter on the generator to get all metrics - response = MockResponse(text_data, 'text/plain; version=0.0.4') + response = MockResponse(text_data, text_content_type) check = p_check metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] @@ -1006,13 +702,10 @@ def test_parse_two_histograms_with_label(p_check, mocked_prometheus_scraper_conf # in metrics with more than one label # the labels don't always get parsed in a deterministic order # deconstruct the metric to ensure it's equal - assert expected_etcd_metric.help == current_metric.help + assert expected_etcd_metric.documentation == current_metric.documentation assert expected_etcd_metric.name == current_metric.name assert expected_etcd_metric.type == current_metric.type - for idx in range(len(expected_etcd_metric.metric)): - assert expected_etcd_metric.metric[idx].summary == current_metric.metric[idx].summary - for label in expected_etcd_metric.metric[idx].label: - assert label in current_metric.metric[idx].label + assert sorted(expected_etcd_metric.samples, key=lambda i: i[0]) == sorted(current_metric.samples, key=lambda i: i[0]) def test_parse_one_summary(p_check, mocked_prometheus_scraper_config): @@ -1053,42 +746,61 @@ def test_parse_one_summary(p_check, mocked_prometheus_scraper_config): 'http_response_size_bytes_count{handler="prometheus"} 5\n' ) - expected_etcd_metric = metrics_pb2.MetricFamily() - expected_etcd_metric.help = "The HTTP response sizes in bytes." - expected_etcd_metric.name = "http_response_size_bytes" - expected_etcd_metric.type = 2 - - summary_metric = expected_etcd_metric.metric.add() + expected_etcd_metric = SummaryMetricFamily('http_response_size_bytes', 'The HTTP response sizes in bytes.', labels=["handler"]) + expected_etcd_metric.add_metric(["prometheus"], 5.0, 120512.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"handler":"prometheus", "quantile": "0.5"}, 24547.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"handler":"prometheus", "quantile": "0.9"}, 25763.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"handler":"prometheus", "quantile": "0.99"}, 25763.0) - # Label for prometheus handler - summary_label = summary_metric.label.add() - summary_label.name, summary_label.value = "handler", "prometheus" - - # Root summary sample - summary_metric.summary.sample_count = 5 - summary_metric.summary.sample_sum = 120512 + # Iter on the generator to get all metrics + response = MockResponse(text_data, text_content_type) + check = p_check + metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] - # Create quantiles 0.5, 0.9, 0.99 - quantile_05 = summary_metric.summary.quantile.add() - quantile_05.quantile = 0.5 - quantile_05.value = 24547 + assert 1 == len(metrics) + current_metric = metrics[0] + assert expected_etcd_metric.documentation == current_metric.documentation + assert expected_etcd_metric.name == current_metric.name + assert expected_etcd_metric.type == current_metric.type + assert sorted(expected_etcd_metric.samples, key=lambda i: i[0]) == sorted(current_metric.samples, key=lambda i: i[0]) - quantile_09 = summary_metric.summary.quantile.add() - quantile_09.quantile = 0.9 - quantile_09.value = 25763 +def test_parse_one_summary_with_no_quantile(p_check, mocked_prometheus_scraper_config): + """ + name: "http_response_size_bytes" + help: "The HTTP response sizes in bytes." + type: SUMMARY + metric { + label { + name: "handler" + value: "prometheus" + } + summary { + sample_count: 5 + sample_sum: 120512.0 + } + } + """ + text_data = ( + '# HELP http_response_size_bytes The HTTP response sizes in bytes.\n' + '# TYPE http_response_size_bytes summary\n' + 'http_response_size_bytes_sum{handler="prometheus"} 120512\n' + 'http_response_size_bytes_count{handler="prometheus"} 5\n' + ) - quantile_099 = summary_metric.summary.quantile.add() - quantile_099.quantile = 0.99 - quantile_099.value = 25763 + expected_etcd_metric = SummaryMetricFamily('http_response_size_bytes', 'The HTTP response sizes in bytes.', labels=["handler"]) + expected_etcd_metric.add_metric(["prometheus"], 5.0, 120512.0) # Iter on the generator to get all metrics - response = MockResponse(text_data, 'text/plain; version=0.0.4') + response = MockResponse(text_data, text_content_type) check = p_check metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] assert 1 == len(metrics) current_metric = metrics[0] - assert expected_etcd_metric == current_metric + assert expected_etcd_metric.documentation == current_metric.documentation + assert expected_etcd_metric.name == current_metric.name + assert expected_etcd_metric.type == current_metric.type + assert sorted(expected_etcd_metric.samples, key=lambda i: i[0]) == sorted(current_metric.samples, key=lambda i: i[0]) def test_parse_two_summaries_with_labels(p_check, mocked_prometheus_scraper_config): @@ -1107,84 +819,28 @@ def test_parse_two_summaries_with_labels(p_check, mocked_prometheus_scraper_conf 'http_response_size_bytes_count{from="cluster",handler="prometheus"} 4\n' ) - expected_etcd_metric = metrics_pb2.MetricFamily() - expected_etcd_metric.help = "The HTTP response sizes in bytes." - expected_etcd_metric.name = "http_response_size_bytes" - expected_etcd_metric.type = 2 - - # Metric from internet # - summary_metric_from_internet = expected_etcd_metric.metric.add() - - # Label for prometheus handler - summary_label = summary_metric_from_internet.label.add() - summary_label.name, summary_label.value = "handler", "prometheus" - - summary_label = summary_metric_from_internet.label.add() - summary_label.name, summary_label.value = "from", "internet" - - # Root summary sample - summary_metric_from_internet.summary.sample_count = 5 - summary_metric_from_internet.summary.sample_sum = 120512 - - # Create quantiles 0.5, 0.9, 0.99 - quantile_05 = summary_metric_from_internet.summary.quantile.add() - quantile_05.quantile = 0.5 - quantile_05.value = 24547 - - quantile_09 = summary_metric_from_internet.summary.quantile.add() - quantile_09.quantile = 0.9 - quantile_09.value = 25763 - - quantile_099 = summary_metric_from_internet.summary.quantile.add() - quantile_099.quantile = 0.99 - quantile_099.value = 25763 - - # Metric from cluster # - summary_metric_from_cluster = expected_etcd_metric.metric.add() - - # Label for prometheus handler - summary_label = summary_metric_from_cluster.label.add() - summary_label.name, summary_label.value = "handler", "prometheus" - - summary_label = summary_metric_from_cluster.label.add() - summary_label.name, summary_label.value = "from", "cluster" - - # Root summary sample - summary_metric_from_cluster.summary.sample_count = 4 - summary_metric_from_cluster.summary.sample_sum = 94913 - - # Create quantiles 0.5, 0.9, 0.99 - quantile_05 = summary_metric_from_cluster.summary.quantile.add() - quantile_05.quantile = 0.5 - quantile_05.value = 24615 - - quantile_09 = summary_metric_from_cluster.summary.quantile.add() - quantile_09.quantile = 0.9 - quantile_09.value = 24627 - - quantile_099 = summary_metric_from_cluster.summary.quantile.add() - quantile_099.quantile = 0.99 - quantile_099.value = 24627 + expected_etcd_metric = SummaryMetricFamily('http_response_size_bytes', 'The HTTP response sizes in bytes.', labels=["from","handler"]) + expected_etcd_metric.add_metric(["internet","prometheus"], 5.0, 120512.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"from":"internet", "handler":"prometheus", "quantile": "0.5"}, 24547.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"from":"internet", "handler":"prometheus", "quantile": "0.9"}, 25763.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"from":"internet", "handler":"prometheus", "quantile": "0.99"}, 25763.0) + expected_etcd_metric.add_metric(["cluster","prometheus"], 4.0, 94913.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"from":"cluster", "handler":"prometheus", "quantile": "0.5"}, 24615.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"from":"cluster", "handler":"prometheus", "quantile": "0.9"}, 24627.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"from":"cluster", "handler":"prometheus", "quantile": "0.99"}, 24627.0) # Iter on the generator to get all metrics - response = MockResponse(text_data, 'text/plain; version=0.0.4') + response = MockResponse(text_data, text_content_type) check = p_check metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] assert 1 == len(metrics) current_metric = metrics[0] - # in metrics with more than one label - # the labels don't always get parsed in a deterministic order - # deconstruct the metric to ensure it's equal - assert expected_etcd_metric.help == current_metric.help + assert expected_etcd_metric.documentation == current_metric.documentation assert expected_etcd_metric.name == current_metric.name assert expected_etcd_metric.type == current_metric.type - for idx in range(len(expected_etcd_metric.metric)): - assert expected_etcd_metric.metric[idx].summary == current_metric.metric[idx].summary - for label in expected_etcd_metric.metric[idx].label: - assert label in current_metric.metric[idx].label - + assert sorted(expected_etcd_metric.samples, key=lambda i: i[0]) == sorted(current_metric.samples, key=lambda i: i[0]) def test_parse_one_summary_with_none_values(p_check, mocked_prometheus_scraper_config): text_data = ( @@ -1197,44 +853,25 @@ def test_parse_one_summary_with_none_values(p_check, mocked_prometheus_scraper_c 'http_response_size_bytes_count{handler="prometheus"} 0\n' ) - expected_etcd_metric = metrics_pb2.MetricFamily() - expected_etcd_metric.help = "The HTTP response sizes in bytes." - expected_etcd_metric.name = "http_response_size_bytes" - expected_etcd_metric.type = 2 - - summary_metric = expected_etcd_metric.metric.add() - - # Label for prometheus handler - summary_label = summary_metric.label.add() - summary_label.name, summary_label.value = "handler", "prometheus" - - # Root summary sample - summary_metric.summary.sample_count = 0 - summary_metric.summary.sample_sum = 0. - - # Create quantiles 0.5, 0.9, 0.99 - quantile_05 = summary_metric.summary.quantile.add() - quantile_05.quantile = 0.5 - quantile_05.value = float('nan') - - quantile_09 = summary_metric.summary.quantile.add() - quantile_09.quantile = 0.9 - quantile_09.value = float('nan') - - quantile_099 = summary_metric.summary.quantile.add() - quantile_099.quantile = 0.99 - quantile_099.value = float('nan') + expected_etcd_metric = SummaryMetricFamily('http_response_size_bytes', 'The HTTP response sizes in bytes.', labels=["handler"]) + expected_etcd_metric.add_metric(["prometheus"], 0.0, 0.0) + expected_etcd_metric.add_sample("http_response_size_bytes", {"handler":"prometheus", "quantile": "0.5"}, float('nan')) + expected_etcd_metric.add_sample("http_response_size_bytes", {"handler":"prometheus", "quantile": "0.9"}, float('nan')) + expected_etcd_metric.add_sample("http_response_size_bytes", {"handler":"prometheus", "quantile": "0.99"}, float('nan')) # Iter on the generator to get all metrics - response = MockResponse(text_data, 'text/plain; version=0.0.4') + response = MockResponse(text_data, text_content_type) check = p_check metrics = [k for k in check.parse_metric_family(response, mocked_prometheus_scraper_config)] assert 1 == len(metrics) current_metric = metrics[0] + assert expected_etcd_metric.documentation == current_metric.documentation + assert expected_etcd_metric.name == current_metric.name + assert expected_etcd_metric.type == current_metric.type # As the NaN value isn't supported when we are calling assertEqual - # we need to compare the object representation instead of the object itself - assert expected_etcd_metric.__repr__() == current_metric.__repr__() - + assert math.isnan(current_metric.samples[0][2]) + assert math.isnan(current_metric.samples[1][2]) + assert math.isnan(current_metric.samples[2][2]) def test_label_joins(aggregator, mocked_prometheus_check, mocked_prometheus_scraper_config, mock_get): """ Tests label join on text format """ @@ -1440,7 +1077,7 @@ def test_label_joins_gc(aggregator, mocked_prometheus_check, mocked_prometheus_s assert 15 == len(mocked_prometheus_scraper_config['_label_mapping']['pod']) text_data = mock_get.replace('dd-agent-62bgh', 'dd-agent-1337') mock_response = mock.MagicMock( - status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={'Content-Type': "text/plain"} + status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={'Content-Type': text_content_type} ) with mock.patch('requests.get', return_value=mock_response, __name__="get"): check.process(mocked_prometheus_scraper_config) diff --git a/datadog_checks_base/tests/test_openmetrics_base_check.py b/datadog_checks_base/tests/test_openmetrics_base_check.py index 93364af79c4977..c44ce226b6d8e2 100644 --- a/datadog_checks_base/tests/test_openmetrics_base_check.py +++ b/datadog_checks_base/tests/test_openmetrics_base_check.py @@ -7,13 +7,12 @@ def test_rate_override(): 'metrics': [{"test_rate": "test.rate"}], 'type_overrides': {"test_rate": "rate"} } - expected_type_overrides = {"test_rate": "gauge"} + expected_type_overrides = {"test_rate": "rate"} check = OpenMetricsBaseCheck('prometheus_check', {}, {}, [instance], default_namespace="foo") processed_type_overrides = check.config_map[endpoint]['type_overrides'] assert expected_type_overrides == processed_type_overrides - assert ["test_rate"] == check.config_map[endpoint]['rate_metrics'] def test_timeout_override():