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))