github.com/apache/beam/sdks/v2@v2.48.2/python/apache_beam/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  User-facing classes for Metrics API.
    20  
    21  The classes in this file allow users to define and use metrics to be collected
    22  and displayed as part of their pipeline execution.
    23  
    24  - Metrics - This class lets pipeline and transform writers create and access
    25      metric objects such as counters, distributions, etc.
    26  """
    27  # pytype: skip-file
    28  # mypy: disallow-untyped-defs
    29  
    30  import logging
    31  from typing import TYPE_CHECKING
    32  from typing import Dict
    33  from typing import FrozenSet
    34  from typing import Iterable
    35  from typing import List
    36  from typing import Optional
    37  from typing import Set
    38  from typing import Type
    39  from typing import Union
    40  
    41  from apache_beam.metrics import cells
    42  from apache_beam.metrics.execution import MetricUpdater
    43  from apache_beam.metrics.metricbase import Counter
    44  from apache_beam.metrics.metricbase import Distribution
    45  from apache_beam.metrics.metricbase import Gauge
    46  from apache_beam.metrics.metricbase import MetricName
    47  
    48  if TYPE_CHECKING:
    49    from apache_beam.metrics.execution import MetricKey
    50    from apache_beam.metrics.metricbase import Metric
    51  
    52  __all__ = ['Metrics', 'MetricsFilter']
    53  
    54  _LOGGER = logging.getLogger(__name__)
    55  
    56  
    57  class Metrics(object):
    58    """Lets users create/access metric objects during pipeline execution."""
    59    @staticmethod
    60    def get_namespace(namespace):
    61      # type: (Union[Type, str]) -> str
    62      if isinstance(namespace, type):
    63        return '{}.{}'.format(namespace.__module__, namespace.__name__)
    64      elif isinstance(namespace, str):
    65        return namespace
    66      else:
    67        raise ValueError('Unknown namespace type')
    68  
    69    @staticmethod
    70    def counter(namespace, name):
    71      # type: (Union[Type, str], str) -> Metrics.DelegatingCounter
    72  
    73      """Obtains or creates a Counter metric.
    74  
    75      Args:
    76        namespace: A class or string that gives the namespace to a metric
    77        name: A string that gives a unique name to a metric
    78  
    79      Returns:
    80        A Counter object.
    81      """
    82      namespace = Metrics.get_namespace(namespace)
    83      return Metrics.DelegatingCounter(MetricName(namespace, name))
    84  
    85    @staticmethod
    86    def distribution(namespace, name):
    87      # type: (Union[Type, str], str) -> Metrics.DelegatingDistribution
    88  
    89      """Obtains or creates a Distribution metric.
    90  
    91      Distribution metrics are restricted to integer-only distributions.
    92  
    93      Args:
    94        namespace: A class or string that gives the namespace to a metric
    95        name: A string that gives a unique name to a metric
    96  
    97      Returns:
    98        A Distribution object.
    99      """
   100      namespace = Metrics.get_namespace(namespace)
   101      return Metrics.DelegatingDistribution(MetricName(namespace, name))
   102  
   103    @staticmethod
   104    def gauge(namespace, name):
   105      # type: (Union[Type, str], str) -> Metrics.DelegatingGauge
   106  
   107      """Obtains or creates a Gauge metric.
   108  
   109      Gauge metrics are restricted to integer-only values.
   110  
   111      Args:
   112        namespace: A class or string that gives the namespace to a metric
   113        name: A string that gives a unique name to a metric
   114  
   115      Returns:
   116        A Distribution object.
   117      """
   118      namespace = Metrics.get_namespace(namespace)
   119      return Metrics.DelegatingGauge(MetricName(namespace, name))
   120  
   121    class DelegatingCounter(Counter):
   122      """Metrics Counter that Delegates functionality to MetricsEnvironment."""
   123      def __init__(self, metric_name, process_wide=False):
   124        # type: (MetricName, bool) -> None
   125        super().__init__(metric_name)
   126        self.inc = MetricUpdater(  # type: ignore[assignment]
   127            cells.CounterCell,
   128            metric_name,
   129            default_value=1,
   130            process_wide=process_wide)
   131  
   132    class DelegatingDistribution(Distribution):
   133      """Metrics Distribution Delegates functionality to MetricsEnvironment."""
   134      def __init__(self, metric_name):
   135        # type: (MetricName) -> None
   136        super().__init__(metric_name)
   137        self.update = MetricUpdater(cells.DistributionCell, metric_name)  # type: ignore[assignment]
   138  
   139    class DelegatingGauge(Gauge):
   140      """Metrics Gauge that Delegates functionality to MetricsEnvironment."""
   141      def __init__(self, metric_name):
   142        # type: (MetricName) -> None
   143        super().__init__(metric_name)
   144        self.set = MetricUpdater(cells.GaugeCell, metric_name)  # type: ignore[assignment]
   145  
   146  
   147  class MetricResults(object):
   148    COUNTERS = "counters"
   149    DISTRIBUTIONS = "distributions"
   150    GAUGES = "gauges"
   151  
   152    @staticmethod
   153    def _matches_name(filter, metric_key):
   154      # type: (MetricsFilter, MetricKey) -> bool
   155      if ((filter.namespaces and
   156           metric_key.metric.namespace not in filter.namespaces) or
   157          (filter.names and metric_key.metric.name not in filter.names)):
   158        return False
   159      else:
   160        return True
   161  
   162    @staticmethod
   163    def _is_sub_list(needle, haystack):
   164      # type: (List[str], List[str]) -> bool
   165  
   166      """True iff `needle` is a sub-list of `haystack` (i.e. a contiguous slice
   167      of `haystack` exactly matches `needle`"""
   168      needle_len = len(needle)
   169      haystack_len = len(haystack)
   170      for i in range(0, haystack_len - needle_len + 1):
   171        if haystack[i:i + needle_len] == needle:
   172          return True
   173  
   174      return False
   175  
   176    @staticmethod
   177    def _matches_sub_path(actual_scope, filter_scope):
   178      # type: (str, str) -> bool
   179  
   180      """True iff the '/'-delimited pieces of filter_scope exist as a sub-list
   181      of the '/'-delimited pieces of actual_scope"""
   182      return bool(
   183          actual_scope and MetricResults._is_sub_list(
   184              filter_scope.split('/'), actual_scope.split('/')))
   185  
   186    @staticmethod
   187    def _matches_scope(filter, metric_key):
   188      # type: (MetricsFilter, MetricKey) -> bool
   189      if not filter.steps:
   190        return True
   191  
   192      for step in filter.steps:
   193        if MetricResults._matches_sub_path(metric_key.step, step):
   194          return True
   195  
   196      return False
   197  
   198    @staticmethod
   199    def matches(filter, metric_key):
   200      # type: (Optional[MetricsFilter], MetricKey) -> bool
   201      if filter is None:
   202        return True
   203  
   204      if (MetricResults._matches_name(filter, metric_key) and
   205          MetricResults._matches_scope(filter, metric_key)):
   206        return True
   207      return False
   208  
   209    def query(self, filter=None):
   210      # type: (Optional[MetricsFilter]) -> Dict[str, List[MetricResults]]
   211  
   212      """Queries the runner for existing user metrics that match the filter.
   213  
   214      It should return a dictionary, with lists of each kind of metric, and
   215      each list contains the corresponding kind of MetricResult. Like so:
   216  
   217          {
   218            "counters": [MetricResult(counter_key, committed, attempted), ...],
   219            "distributions": [MetricResult(dist_key, committed, attempted), ...],
   220            "gauges": []  // Empty list if nothing matched the filter.
   221          }
   222  
   223      The committed / attempted values are DistributionResult / GaugeResult / int
   224      objects.
   225      """
   226      raise NotImplementedError
   227  
   228  
   229  class MetricsFilter(object):
   230    """Simple object to filter metrics results.
   231  
   232    If filters by matching a result's step-namespace-name with three internal
   233    sets. No execution/matching logic is added to this object, so that it may
   234    be used to construct arguments as an RPC request. It is left for runners
   235    to implement matching logic by themselves.
   236  
   237    Note: This class only supports user defined metrics.
   238    """
   239    def __init__(self):
   240      # type: () -> None
   241      self._names = set()  # type: Set[str]
   242      self._namespaces = set()  # type: Set[str]
   243      self._steps = set()  # type: Set[str]
   244  
   245    @property
   246    def steps(self):
   247      # type: () -> FrozenSet[str]
   248      return frozenset(self._steps)
   249  
   250    @property
   251    def names(self):
   252      # type: () -> FrozenSet[str]
   253      return frozenset(self._names)
   254  
   255    @property
   256    def namespaces(self):
   257      # type: () -> FrozenSet[str]
   258      return frozenset(self._namespaces)
   259  
   260    def with_metric(self, metric):
   261      # type: (Metric) -> MetricsFilter
   262      name = metric.metric_name.name or ''
   263      namespace = metric.metric_name.namespace or ''
   264      return self.with_name(name).with_namespace(namespace)
   265  
   266    def with_name(self, name):
   267      # type: (str) -> MetricsFilter
   268      return self.with_names([name])
   269  
   270    def with_names(self, names):
   271      # type: (Iterable[str]) -> MetricsFilter
   272      if isinstance(names, str):
   273        raise ValueError('Names must be a collection, not a string')
   274  
   275      self._names.update(names)
   276      return self
   277  
   278    def with_namespace(self, namespace):
   279      # type: (Union[Type, str]) -> MetricsFilter
   280      return self.with_namespaces([namespace])
   281  
   282    def with_namespaces(self, namespaces):
   283      # type: (Iterable[Union[Type, str]]) -> MetricsFilter
   284      if isinstance(namespaces, str):
   285        raise ValueError('Namespaces must be an iterable, not a string')
   286  
   287      self._namespaces.update([Metrics.get_namespace(ns) for ns in namespaces])
   288      return self
   289  
   290    def with_step(self, step):
   291      # type: (str) -> MetricsFilter
   292      return self.with_steps([step])
   293  
   294    def with_steps(self, steps):
   295      # type: (Iterable[str]) -> MetricsFilter
   296      if isinstance(steps, str):
   297        raise ValueError('Steps must be an iterable, not a string')
   298  
   299      self._steps.update(steps)
   300      return self