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