Wei-Yu Chen | ad55cb8 | 2022-02-15 20:07:01 +0800 | [diff] [blame] | 1 | # SPDX-FileCopyrightText: 2020 The Magma Authors. |
| 2 | # SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org> |
| 3 | # |
| 4 | # SPDX-License-Identifier: BSD-3-Clause |
Wei-Yu Chen | 49950b9 | 2021-11-08 19:19:18 +0800 | [diff] [blame] | 5 | |
| 6 | import logging |
| 7 | import time |
| 8 | |
| 9 | import metrics_pb2 |
| 10 | from orc8r.protos import metricsd_pb2 |
| 11 | from prometheus_client import REGISTRY |
| 12 | |
| 13 | |
| 14 | def get_metrics(registry=REGISTRY, verbose=False): |
| 15 | """ |
| 16 | Collects timeseries samples from prometheus metric collector registry |
| 17 | adds a common timestamp, and encodes them to protobuf |
| 18 | |
| 19 | Arguments: |
| 20 | regsitry: a prometheus CollectorRegistry instance |
| 21 | verbose: whether to optimize for bandwidth and ignore metric name/help |
| 22 | |
| 23 | Returns: |
| 24 | a prometheus MetricFamily protobuf stream |
| 25 | """ |
| 26 | timestamp_ms = int(time.time() * 1000) |
| 27 | for metric_family in registry.collect(): |
| 28 | if metric_family.type in ('counter', 'gauge'): |
| 29 | family_proto = encode_counter_gauge(metric_family, timestamp_ms) |
| 30 | elif metric_family.type == 'summary': |
| 31 | family_proto = encode_summary(metric_family, timestamp_ms) |
| 32 | elif metric_family.type == 'histogram': |
| 33 | family_proto = encode_histogram(metric_family, timestamp_ms) |
| 34 | |
| 35 | if verbose: |
| 36 | family_proto.help = metric_family.documentation |
| 37 | family_proto.name = metric_family.name |
| 38 | else: |
| 39 | try: |
| 40 | family_proto.name = \ |
| 41 | str(metricsd_pb2.MetricName.Value(metric_family.name)) |
| 42 | except ValueError as e: |
| 43 | logging.debug(e) # If enum is not defined |
| 44 | family_proto.name = metric_family.name |
| 45 | yield family_proto |
| 46 | |
| 47 | |
| 48 | def encode_counter_gauge(family, timestamp_ms): |
| 49 | """ |
| 50 | Takes a Counter/Gauge family which is a collection of timeseries |
| 51 | samples that share a name (uniquely identified by labels) and yields |
| 52 | equivalent protobufs. |
| 53 | |
| 54 | Each timeseries corresponds to a single sample tuple of the format: |
| 55 | (NAME, LABELS, VALUE) |
| 56 | |
| 57 | Arguments: |
| 58 | family: a prometheus gauge metric family |
| 59 | timestamp_ms: the timestamp to attach to the samples |
| 60 | Raises: |
| 61 | ValueError if metric name is not defined in MetricNames protobuf |
| 62 | Returns: |
| 63 | A Counter or Gauge prometheus MetricFamily protobuf |
| 64 | """ |
| 65 | family_proto = metrics_pb2.MetricFamily() |
| 66 | family_proto.type = \ |
| 67 | metrics_pb2.MetricType.Value(family.type.upper()) |
| 68 | for sample in family.samples: |
| 69 | metric_proto = metrics_pb2.Metric() |
| 70 | if family_proto.type == metrics_pb2.COUNTER: |
| 71 | metric_proto.counter.value = sample[2] |
| 72 | elif family_proto.type == metrics_pb2.GAUGE: |
| 73 | metric_proto.gauge.value = sample[2] |
| 74 | # Add meta-data to the timeseries |
| 75 | metric_proto.timestamp_ms = timestamp_ms |
| 76 | metric_proto.label.extend(_convert_labels_to_enums(sample[1].items())) |
| 77 | # Append metric sample to family |
| 78 | family_proto.metric.extend([metric_proto]) |
| 79 | return family_proto |
| 80 | |
| 81 | |
| 82 | def encode_summary(family, timestamp_ms): |
| 83 | """ |
| 84 | Takes a Summary Metric family which is a collection of timeseries |
| 85 | samples that share a name (uniquely identified by labels) and yields |
| 86 | equivalent protobufs. |
| 87 | |
| 88 | Each summary timeseries consists of sample tuples for the count, sum, |
| 89 | and quantiles in the format (NAME,LABELS,VALUE). The NAME is suffixed |
| 90 | with either _count, _sum to indicate count and sum respectively. |
| 91 | Quantile samples will be of the same NAME with quantile label. |
| 92 | |
| 93 | Arguments: |
| 94 | family: a prometheus summary metric family |
| 95 | timestamp_ms: the timestamp to attach to the samples |
| 96 | Raises: |
| 97 | ValueError if metric name is not defined in MetricNames protobuf |
| 98 | Returns: |
| 99 | a Summary prometheus MetricFamily protobuf |
| 100 | """ |
| 101 | family_proto = metrics_pb2.MetricFamily() |
| 102 | family_proto.type = metrics_pb2.SUMMARY |
| 103 | metric_protos = {} |
| 104 | # Build a map of each of the summary timeseries from the samples |
| 105 | for sample in family.samples: |
| 106 | quantile = sample[1].pop('quantile', None) # Remove from label set |
| 107 | # Each time series identified by label set excluding the quantile |
| 108 | metric_proto = \ |
| 109 | metric_protos.setdefault( |
| 110 | frozenset(sample[1].items()), |
| 111 | metrics_pb2.Metric(), |
| 112 | ) |
| 113 | if sample[0].endswith('_count'): |
| 114 | metric_proto.summary.sample_count = int(sample[2]) |
| 115 | elif sample[0].endswith('_sum'): |
| 116 | metric_proto.summary.sample_sum = sample[2] |
| 117 | elif quantile: |
| 118 | quantile = metric_proto.summary.quantile.add() |
| 119 | quantile.value = sample[2] |
| 120 | quantile.quantile = _goStringToFloat(quantile) |
| 121 | # Go back and add meta-data to the timeseries |
| 122 | for labels, metric_proto in metric_protos.items(): |
| 123 | metric_proto.timestamp_ms = timestamp_ms |
| 124 | metric_proto.label.extend(_convert_labels_to_enums(labels)) |
| 125 | # Add it to the family |
| 126 | family_proto.metric.extend([metric_proto]) |
| 127 | return family_proto |
| 128 | |
| 129 | |
| 130 | def encode_histogram(family, timestamp_ms): |
| 131 | """ |
| 132 | Takes a Histogram Metric family which is a collection of timeseries |
| 133 | samples that share a name (uniquely identified by labels) and yields |
| 134 | equivalent protobufs. |
| 135 | |
| 136 | Each summary timeseries consists of sample tuples for the count, sum, |
| 137 | and quantiles in the format (NAME,LABELS,VALUE). The NAME is suffixed |
| 138 | with either _count, _sum, _buckets to indicate count, sum and buckets |
| 139 | respectively. Bucket samples will also contain a le to indicate its |
| 140 | upper bound. |
| 141 | |
| 142 | Arguments: |
| 143 | family: a prometheus histogram metric family |
| 144 | timestamp_ms: the timestamp to attach to the samples |
| 145 | Raises: |
| 146 | ValueError if metric name is not defined in MetricNames protobuf |
| 147 | Returns: |
| 148 | a Histogram prometheus MetricFamily protobuf |
| 149 | """ |
| 150 | family_proto = metrics_pb2.MetricFamily() |
| 151 | family_proto.type = metrics_pb2.HISTOGRAM |
| 152 | metric_protos = {} |
| 153 | for sample in family.samples: |
| 154 | upper_bound = sample[1].pop('le', None) # Remove from label set |
| 155 | metric_proto = \ |
| 156 | metric_protos.setdefault( |
| 157 | frozenset(sample[1].items()), |
| 158 | metrics_pb2.Metric(), |
| 159 | ) |
| 160 | if sample[0].endswith('_count'): |
| 161 | metric_proto.histogram.sample_count = int(sample[2]) |
| 162 | elif sample[0].endswith('_sum'): |
| 163 | metric_proto.histogram.sample_sum = sample[2] |
| 164 | elif sample[0].endswith('_bucket'): |
| 165 | quantile = metric_proto.histogram.bucket.add() |
| 166 | quantile.cumulative_count = int(sample[2]) |
| 167 | quantile.upper_bound = _goStringToFloat(upper_bound) |
| 168 | # Go back and add meta-data to the timeseries |
| 169 | for labels, metric_proto in metric_protos.items(): |
| 170 | metric_proto.timestamp_ms = timestamp_ms |
| 171 | metric_proto.label.extend(_convert_labels_to_enums(labels)) |
| 172 | # Add it to the family |
| 173 | family_proto.metric.extend([metric_proto]) |
| 174 | return family_proto |
| 175 | |
| 176 | |
| 177 | def _goStringToFloat(s): |
| 178 | if s == '+Inf': |
| 179 | return float("inf") |
| 180 | elif s == '-Inf': |
| 181 | return float("-inf") |
| 182 | elif s == 'NaN': |
| 183 | return float('nan') |
| 184 | else: |
| 185 | return float(s) |
| 186 | |
| 187 | |
| 188 | def _convert_labels_to_enums(labels): |
| 189 | """ |
| 190 | Try to convert both the label names and label values to enum values. |
| 191 | Defaults to the given name and value if it fails to convert. |
| 192 | Arguments: |
| 193 | labels: an array of label pairs that may contain enum names |
| 194 | Returns: |
| 195 | an array of label pairs with enum names converted to enum values |
| 196 | """ |
| 197 | new_labels = [] |
| 198 | for name, value in labels: |
| 199 | try: |
| 200 | name = str(metricsd_pb2.MetricLabelName.Value(name)) |
| 201 | except ValueError as e: |
| 202 | logging.debug(e) |
| 203 | new_labels.append(metrics_pb2.LabelPair(name=name, value=value)) |
| 204 | return new_labels |