github.com/apache/beam/sdks/v2@v2.48.2/python/apache_beam/internal/metrics/metric.py (about) 1 # 2 # Licensed to the Apache Software Foundation (ASF) under one or more 3 # contributor license agreements. See the NOTICE file distributed with 4 # this work for additional information regarding copyright ownership. 5 # The ASF licenses this file to You under the Apache License, Version 2.0 6 # (the "License"); you may not use this file except in compliance with 7 # the License. You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 # 17 18 """ 19 Metrics API classes for internal use only. 20 21 Users should use apache_beam.metrics.metric package instead. 22 23 For internal use only. No backwards compatibility guarantees. 24 """ 25 # pytype: skip-file 26 # mypy: disallow-untyped-defs 27 28 import datetime 29 import logging 30 import threading 31 import time 32 from typing import TYPE_CHECKING 33 from typing import Dict 34 from typing import Optional 35 from typing import Type 36 from typing import Union 37 38 from apache_beam.internal.metrics.cells import HistogramCellFactory 39 from apache_beam.metrics import monitoring_infos 40 from apache_beam.metrics.execution import MetricUpdater 41 from apache_beam.metrics.metric import Metrics as UserMetrics 42 from apache_beam.metrics.metricbase import Histogram 43 from apache_beam.metrics.metricbase import MetricName 44 45 if TYPE_CHECKING: 46 from apache_beam.metrics.cells import MetricCell 47 from apache_beam.metrics.cells import MetricCellFactory 48 from apache_beam.utils.histogram import BucketType 49 50 # Protect against environments where bigquery library is not available. 51 # pylint: disable=wrong-import-order, wrong-import-position 52 try: 53 from apitools.base.py.exceptions import HttpError 54 except ImportError: 55 pass 56 57 __all__ = ['Metrics'] 58 59 _LOGGER = logging.getLogger(__name__) 60 61 62 class Metrics(object): 63 @staticmethod 64 def counter(urn, labels=None, process_wide=False): 65 # type: (str, Optional[Dict[str, str]], bool) -> UserMetrics.DelegatingCounter 66 67 """Obtains or creates a Counter metric. 68 69 Args: 70 namespace: A class or string that gives the namespace to a metric 71 name: A string that gives a unique name to a metric 72 urn: URN to populate on a MonitoringInfo, when sending to RunnerHarness. 73 labels: Labels to populate on a MonitoringInfo 74 process_wide: Whether or not the metric is specific to the current bundle 75 or should be calculated for the entire process. 76 77 Returns: 78 A Counter object. 79 """ 80 return UserMetrics.DelegatingCounter( 81 MetricName(namespace=None, name=None, urn=urn, labels=labels), 82 process_wide=process_wide) 83 84 @staticmethod 85 def histogram(namespace, name, bucket_type, logger=None): 86 # type: (Union[Type, str], str, BucketType, Optional[MetricLogger]) -> Metrics.DelegatingHistogram 87 88 """Obtains or creates a Histogram metric. 89 90 Args: 91 namespace: A class or string that gives the namespace to a metric 92 name: A string that gives a unique name to a metric 93 bucket_type: A type of bucket used in a histogram. A subclass of 94 apache_beam.utils.histogram.BucketType 95 logger: MetricLogger for logging locally aggregated metric 96 97 Returns: 98 A Histogram object. 99 """ 100 namespace = UserMetrics.get_namespace(namespace) 101 return Metrics.DelegatingHistogram( 102 MetricName(namespace, name), bucket_type, logger) 103 104 class DelegatingHistogram(Histogram): 105 """Metrics Histogram that Delegates functionality to MetricsEnvironment.""" 106 def __init__(self, metric_name, bucket_type, logger): 107 # type: (MetricName, BucketType, Optional[MetricLogger]) -> None 108 super().__init__(metric_name) 109 self.metric_name = metric_name 110 self.cell_type = HistogramCellFactory(bucket_type) 111 self.logger = logger 112 self.updater = MetricUpdater(self.cell_type, self.metric_name) 113 114 def update(self, value): 115 # type: (object) -> None 116 self.updater(value) 117 if self.logger: 118 self.logger.update(self.cell_type, self.metric_name, value) 119 120 121 class MetricLogger(object): 122 """Simple object to locally aggregate and log metrics.""" 123 def __init__(self): 124 # type: () -> None 125 self._metric = {} # type: Dict[MetricName, MetricCell] 126 self._lock = threading.Lock() 127 self._last_logging_millis = int(time.time() * 1000) 128 self.minimum_logging_frequency_msec = 180000 129 130 def update(self, cell_type, metric_name, value): 131 # type: (Union[Type[MetricCell], MetricCellFactory], MetricName, object) -> None 132 cell = self._get_metric_cell(cell_type, metric_name) 133 cell.update(value) 134 135 def _get_metric_cell(self, cell_type, metric_name): 136 # type: (Union[Type[MetricCell], MetricCellFactory], MetricName) -> MetricCell 137 with self._lock: 138 if metric_name not in self._metric: 139 self._metric[metric_name] = cell_type() 140 return self._metric[metric_name] 141 142 def log_metrics(self, reset_after_logging=False): 143 # type: (bool) -> None 144 if self._lock.acquire(False): 145 try: 146 current_millis = int(time.time() * 1000) 147 if ((current_millis - self._last_logging_millis) > 148 self.minimum_logging_frequency_msec): 149 logging_metric_info = [ 150 '[Locally aggregated metrics since %s]' % 151 datetime.datetime.fromtimestamp( 152 self._last_logging_millis / 1000.0) 153 ] 154 for name, cell in self._metric.items(): 155 logging_metric_info.append('%s: %s' % (name, cell.get_cumulative())) 156 _LOGGER.info('\n'.join(logging_metric_info)) 157 if reset_after_logging: 158 self._metric = {} 159 self._last_logging_millis = current_millis 160 finally: 161 self._lock.release() 162 163 164 class ServiceCallMetric(object): 165 """Metric class which records Service API call metrics. 166 167 This class will capture a request count metric for the specified 168 request_count_urn and base_labels. 169 170 When call() is invoked the status must be provided, which will 171 be converted to a canonical GCP status code, if possible. 172 173 TODO(ajamato): Add Request latency metric. 174 """ 175 def __init__(self, request_count_urn, base_labels=None): 176 # type: (str, Optional[Dict[str, str]]) -> None 177 self.base_labels = base_labels if base_labels else {} 178 self.request_count_urn = request_count_urn 179 180 def call(self, status): 181 # type: (Union[int, str, HttpError]) -> None 182 183 """Record the status of the call into appropriate metrics.""" 184 canonical_status = self.convert_to_canonical_status_string(status) 185 additional_labels = {monitoring_infos.STATUS_LABEL: canonical_status} 186 187 labels = dict( 188 list(self.base_labels.items()) + list(additional_labels.items())) 189 190 request_counter = Metrics.counter( 191 urn=self.request_count_urn, labels=labels, process_wide=True) 192 request_counter.inc() 193 194 def convert_to_canonical_status_string(self, status): 195 # type: (Union[int, str, HttpError]) -> str 196 197 """Converts a status to a canonical GCP status cdoe string.""" 198 http_status_code = None 199 if isinstance(status, int): 200 http_status_code = status 201 elif isinstance(status, str): 202 return status.lower() 203 elif isinstance(status, HttpError): 204 http_status_code = int(status.status_code) 205 http_to_canonical_gcp_status = { 206 200: 'ok', 207 400: 'out_of_range', 208 401: 'unauthenticated', 209 403: 'permission_denied', 210 404: 'not_found', 211 409: 'already_exists', 212 429: 'resource_exhausted', 213 499: 'cancelled', 214 500: 'internal', 215 501: 'not_implemented', 216 503: 'unavailable', 217 504: 'deadline_exceeded' 218 } 219 if (http_status_code is not None and 220 http_status_code in http_to_canonical_gcp_status): 221 return http_to_canonical_gcp_status[http_status_code] 222 return str(http_status_code) 223 224 @staticmethod 225 def bigtable_error_code_to_grpc_status_string(grpc_status_code): 226 # type: (Optional[int]) -> str 227 228 """ 229 Converts the bigtable error code to a canonical GCP status code string. 230 231 This Bigtable client library is not using the canonical http status code 232 values (i.e. https://cloud.google.com/apis/design/errors)" 233 Instead they are numbered using an enum with these values corresponding 234 to each status code: https://cloud.google.com/bigtable/docs/status-codes 235 236 Args: 237 grpc_status_code: An int that corresponds to an enum of status codes 238 239 Returns: 240 A GCP status code string 241 """ 242 grpc_to_canonical_gcp_status = { 243 0: 'ok', 244 1: 'cancelled', 245 2: 'unknown', 246 3: 'invalid_argument', 247 4: 'deadline_exceeded', 248 5: 'not_found', 249 6: 'already_exists', 250 7: 'permission_denied', 251 8: 'resource_exhausted', 252 9: 'failed_precondition', 253 10: 'aborted', 254 11: 'out_of_range', 255 12: 'unimplemented', 256 13: 'internal', 257 14: 'unavailable' 258 } 259 if grpc_status_code is None: 260 # Bigtable indicates this can be retried but itself has exhausted retry 261 # timeout or there is no retry policy set for bigtable. 262 return grpc_to_canonical_gcp_status[4] 263 return grpc_to_canonical_gcp_status.get( 264 grpc_status_code, str(grpc_status_code))