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