From bbfb6f67e248b683ff37a7056dbfde45ffabab08 Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 12 Dec 2019 16:41:06 -0800 Subject: [PATCH 01/41] metrics --- .../src/opentelemetry/sdk/metrics/__init__.py | 53 ++++++++++++---- .../sdk/metrics/export/aggregate.py | 46 ++++++++++++++ .../sdk/metrics/export/batcher.py | 60 +++++++++++++++++++ 3 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index bb495bc1be3..aaee1572f01 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -28,6 +28,7 @@ class LabelSet(metrics_api.LabelSet): def __init__(self, labels: Dict[str, str] = None): self.labels = labels + self.encoded = '' class BaseHandle: @@ -36,11 +37,12 @@ def __init__( value_type: Type[metrics_api.ValueT], enabled: bool, monotonic: bool, + aggregator ): - self.data = value_type() self.value_type = value_type self.enabled = enabled self.monotonic = monotonic + self.aggregator = aggregator self.last_update_timestamp = time_ns() def _validate_update(self, value: metrics_api.ValueT) -> bool: @@ -53,6 +55,10 @@ def _validate_update(self, value: metrics_api.ValueT) -> bool: return False return True + def update(self, value: metrics_api.ValueT): + self.last_update_timestamp = time_ns() + self.aggregator.update(value) + def __repr__(self): return '{}(data="{}", last_update_timestamp={})'.format( type(self).__name__, self.data, self.last_update_timestamp @@ -66,8 +72,7 @@ def add(self, value: metrics_api.ValueT) -> None: if self.monotonic and value < 0: logger.warning("Monotonic counter cannot descend.") return - self.last_update_timestamp = time_ns() - self.data += value + self.update(value) class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): @@ -77,8 +82,7 @@ def set(self, value: metrics_api.ValueT) -> None: if self.monotonic and value < self.data: logger.warning("Monotonic gauge cannot descend.") return - self.last_update_timestamp = time_ns() - self.data = value + self.update(value) class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): @@ -88,8 +92,7 @@ def record(self, value: metrics_api.ValueT) -> None: if self.monotonic and value < 0: logger.warning("Monotonic measure cannot accept negatives.") return - self.last_update_timestamp = time_ns() - # TODO: record + self.update(value) class Metric(metrics_api.Metric): @@ -103,6 +106,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], + meter: Meter, label_keys: Sequence[str] = None, enabled: bool = True, monotonic: bool = False, @@ -121,7 +125,8 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: handle = self.handles.get(label_set) if not handle: handle = self.HANDLE_TYPE( - self.value_type, self.enabled, self.monotonic + self.value_type, self.enabled, self.monotonic, + meter.batcher.aggregator_for(self.__class__) ) self.handles[label_set] = handle return handle @@ -150,6 +155,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], + meter: Meter, label_keys: Sequence[str] = None, enabled: bool = True, monotonic: bool = True, @@ -186,6 +192,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], + meter: Meter, label_keys: Sequence[str] = None, enabled: bool = True, monotonic: bool = False, @@ -222,6 +229,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], + meter: Meter, label_keys: Sequence[str] = None, enabled: bool = False, monotonic: bool = False, @@ -242,6 +250,13 @@ def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: UPDATE_FUNCTION = record +class Record: + + def __init__(self, encoded, metric_type, enabled, aggregator): + self.encoded = encoded + self.metric_type = metric_type + self.aggregator = aggregator + # Used when getting a LabelSet with no key/values EMPTY_LABEL_SET = LabelSet() @@ -250,8 +265,18 @@ def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`.""" - def __init__(self): - self.labels = {} + def __init__(self, batcher): + self.label_sets = {} + self.metrics = {} + self.batcher = batcher + + def collect(self): + for metric in metrics: + if metric.enabled: + metric_type = metric.metric_type + for label_set, handle in metrics.handles: + record = Record(label_set.encoded, metric_type, handle.aggregator) + self.batcher.process(record) def record_batch( self, @@ -275,7 +300,7 @@ def create_metric( ) -> metrics_api.MetricT: """See `opentelemetry.metrics.Meter.create_metric`.""" # Ignore type b/c of mypy bug in addition to missing annotations - return metric_type( # type: ignore + metric = metric_type( # type: ignore name, description, unit, @@ -284,6 +309,8 @@ def create_metric( enabled=enabled, monotonic=monotonic, ) + self.metrics.add(metric) + return metric def get_label_set(self, labels: Dict[str, str]): """See `opentelemetry.metrics.Meter.create_metric`. @@ -299,8 +326,8 @@ def get_label_set(self, labels: Dict[str, str]): encoded = tuple(sorted(labels.items())) # If LabelSet exists for this meter in memory, use existing one if encoded not in self.labels: - self.labels[encoded] = LabelSet(labels=labels) - return self.labels[encoded] + self.label_sets[encoded] = LabelSet(labels=labels, encoded=encoded) + return self.label_sets[encoded] meter = Meter() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py new file mode 100644 index 00000000000..553bc2dde1c --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -0,0 +1,46 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc + +class Aggregator(abc.ABC): + + def __init__(self): + self.current = None + + @abc.abstractmethod + def update(self, value): + pass + + @abc.abstractmethod + def checkpoint(self): + pass + +class CounterAggregator(Aggregator): + + def __init__(self): + super().__init__(self) + self.current = 0 + self.check_point = 0 + + def update(self, value): + self.current += value + + def checkpoint(self): + # TODO: Implement lock-free algorithm for concurrency + self.check_point = self.current + self.current = 0 + + def merge(self, aggregator): + self.check_point += aggregator.check_point diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py new file mode 100644 index 00000000000..a95378003c8 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -0,0 +1,60 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc + +from typing import Type +from opentelemetry.metrics import Counter, MetricT +from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator + + +class BatchKey: + + def __init__(self, metric_type, encoded): + self.metric_type = metric_type + self.encoded = encoded + + +class Batcher(abc.ABC): + + def __init__(self, keep_state): + self.batch_map = {} + self.keep_state = keep_state + + def aggregator_for(self, metric_type: Type[MetricT]): + if metric_type == Counter: + return CounterAggregator() + else: + # TODO: Add other aggregators + return CounterAggregator() + + @abc.abstractmethod + def process(self, record): + pass + + +class UngroupedBatcher(Batcher): + + def process(self, record): + record.aggregator.checkpoint() + # TODO: Race case of incoming update at the same time as process + batch_key = BatchKey(record.metric_type, record.encoded) + aggregator = self.batch_map.get(batch_key) + if aggregator: + # Since checkpoints are reset every time process is called, merge + # the accumulated value from the + aggregator.merge(record.aggregator) + else: + + record.aggregator.checkpoint() From da5a9f4802e10db68b649909850ffae3ddbec465 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 17 Dec 2019 13:04:10 -0800 Subject: [PATCH 02/41] implementation --- .../metrics_example.py | 12 ++++-- .../src/opentelemetry/sdk/metrics/__init__.py | 39 ++++++++++--------- .../sdk/metrics/export/__init__.py | 31 +++++++++------ .../sdk/metrics/export/aggregate.py | 4 +- .../sdk/metrics/export/batcher.py | 32 +++++++-------- .../sdk/metrics/export/controller.py | 39 +++++++++++++++++++ 6 files changed, 104 insertions(+), 53 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py index 246d6c3507d..2ea8b370515 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py @@ -18,8 +18,12 @@ from opentelemetry import metrics from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController -metrics.set_preferred_meter_implementation(lambda _: Meter()) +batcher = UngroupedBatcher(True) +metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) meter = metrics.meter() counter = meter.create_metric( "available memory", @@ -27,7 +31,7 @@ "bytes", int, Counter, - ("environment",), + ("environment",) ) label_set = meter.get_label_set({"environment": "staging"}) @@ -41,6 +45,6 @@ # Record batch usage meter.record_batch(label_set, [(counter, 50)]) -print(counter_handle.data) -# TODO: exporters +exporter = ConsoleMetricsExporter() +controller = PushController(meter, exporter, 5) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index aaee1572f01..403a8848c34 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -26,9 +26,9 @@ class LabelSet(metrics_api.LabelSet): """See `opentelemetry.metrics.LabelSet.""" - def __init__(self, labels: Dict[str, str] = None): + def __init__(self, labels: Dict[str, str] = None, encoded=''): self.labels = labels - self.encoded = '' + self.encoded = encoded class BaseHandle: @@ -106,7 +106,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - meter: Meter, + meter: "Meter", label_keys: Sequence[str] = None, enabled: bool = True, monotonic: bool = False, @@ -115,6 +115,7 @@ def __init__( self.description = description self.unit = unit self.value_type = value_type + self.meter = meter self.label_keys = label_keys self.enabled = enabled self.monotonic = monotonic @@ -126,7 +127,7 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: if not handle: handle = self.HANDLE_TYPE( self.value_type, self.enabled, self.monotonic, - meter.batcher.aggregator_for(self.__class__) + self.meter.batcher.aggregator_for(self.__class__) ) self.handles[label_set] = handle return handle @@ -155,7 +156,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - meter: Meter, + meter: "Meter", label_keys: Sequence[str] = None, enabled: bool = True, monotonic: bool = True, @@ -165,6 +166,7 @@ def __init__( description, unit, value_type, + meter, label_keys=label_keys, enabled=enabled, monotonic=monotonic, @@ -192,7 +194,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - meter: Meter, + meter: "Meter", label_keys: Sequence[str] = None, enabled: bool = True, monotonic: bool = False, @@ -202,6 +204,7 @@ def __init__( description, unit, value_type, + meter, label_keys=label_keys, enabled=enabled, monotonic=monotonic, @@ -229,7 +232,7 @@ def __init__( description: str, unit: str, value_type: Type[metrics_api.ValueT], - meter: Meter, + meter: "Meter", label_keys: Sequence[str] = None, enabled: bool = False, monotonic: bool = False, @@ -239,6 +242,7 @@ def __init__( description, unit, value_type, + meter, label_keys=label_keys, enabled=enabled, monotonic=monotonic, @@ -252,9 +256,9 @@ def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: class Record: - def __init__(self, encoded, metric_type, enabled, aggregator): - self.encoded = encoded - self.metric_type = metric_type + def __init__(self, metric, label_set, aggregator): + self.metric = metric + self.label_set = label_set self.aggregator = aggregator @@ -267,15 +271,14 @@ class Meter(metrics_api.Meter): def __init__(self, batcher): self.label_sets = {} - self.metrics = {} + self.metrics = set() self.batcher = batcher def collect(self): - for metric in metrics: + for metric in self.metrics: if metric.enabled: - metric_type = metric.metric_type - for label_set, handle in metrics.handles: - record = Record(label_set.encoded, metric_type, handle.aggregator) + for label_set, handle in metric.handles.items(): + record = Record(metric, label_set, handle.aggregator) self.batcher.process(record) def record_batch( @@ -305,6 +308,7 @@ def create_metric( description, unit, value_type, + self, label_keys=label_keys, enabled=enabled, monotonic=monotonic, @@ -325,9 +329,6 @@ def get_label_set(self, labels: Dict[str, str]): # Use simple encoding for now until encoding API is implemented encoded = tuple(sorted(labels.items())) # If LabelSet exists for this meter in memory, use existing one - if encoded not in self.labels: + if encoded not in self.label_sets: self.label_sets[encoded] = LabelSet(labels=labels, encoded=encoded) return self.label_sets[encoded] - - -meter = Meter() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index b6cb396331a..e5c7376e09b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -15,7 +15,7 @@ from enum import Enum from typing import Sequence, Tuple -from .. import Metric +from opentelemetry.sdk.metrics import Metric, LabelSet class MetricsExportResult(Enum): @@ -24,6 +24,14 @@ class MetricsExportResult(Enum): FAILED_NOT_RETRYABLE = 2 +class MetricRecord: + + def __init__(self, aggregator, label_set, metric): + self.aggregator = aggregator + self.label_set = label_set + self.metric = metric + + class MetricsExporter: """Interface for exporting metrics. @@ -32,15 +40,15 @@ class MetricsExporter: """ def export( - self, metric_tuples: Sequence[Tuple[Metric, Sequence[str]]] + self, metric_records: Sequence[MetricRecord] ) -> "MetricsExportResult": """Exports a batch of telemetry data. Args: - metric_tuples: A sequence of metric pairs. A metric pair consists - of a `Metric` and a sequence of strings. The sequence of - strings will be used to get the corresponding `MetricHandle` - from the `Metric` to export. + metric_records: A sequence of `MetricRecord`. A `MetricRecord` + contains the metric to be exported, the label set associated + with that metric, as well as the aggregator used to export the + current checkpointed value. Returns: The result of the export @@ -57,17 +65,16 @@ class ConsoleMetricsExporter(MetricsExporter): """Implementation of `MetricsExporter` that prints metrics to the console. This class can be used for diagnostic purposes. It prints the exported - metric handles to the console STDOUT. + metrics to the console STDOUT. """ def export( - self, metric_tuples: Sequence[Tuple[Metric, Sequence[str]]] + self, metric_records: Sequence[MetricRecord] ) -> "MetricsExportResult": - for metric, label_values in metric_tuples: - handle = metric.get_handle(label_values) + for record in metric_records: print( - '{}(data="{}", label_values="{}", metric_data={})'.format( - type(self).__name__, metric, label_values, handle + '{}(data="{}", label_set="{}", value={})'.format( + type(self).__name__, record.metric, record.label_set.labels, record.aggregator.check_point ) ) return MetricsExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 553bc2dde1c..edc0241a883 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -18,6 +18,7 @@ class Aggregator(abc.ABC): def __init__(self): self.current = None + self.check_point = None @abc.abstractmethod def update(self, value): @@ -30,7 +31,7 @@ def checkpoint(self): class CounterAggregator(Aggregator): def __init__(self): - super().__init__(self) + super().__init__() self.current = 0 self.check_point = 0 @@ -40,7 +41,6 @@ def update(self, value): def checkpoint(self): # TODO: Implement lock-free algorithm for concurrency self.check_point = self.current - self.current = 0 def merge(self, aggregator): self.check_point += aggregator.check_point diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index a95378003c8..0b727b647b9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -16,16 +16,10 @@ from typing import Type from opentelemetry.metrics import Counter, MetricT +from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator -class BatchKey: - - def __init__(self, metric_type, encoded): - self.metric_type = metric_type - self.encoded = encoded - - class Batcher(abc.ABC): def __init__(self, keep_state): @@ -39,6 +33,16 @@ def aggregator_for(self, metric_type: Type[MetricT]): # TODO: Add other aggregators return CounterAggregator() + def check_point_set(self): + metric_records = [] + for key, value in self.batch_map.items(): + metric_records.append(MetricRecord(value[0], value[1], key[0])) + return metric_records + + def finished_collection(self): + if not self.keep_state: + self.batch_map = {} + @abc.abstractmethod def process(self, record): pass @@ -47,14 +51,10 @@ def process(self, record): class UngroupedBatcher(Batcher): def process(self, record): + # checkpoints the current aggregator value to be collected for export record.aggregator.checkpoint() # TODO: Race case of incoming update at the same time as process - batch_key = BatchKey(record.metric_type, record.encoded) - aggregator = self.batch_map.get(batch_key) - if aggregator: - # Since checkpoints are reset every time process is called, merge - # the accumulated value from the - aggregator.merge(record.aggregator) - else: - - record.aggregator.checkpoint() + batch_key = (record.metric, record.label_set.encoded) + batch_value = self.batch_map.get(batch_key) + if not batch_value: + self.batch_map[batch_key] = (record.aggregator, record.label_set) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py new file mode 100644 index 00000000000..7d12c7add2b --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -0,0 +1,39 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading + +class PushController(threading.Thread): + + def __init__(self, meter, exporter, interval): + super().__init__() + self.meter = meter + self.exporter = exporter + self.interval = interval + self.finished = threading.Event() + self.start() + + def run(self): + while not self.finished.wait(self.interval): + self.tick() + + def cancel(self): + self.finished.set() + + def tick(self): + # Collect all of the meter's metrics to be exported + self.meter.collect() + # Export the given metrics in the batcher + self.exporter.export(self.meter.batcher.check_point_set()) + self.meter.batcher.finished_collection() \ No newline at end of file From c783e481550a991697d89da643327147b6e6b43e Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 17 Dec 2019 14:53:00 -0800 Subject: [PATCH 03/41] comments --- .../src/opentelemetry/metrics/__init__.py | 4 +- .../src/opentelemetry/sdk/metrics/__init__.py | 92 +++++++++++++------ .../sdk/metrics/export/__init__.py | 4 +- .../sdk/metrics/export/aggregate.py | 8 +- .../sdk/metrics/export/batcher.py | 49 ++++++++-- .../sdk/metrics/export/controller.py | 8 +- 6 files changed, 120 insertions(+), 45 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 465020606d2..1d0b06abe54 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -220,7 +220,7 @@ def create_metric( metric_type: Type[MetricT], label_keys: Sequence[str] = None, enabled: bool = True, - monotonic: bool = False, + alternate: bool = False, ) -> "Metric": """Creates a ``metric_kind`` metric with type ``value_type``. @@ -232,7 +232,7 @@ def create_metric( metric_type: The type of metric being created. label_keys: The keys for the labels with dynamic values. enabled: Whether to report the metric by default. - monotonic: Whether to only allow non-negative values. + alternate: Whether to only allow non-negative values. Returns: A new ``metric_type`` metric with values of ``value_type``. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 403a8848c34..770ac9c75e1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -17,6 +17,8 @@ from typing import Dict, Sequence, Tuple, Type from opentelemetry import metrics as metrics_api +from opentelemetry.sdk.metrics.export.aggregate import Aggregator +from opentelemetry.sdk.metrics.export.batcher import Batcher from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -26,22 +28,34 @@ class LabelSet(metrics_api.LabelSet): """See `opentelemetry.metrics.LabelSet.""" - def __init__(self, labels: Dict[str, str] = None, encoded=''): + def __init__(self, labels: Dict[str, str] = None, encoded=""): self.labels = labels self.encoded = encoded class BaseHandle: + """The base handle class containing common behavior for all handles. + + Handles are responsible for operating on data for metric instruments for a + specific set of labels. + + Args: + value_type: The type of values this handle holds (int, float). + enabled: True if the originating instrument is enabled. + alternate: Indicates applying default behavior for updating values. + aggregator: The aggregator for this handle. Will handle aggregation + upon updates and checkpointing of values for exporting. + """ def __init__( self, value_type: Type[metrics_api.ValueT], enabled: bool, - monotonic: bool, - aggregator + alternate: bool, + aggregator: Aggregator ): self.value_type = value_type self.enabled = enabled - self.monotonic = monotonic + self.alternate = alternate self.aggregator = aggregator self.last_update_timestamp = time_ns() @@ -69,7 +83,7 @@ class CounterHandle(metrics_api.CounterHandle, BaseHandle): def add(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.CounterHandle.add`.""" if self._validate_update(value): - if self.monotonic and value < 0: + if self.alternate and value < 0: logger.warning("Monotonic counter cannot descend.") return self.update(value) @@ -79,7 +93,7 @@ class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): def set(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.GaugeHandle.set`.""" if self._validate_update(value): - if self.monotonic and value < self.data: + if self.alternate and value < self.data: logger.warning("Monotonic gauge cannot descend.") return self.update(value) @@ -89,15 +103,20 @@ class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): def record(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.MeasureHandle.record`.""" if self._validate_update(value): - if self.monotonic and value < 0: - logger.warning("Monotonic measure cannot accept negatives.") + if self.alternate and value < 0: + logger.warning("Absolute measure cannot accept negatives.") return self.update(value) class Metric(metrics_api.Metric): - """See `opentelemetry.metrics.Metric`.""" + """Base class for all metric types. + Also known as `metric instrument`. This is the class that is used to + represent a metric that is to be continuously recorded and tracked. Each + metric has a set of handles that are created from the metric. See + `BaseHandle` for information on handles. + """ HANDLE_TYPE = BaseHandle def __init__( @@ -109,7 +128,7 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = None, enabled: bool = True, - monotonic: bool = False, + alternate: bool = False, ): self.name = name self.description = description @@ -118,7 +137,7 @@ def __init__( self.meter = meter self.label_keys = label_keys self.enabled = enabled - self.monotonic = monotonic + self.alternate = alternate self.handles = {} def get_handle(self, label_set: LabelSet) -> BaseHandle: @@ -126,7 +145,8 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: handle = self.handles.get(label_set) if not handle: handle = self.HANDLE_TYPE( - self.value_type, self.enabled, self.monotonic, + self.value_type, self.enabled, self.alternate, + # Aggregator will be created based off type of metric self.meter.batcher.aggregator_for(self.__class__) ) self.handles[label_set] = handle @@ -159,7 +179,7 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = None, enabled: bool = True, - monotonic: bool = True, + alternate: bool = True, ): super().__init__( name, @@ -169,7 +189,7 @@ def __init__( meter, label_keys=label_keys, enabled=enabled, - monotonic=monotonic, + alternate=alternate, ) def add(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: @@ -197,7 +217,7 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = None, enabled: bool = True, - monotonic: bool = False, + alternate: bool = False, ): super().__init__( name, @@ -207,7 +227,7 @@ def __init__( meter, label_keys=label_keys, enabled=enabled, - monotonic=monotonic, + alternate=alternate, ) def set(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: @@ -221,7 +241,7 @@ class Measure(Metric, metrics_api.Measure): """See `opentelemetry.metrics.Measure`. By default, measure metrics can accept both positive and negatives. - Negative inputs will be discarded when monotonic is True. + Negative inputs will be discarded when alternate is True. """ HANDLE_TYPE = MeasureHandle @@ -235,7 +255,7 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = None, enabled: bool = False, - monotonic: bool = False, + alternate: bool = False ): super().__init__( name, @@ -245,7 +265,7 @@ def __init__( meter, label_keys=label_keys, enabled=enabled, - monotonic=monotonic, + alternate=alternate, ) def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: @@ -255,8 +275,13 @@ def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: UPDATE_FUNCTION = record class Record: + """Container class used for processing in the `Batcher`""" - def __init__(self, metric, label_set, aggregator): + def __init__(self, + metric: Metric, + label_set: LabelSet, + aggregator: Aggregator + ): self.metric = metric self.label_set = label_set self.aggregator = aggregator @@ -267,18 +292,31 @@ def __init__(self, metric, label_set, aggregator): class Meter(metrics_api.Meter): - """See `opentelemetry.metrics.Meter`.""" - - def __init__(self, batcher): + """See `opentelemetry.metrics.Meter`. + + Args: + batcher: The `Batcher` used for this meter. + """ + def __init__(self, batcher: Batcher): + self.batcher = batcher + # label_sets is a map of the labelset's encoded value to the label set self.label_sets = {} self.metrics = set() - self.batcher = batcher - def collect(self): + def collect(self) -> None: + """Collects all the metrics created with this `Meter` for export. + + Utilizes the batcher to create checkpoints of the current values in + each aggregator belonging to the metrics that were created with this + meter instance. + """ for metric in self.metrics: if metric.enabled: for label_set, handle in metric.handles.items(): + # Consider storing records in memory? record = Record(metric, label_set, handle.aggregator) + # Checkpoints the current aggregators + # Applies different batching logic based on type of batcher self.batcher.process(record) def record_batch( @@ -299,7 +337,7 @@ def create_metric( metric_type: Type[metrics_api.MetricT], label_keys: Sequence[str] = None, enabled: bool = True, - monotonic: bool = False, + alternate: bool = False, ) -> metrics_api.MetricT: """See `opentelemetry.metrics.Meter.create_metric`.""" # Ignore type b/c of mypy bug in addition to missing annotations @@ -311,7 +349,7 @@ def create_metric( self, label_keys=label_keys, enabled=enabled, - monotonic=monotonic, + alternate=alternate, ) self.metrics.add(metric) return metric diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index e5c7376e09b..2af7dfb305d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -15,8 +15,6 @@ from enum import Enum from typing import Sequence, Tuple -from opentelemetry.sdk.metrics import Metric, LabelSet - class MetricsExportResult(Enum): SUCCESS = 0 @@ -45,7 +43,7 @@ def export( """Exports a batch of telemetry data. Args: - metric_records: A sequence of `MetricRecord`. A `MetricRecord` + metric_records: A sequence of `MetricRecord` s. A `MetricRecord` contains the metric to be exported, the label set associated with that metric, as well as the aggregator used to export the current checkpointed value. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index edc0241a883..f3979f93a3c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -15,6 +15,11 @@ import abc class Aggregator(abc.ABC): + """Base class for aggregators. + + Aggregators are responsible for holding aggregated values and taking a + snapshot of these values upon export (check_point). + """ def __init__(self): self.current = None @@ -29,6 +34,7 @@ def checkpoint(self): pass class CounterAggregator(Aggregator): + """Aggregator for `Counter` metrics.""" def __init__(self): super().__init__() @@ -42,5 +48,3 @@ def checkpoint(self): # TODO: Implement lock-free algorithm for concurrency self.check_point = self.current - def merge(self, aggregator): - self.check_point += aggregator.check_point diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 0b727b647b9..6d73d255a4d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -14,46 +14,75 @@ import abc -from typing import Type +from typing import Sequence, Type from opentelemetry.metrics import Counter, MetricT +from opentelemetry.sdk.metrics import Record from opentelemetry.sdk.metrics.export import MetricRecord -from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator +from opentelemetry.sdk.metrics \ + .export.aggregate import Aggregator, CounterAggregator class Batcher(abc.ABC): + """Base class for all batcher types. - def __init__(self, keep_state): + The batcher is responsible for storing the aggregators and aggregated + received from updates from metrics in the meter. The stored values will be + sent to an exporter for exporting. + """ + def __init__(self, keep_state: bool): self.batch_map = {} + # keep_state=True indicates the batcher computes checkpoints from over + # the process lifetime. False indicates the batcher computes + # checkpoints which descrive the updates of a single collection period + # (deltas) self.keep_state = keep_state - def aggregator_for(self, metric_type: Type[MetricT]): + def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: + """Returns an aggregator based off metric type. + + Aggregators keep track of and updates values when metrics get updated. + """ if metric_type == Counter: return CounterAggregator() else: # TODO: Add other aggregators return CounterAggregator() - def check_point_set(self): + def check_point_set(self) -> Sequence[MetricRecord]: + """Returns a list of `MetricRecord` s used for exporting. + + The list of `MetricRecord` s is a snapshot created from the current + data in all of the aggregators in this batcher. + """ metric_records = [] for key, value in self.batch_map.items(): metric_records.append(MetricRecord(value[0], value[1], key[0])) return metric_records def finished_collection(self): + """Performs certain post-export logic. + + For batchers that are stateless, resets the batch map. + """ if not self.keep_state: self.batch_map = {} @abc.abstractmethod - def process(self, record): - pass + def process(self, record: Record) -> None: + """Stores record information to be ready for exporting. + + Depending on type of batcher, performs pre-export logic, such as + filtering records based off of keys. + """ class UngroupedBatcher(Batcher): + """Accepts all records and passes them for exporting""" - def process(self, record): - # checkpoints the current aggregator value to be collected for export - record.aggregator.checkpoint() + def process(self, record: Record): # TODO: Race case of incoming update at the same time as process + # Checkpoints the current aggregator value to be collected for export + record.aggregator.checkpoint() batch_key = (record.metric, record.label_set.encoded) batch_value = self.batch_map.get(batch_key) if not batch_value: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 7d12c7add2b..1a315fbc902 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -15,6 +15,11 @@ import threading class PushController(threading.Thread): + """A push based controller, used for exporting. + + Uses a worker thread that periodically collects metrics for exporting, + exports them and performs some post-processing. + """ def __init__(self, meter, exporter, interval): super().__init__() @@ -36,4 +41,5 @@ def tick(self): self.meter.collect() # Export the given metrics in the batcher self.exporter.export(self.meter.batcher.check_point_set()) - self.meter.batcher.finished_collection() \ No newline at end of file + # Perform post-exporting logic based on batcher configuration + self.meter.batcher.finished_collection() From 88bdc576799286a57c15d57e5dac0414dedecbc5 Mon Sep 17 00:00:00 2001 From: Leighton Date: Wed, 18 Dec 2019 11:21:40 -0800 Subject: [PATCH 04/41] fix tests --- .../src/opentelemetry/sdk/metrics/__init__.py | 10 +- .../sdk/metrics/export/batcher.py | 5 +- .../tests/metrics/export/test_export.py | 13 ++- .../tests/metrics/test_metrics.py | 98 +++++++++++-------- 4 files changed, 71 insertions(+), 55 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 770ac9c75e1..3958814e3da 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -18,7 +18,7 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export.aggregate import Aggregator -from opentelemetry.sdk.metrics.export.batcher import Batcher +from opentelemetry.sdk.metrics.export.batcher import Batcher, UngroupedBatcher from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -75,7 +75,7 @@ def update(self, value: metrics_api.ValueT): def __repr__(self): return '{}(data="{}", last_update_timestamp={})'.format( - type(self).__name__, self.data, self.last_update_timestamp + type(self).__name__, self.aggregator.current, self.last_update_timestamp ) @@ -93,7 +93,7 @@ class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): def set(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.GaugeHandle.set`.""" if self._validate_update(value): - if self.alternate and value < self.data: + if self.alternate and value < self.aggregator.current: logger.warning("Monotonic gauge cannot descend.") return self.update(value) @@ -254,7 +254,7 @@ def __init__( value_type: Type[metrics_api.ValueT], meter: "Meter", label_keys: Sequence[str] = None, - enabled: bool = False, + enabled: bool = True, alternate: bool = False ): super().__init__( @@ -297,7 +297,7 @@ class Meter(metrics_api.Meter): Args: batcher: The `Batcher` used for this meter. """ - def __init__(self, batcher: Batcher): + def __init__(self, batcher: Batcher = UngroupedBatcher(True)): self.batcher = batcher # label_sets is a map of the labelset's encoded value to the label set self.label_sets = {} diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 6d73d255a4d..d2ce26d674e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -16,7 +16,6 @@ from typing import Sequence, Type from opentelemetry.metrics import Counter, MetricT -from opentelemetry.sdk.metrics import Record from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics \ .export.aggregate import Aggregator, CounterAggregator @@ -68,7 +67,7 @@ def finished_collection(self): self.batch_map = {} @abc.abstractmethod - def process(self, record: Record) -> None: + def process(self, record: "Record") -> None: """Stores record information to be ready for exporting. Depending on type of batcher, performs pre-export logic, such as @@ -79,7 +78,7 @@ def process(self, record: Record) -> None: class UngroupedBatcher(Batcher): """Accepts all records and passes them for exporting""" - def process(self, record: Record): + def process(self, record: "Record"): # TODO: Race case of incoming update at the same time as process # Checkpoints the current aggregator value to be collected for export record.aggregator.checkpoint() diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 4d8e6df8575..839dc035b7c 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -16,7 +16,9 @@ from unittest import mock from opentelemetry.sdk import metrics -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics \ + .export import ConsoleMetricsExporter, MetricRecord +from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator class TestConsoleMetricsExporter(unittest.TestCase): @@ -34,10 +36,11 @@ def test_export(self): ) kvp = {"environment": "staging"} label_set = meter.get_label_set(kvp) - handle = metric.get_handle(label_set) - result = '{}(data="{}", label_values="{}", metric_data={})'.format( - ConsoleMetricsExporter.__name__, metric, label_set, handle + aggregator = CounterAggregator() + record = MetricRecord(aggregator, label_set, metric) + result = '{}(data="{}", label_set="{}", value={})'.format( + ConsoleMetricsExporter.__name__, metric, label_set.labels, aggregator.check_point ) with mock.patch("sys.stdout") as mock_stdout: - exporter.export([(metric, label_set)]) + exporter.export([record]) mock_stdout.write.assert_any_call(result) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 81e6dd2c9d5..7f5c115651d 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -17,6 +17,7 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk import metrics +from opentelemetry.sdk.metrics import export class TestMeter(unittest.TestCase): @@ -34,7 +35,7 @@ def test_record_batch(self): label_set = meter.get_label_set(kvp) record_tuples = [(counter, 1.0)] meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.get_handle(label_set).data, 1.0) + self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) def test_record_batch_multiple(self): meter = metrics.Meter() @@ -44,15 +45,16 @@ def test_record_batch_multiple(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - gauge = metrics.Gauge("name", "desc", "unit", int, label_keys) + gauge = metrics.Gauge("name", "desc", "unit", int, meter, label_keys) measure = metrics.Measure( "name", "desc", "unit", float, meter, label_keys ) record_tuples = [(counter, 1.0), (gauge, 5), (measure, 3.0)] meter.record_batch(label_set, record_tuples) - self.assertEqual(counter.get_handle(label_set).data, 1.0) - self.assertEqual(gauge.get_handle(label_set).data, 5) - self.assertEqual(measure.get_handle(label_set).data, 0) + self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) + self.assertEqual(gauge.get_handle(label_set).aggregator.current, 5.0) + # TODO: Fix when aggregator implemented for measure + self.assertEqual(measure.get_handle(label_set).aggregator.current, 3.0) def test_record_batch_exists(self): meter = metrics.Meter() @@ -67,7 +69,7 @@ def test_record_batch_exists(self): record_tuples = [(counter, 1.0)] meter.record_batch(label_set, record_tuples) self.assertEqual(counter.get_handle(label_set), handle) - self.assertEqual(handle.data, 2.0) + self.assertEqual(handle.aggregator.current, 2.0) def test_create_metric(self): meter = metrics.Meter() @@ -101,7 +103,7 @@ def test_get_label_set(self): kvp = {"environment": "staging", "a": "z"} label_set = meter.get_label_set(kvp) encoded = tuple(sorted(kvp.items())) - self.assertIs(meter.labels[encoded], label_set) + self.assertIs(meter.label_sets[encoded], label_set) def test_get_label_set_empty(self): meter = metrics.Meter() @@ -132,114 +134,126 @@ def test_get_handle(self): class TestCounter(unittest.TestCase): def test_add(self): meter = metrics.Meter() - metric = metrics.Counter("name", "desc", "unit", int, ("key",)) + metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) metric.add(label_set, 3) metric.add(label_set, 2) - self.assertEqual(handle.data, 5) + self.assertEqual(handle.aggregator.current, 5) class TestGauge(unittest.TestCase): def test_set(self): meter = metrics.Meter() - metric = metrics.Gauge("name", "desc", "unit", int, ("key",)) + metric = metrics.Gauge("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) metric.set(label_set, 3) - self.assertEqual(handle.data, 3) + self.assertEqual(handle.aggregator.current, 3) metric.set(label_set, 2) - self.assertEqual(handle.data, 2) + # TODO: Fix once other aggregators implemented + self.assertEqual(handle.aggregator.current, 5) class TestMeasure(unittest.TestCase): def test_record(self): meter = metrics.Meter() - metric = metrics.Measure("name", "desc", "unit", int, ("key",)) + metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) metric.record(label_set, 3) - # Record not implemented yet - self.assertEqual(handle.data, 0) + # TODO: Fix once other aggregators implemented + self.assertEqual(handle.aggregator.current, 3) class TestCounterHandle(unittest.TestCase): def test_add(self): - handle = metrics.CounterHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, True, False, aggregator) handle.add(3) - self.assertEqual(handle.data, 3) + self.assertEqual(handle.aggregator.current, 3) def test_add_disabled(self): - handle = metrics.CounterHandle(int, False, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, False, False, aggregator) handle.add(3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_monotonic(self, logger_mock): - handle = metrics.CounterHandle(int, True, True) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, True, True, aggregator) handle.add(-3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_incorrect_type(self, logger_mock): - handle = metrics.CounterHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, True, False, aggregator) handle.add(3.0) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) - +# TODO: fix tests once aggregator implemented class TestGaugeHandle(unittest.TestCase): def test_set(self): - handle = metrics.GaugeHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, True, False, aggregator) handle.set(3) - self.assertEqual(handle.data, 3) + self.assertEqual(handle.aggregator.current, 3) def test_set_disabled(self): - handle = metrics.GaugeHandle(int, False, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, False, False, aggregator) handle.set(3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_set_monotonic(self, logger_mock): - handle = metrics.GaugeHandle(int, True, True) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, True, True, aggregator) handle.set(-3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @mock.patch("opentelemetry.sdk.metrics.logger") def test_set_incorrect_type(self, logger_mock): - handle = metrics.GaugeHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, True, False, aggregator) handle.set(3.0) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) - +# TODO: fix tests once aggregator implemented class TestMeasureHandle(unittest.TestCase): def test_record(self): - handle = metrics.MeasureHandle(int, False, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, False, False, aggregator) handle.record(3) - # Record not implemented yet - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) def test_record_disabled(self): - handle = metrics.MeasureHandle(int, False, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, False, False, aggregator) handle.record(3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_monotonic(self, logger_mock): - handle = metrics.MeasureHandle(int, True, True) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, True, True, aggregator) handle.record(-3) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): - handle = metrics.MeasureHandle(int, True, False) + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, True, False, aggregator) handle.record(3.0) - self.assertEqual(handle.data, 0) + self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) From ea77627e7fa9b8223b3f09b17b1362e110ef2969 Mon Sep 17 00:00:00 2001 From: Leighton Date: Wed, 18 Dec 2019 15:14:08 -0800 Subject: [PATCH 05/41] tests --- .../sdk/metrics/export/aggregate.py | 1 - .../tests/metrics/export/test_export.py | 8 +++ .../tests/metrics/test_metrics.py | 66 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index f3979f93a3c..bc79456e2d4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -47,4 +47,3 @@ def update(self, value): def checkpoint(self): # TODO: Implement lock-free algorithm for concurrency self.check_point = self.current - diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 839dc035b7c..70cafaf7392 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -19,6 +19,7 @@ from opentelemetry.sdk.metrics \ .export import ConsoleMetricsExporter, MetricRecord from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher class TestConsoleMetricsExporter(unittest.TestCase): @@ -44,3 +45,10 @@ def test_export(self): with mock.patch("sys.stdout") as mock_stdout: exporter.export([record]) mock_stdout.write.assert_any_call(result) + + +class TestBatcher(unittest.TestCase): + def test_aggregator_for(self): + batcher = UngroupedBatcher(True) + + diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 7f5c115651d..199acf5b452 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -25,6 +25,43 @@ def test_extends_api(self): meter = metrics.Meter() self.assertIsInstance(meter, metrics_api.Meter) + def test_collect(self): + meter = metrics.Meter() + batcher_mock = mock.Mock() + meter.batcher = batcher_mock + label_keys = ("key1",) + counter = metrics.Counter( + "name", "desc", "unit", float, meter, label_keys + ) + kvp = {"key1": "value1"} + label_set = meter.get_label_set(kvp) + counter.add(label_set, 1.0) + meter.metrics.add(counter) + meter.collect() + self.assertTrue(batcher_mock.process.called) + + def test_collect_no_metrics(self): + meter = metrics.Meter() + batcher_mock = mock.Mock() + meter.batcher = batcher_mock + meter.collect() + self.assertFalse(batcher_mock.process.called) + + def test_collect_disabled_metric(self): + meter = metrics.Meter() + batcher_mock = mock.Mock() + meter.batcher = batcher_mock + label_keys = ("key1",) + counter = metrics.Counter( + "name", "desc", "unit", float, meter, label_keys, False + ) + kvp = {"key1": "value1"} + label_set = meter.get_label_set(kvp) + counter.add(label_set, 1.0) + meter.metrics.add(counter) + meter.collect() + self.assertFalse(batcher_mock.process.called) + def test_record_batch(self): meter = metrics.Meter() label_keys = ("key1",) @@ -198,6 +235,16 @@ def test_add_incorrect_type(self, logger_mock): self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) + @mock.patch("opentelemetry.sdk.metrics.time_ns") + def test_update(self, time_mock): + aggregator = export.aggregate.CounterAggregator() + handle = metrics.CounterHandle(int, True, False, aggregator) + time_mock.return_value = 123 + handle.update(4.0) + self.assertEqual(handle.last_update_timestamp, 123) + self.assertEqual(handle.aggregator.current, 4.0) + + # TODO: fix tests once aggregator implemented class TestGaugeHandle(unittest.TestCase): def test_set(self): @@ -228,6 +275,16 @@ def test_set_incorrect_type(self, logger_mock): self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) + @mock.patch("opentelemetry.sdk.metrics.time_ns") + def test_update(self, time_mock): + aggregator = export.aggregate.CounterAggregator() + handle = metrics.GaugeHandle(int, True, False, aggregator) + time_mock.return_value = 123 + handle.update(4.0) + self.assertEqual(handle.last_update_timestamp, 123) + self.assertEqual(handle.aggregator.current, 4.0) + + # TODO: fix tests once aggregator implemented class TestMeasureHandle(unittest.TestCase): def test_record(self): @@ -257,3 +314,12 @@ def test_record_incorrect_type(self, logger_mock): handle.record(3.0) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) + + @mock.patch("opentelemetry.sdk.metrics.time_ns") + def test_update(self, time_mock): + aggregator = export.aggregate.CounterAggregator() + handle = metrics.MeasureHandle(int, True, False, aggregator) + time_mock.return_value = 123 + handle.update(4.0) + self.assertEqual(handle.last_update_timestamp, 123) + self.assertEqual(handle.aggregator.current, 4.0) From b8e4aed8c874b8ecae1bc4ea4d4bcd4b7d7970d9 Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 19 Dec 2019 14:11:56 -0800 Subject: [PATCH 06/41] fix stateful logic --- .../sdk/metrics/export/aggregate.py | 17 ++++++++++++++--- .../opentelemetry/sdk/metrics/export/batcher.py | 15 ++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index bc79456e2d4..55962267499 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -13,6 +13,7 @@ # limitations under the License. import abc +import threading class Aggregator(abc.ABC): """Base class for aggregators. @@ -24,14 +25,19 @@ class Aggregator(abc.ABC): def __init__(self): self.current = None self.check_point = None + self._lock = threading.Lock() @abc.abstractmethod def update(self, value): - pass + """Updates the current with the new value.""" @abc.abstractmethod def checkpoint(self): - pass + """Stores a snapshot of the current value.""" + + @abc.abstractmethod + def merge(self, other): + """Combines two aggregator values.""" class CounterAggregator(Aggregator): """Aggregator for `Counter` metrics.""" @@ -46,4 +52,9 @@ def update(self, value): def checkpoint(self): # TODO: Implement lock-free algorithm for concurrency - self.check_point = self.current + with self._lock: + self.check_point = self.current + self.current = 0 + + def merge(self, other): + self.check_point += other.check_point diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index d2ce26d674e..e68d80f1526 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -79,10 +79,19 @@ class UngroupedBatcher(Batcher): """Accepts all records and passes them for exporting""" def process(self, record: "Record"): - # TODO: Race case of incoming update at the same time as process # Checkpoints the current aggregator value to be collected for export record.aggregator.checkpoint() batch_key = (record.metric, record.label_set.encoded) batch_value = self.batch_map.get(batch_key) - if not batch_value: - self.batch_map[batch_key] = (record.aggregator, record.label_set) + aggregator = record.aggregator + if batch_value: + # Update the stored checkpointed value if exists. This is for cases + # when an update comes at the same time as a checkpoint call + batch_value[0].merge(aggregator) + return + if self.keep_state: + # if stateful batcher, create a copy of the aggregator and update + # it with the current checkpointed value of long-term storage + aggregator = self.aggregator_for(record.metric.__class__) + aggregator.merge(record.aggregator) + self.batch_map[batch_key] = (aggregator, record.label_set) From bb814ec0ebd1fde216cab18a02aa6b1a358286c8 Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 19 Dec 2019 15:35:19 -0800 Subject: [PATCH 07/41] batcher tests --- .../sdk/metrics/export/batcher.py | 2 +- .../tests/metrics/export/test_export.py | 147 +++++++++++++++++- 2 files changed, 146 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index e68d80f1526..711d07a89e7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -91,7 +91,7 @@ def process(self, record: "Record"): return if self.keep_state: # if stateful batcher, create a copy of the aggregator and update - # it with the current checkpointed value of long-term storage + # it with the current checkpointed value for long-term storage aggregator = self.aggregator_for(record.metric.__class__) aggregator.merge(record.aggregator) self.batch_map[batch_key] = (aggregator, record.label_set) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 70cafaf7392..06609f6c527 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -48,7 +48,150 @@ def test_export(self): class TestBatcher(unittest.TestCase): - def test_aggregator_for(self): + def test_aggregator_for_counter(self): batcher = UngroupedBatcher(True) - + self.assertTrue(isinstance(batcher.aggregator_for(metrics.Counter), + CounterAggregator)) + # TODO: Add other aggregator tests + def test_check_point_set(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + aggregator.update(1.0) + label_set = {} + batch_map = {} + batch_map[(metric, "")] = (aggregator, label_set) + batcher.batch_map = batch_map + records = batcher.check_point_set() + self.assertEqual(len(records), 1) + self.assertEqual(records[0].metric, metric) + self.assertEqual(records[0].label_set, label_set) + self.assertEqual(records[0].aggregator, aggregator) + + def test_check_point_set_empty(self): + batcher = UngroupedBatcher(True) + records = batcher.check_point_set() + self.assertEqual(len(records), 0) + + def test_finished_collection_stateless(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(False) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + aggregator.update(1.0) + label_set = {} + batch_map = {} + batch_map[(metric, "")] = (aggregator, label_set) + batcher.batch_map = batch_map + batcher.finished_collection() + self.assertEqual(len(batcher.batch_map), 0) + + def test_finished_collection_stateful(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + aggregator.update(1.0) + label_set = {} + batch_map = {} + batch_map[(metric, "")] = (aggregator, label_set) + batcher.batch_map = batch_map + batcher.finished_collection() + self.assertEqual(len(batcher.batch_map), 1) + + def test_ungrouped_batcher_process_exists(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + aggregator2 = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + label_set = metrics.LabelSet() + batch_map = {} + batch_map[(metric, "")] = (aggregator, label_set) + aggregator2.update(1.0) + batcher.batch_map = batch_map + record = metrics.Record(metric, label_set, aggregator2) + batcher.process(record) + self.assertEqual(len(batcher.batch_map), 1) + self.assertIsNotNone(batcher.batch_map.get((metric, ""))) + self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) + self.assertEqual(batcher.batch_map.get((metric, ""))[0].check_point, 1.0) + self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) + + def test_ungrouped_batcher_process_not_exists(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + label_set = metrics.LabelSet() + batch_map = {} + aggregator.update(1.0) + batcher.batch_map = batch_map + record = metrics.Record(metric, label_set, aggregator) + batcher.process(record) + self.assertEqual(len(batcher.batch_map), 1) + self.assertIsNotNone(batcher.batch_map.get((metric, ""))) + self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) + self.assertEqual(batcher.batch_map.get((metric, ""))[0].check_point, 1.0) + self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) + + def test_ungrouped_batcher_process_not_stateful(self): + meter = metrics.Meter() + batcher = UngroupedBatcher(True) + aggregator = CounterAggregator() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + meter, + ("environment",), + ) + label_set = metrics.LabelSet() + batch_map = {} + aggregator.update(1.0) + batcher.batch_map = batch_map + record = metrics.Record(metric, label_set, aggregator) + batcher.process(record) + self.assertEqual(len(batcher.batch_map), 1) + self.assertIsNotNone(batcher.batch_map.get((metric, ""))) + self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) + self.assertEqual(batcher.batch_map.get((metric, ""))[0].check_point, 1.0) + self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) From 2f44e84b2c560727638fd56787f2e95975ca3ac4 Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 19 Dec 2019 15:58:55 -0800 Subject: [PATCH 08/41] test aggregate --- .../tests/metrics/export/test_export.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 06609f6c527..6dabecf753c 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -122,6 +122,7 @@ def test_finished_collection_stateful(self): batcher.finished_collection() self.assertEqual(len(batcher.batch_map), 1) + # TODO: Abstract the logic once other batchers implemented def test_ungrouped_batcher_process_exists(self): meter = metrics.Meter() batcher = UngroupedBatcher(True) @@ -195,3 +196,26 @@ def test_ungrouped_batcher_process_not_stateful(self): self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) self.assertEqual(batcher.batch_map.get((metric, ""))[0].check_point, 1.0) self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) + +class TestAggregator(unittest.TestCase): + # TODO: test other aggregators once implemented + def test_counter_update(self): + counter = CounterAggregator() + counter.update(1.0) + counter.update(2.0) + self.assertEqual(counter.current, 3.0) + + def test_counter_check_point(self): + counter = CounterAggregator() + counter.update(2.0) + counter.checkpoint() + self.assertEqual(counter.current, 0) + self.assertEqual(counter.check_point, 2.0) + + def test_counter_merge(self): + counter = CounterAggregator() + counter2 = CounterAggregator() + counter.check_point = 1.0 + counter2.check_point = 3.0 + counter.merge(counter2) + self.assertEqual(counter.check_point, 4.0) From e7c8eaadc9be66873cd33260f867686ffff6d256 Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 26 Dec 2019 15:15:11 -0800 Subject: [PATCH 09/41] fix lint --- docs/conf.py | 1 + ...telemetry.sdk.metrics.export.aggregate.rst | 7 +++ ...entelemetry.sdk.metrics.export.batcher.rst | 11 +++++ docs/opentelemetry.sdk.metrics.export.rst | 7 +++ docs/opentelemetry.sdk.metrics.rst | 8 ++++ .../src/opentelemetry/sdk/metrics/__init__.py | 26 ++++++----- .../sdk/metrics/export/__init__.py | 6 ++- .../sdk/metrics/export/aggregate.py | 4 +- .../sdk/metrics/export/batcher.py | 11 +++-- .../sdk/metrics/export/controller.py | 3 +- .../src/opentelemetry/sdk/trace/__init__.py | 4 +- .../tests/metrics/export/test_export.py | 45 +++++++++++++++---- 12 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 docs/opentelemetry.sdk.metrics.export.aggregate.rst create mode 100644 docs/opentelemetry.sdk.metrics.export.batcher.rst create mode 100644 docs/opentelemetry.sdk.metrics.export.rst diff --git a/docs/conf.py b/docs/conf.py index aa0b4096bc2..1fb46af3e20 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -77,6 +77,7 @@ ("py:class", "ValueT"), ("py:class", "MetricT"), ("py:class", "typing.Tuple"), + ("py:class", "Record"), ("py:class", "pymongo.monitoring.CommandListener"), ] diff --git a/docs/opentelemetry.sdk.metrics.export.aggregate.rst b/docs/opentelemetry.sdk.metrics.export.aggregate.rst new file mode 100644 index 00000000000..7c9306c6846 --- /dev/null +++ b/docs/opentelemetry.sdk.metrics.export.aggregate.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.metrics.export.aggregate +========================================== + +.. automodule:: opentelemetry.sdk.metrics.export.aggregate + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.sdk.metrics.export.batcher.rst b/docs/opentelemetry.sdk.metrics.export.batcher.rst new file mode 100644 index 00000000000..5dbd1d6e582 --- /dev/null +++ b/docs/opentelemetry.sdk.metrics.export.batcher.rst @@ -0,0 +1,11 @@ +opentelemetry.sdk.metrics.export.batcher +========================================== + +.. toctree:: + + opentelemetry.sdk.metrics.export + +.. automodule:: opentelemetry.sdk.metrics.export.batcher + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.sdk.metrics.export.rst b/docs/opentelemetry.sdk.metrics.export.rst new file mode 100644 index 00000000000..1ae51170e4f --- /dev/null +++ b/docs/opentelemetry.sdk.metrics.export.rst @@ -0,0 +1,7 @@ +opentelemetry.sdk.metrics.export +========================================== + +.. automodule:: opentelemetry.sdk.metrics.export + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/opentelemetry.sdk.metrics.rst b/docs/opentelemetry.sdk.metrics.rst index 6d646c3b15f..ec8687dd2dc 100644 --- a/docs/opentelemetry.sdk.metrics.rst +++ b/docs/opentelemetry.sdk.metrics.rst @@ -1,6 +1,14 @@ opentelemetry.sdk.metrics package ========================================== +Submodules +---------- + +.. toctree:: + + opentelemetry.sdk.metrics.export.aggregate + opentelemetry.sdk.metrics.export.batcher + .. automodule:: opentelemetry.sdk.metrics :members: :undoc-members: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 2d49118f57a..d5322d52f8d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -46,12 +46,13 @@ class BaseHandle: aggregator: The aggregator for this handle. Will handle aggregation upon updates and checkpointing of values for exporting. """ + def __init__( self, value_type: Type[metrics_api.ValueT], enabled: bool, alternate: bool, - aggregator: Aggregator + aggregator: Aggregator, ): self.value_type = value_type self.enabled = enabled @@ -75,7 +76,9 @@ def update(self, value: metrics_api.ValueT): def __repr__(self): return '{}(data="{}", last_update_timestamp={})'.format( - type(self).__name__, self.aggregator.current, self.last_update_timestamp + type(self).__name__, + self.aggregator.current, + self.last_update_timestamp, ) @@ -112,11 +115,12 @@ def record(self, value: metrics_api.ValueT) -> None: class Metric(metrics_api.Metric): """Base class for all metric types. - Also known as `metric instrument`. This is the class that is used to + Also known as metric instrument. This is the class that is used to represent a metric that is to be continuously recorded and tracked. Each metric has a set of handles that are created from the metric. See `BaseHandle` for information on handles. """ + HANDLE_TYPE = BaseHandle def __init__( @@ -145,9 +149,11 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: handle = self.handles.get(label_set) if not handle: handle = self.HANDLE_TYPE( - self.value_type, self.enabled, self.alternate, + self.value_type, + self.enabled, + self.alternate, # Aggregator will be created based off type of metric - self.meter.batcher.aggregator_for(self.__class__) + self.meter.batcher.aggregator_for(self.__class__), ) self.handles[label_set] = handle return handle @@ -255,7 +261,7 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, - alternate: bool = False + alternate: bool = False, ): super().__init__( name, @@ -274,13 +280,12 @@ def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: UPDATE_FUNCTION = record + class Record: """Container class used for processing in the `Batcher`""" - def __init__(self, - metric: Metric, - label_set: LabelSet, - aggregator: Aggregator + def __init__( + self, metric: Metric, label_set: LabelSet, aggregator: Aggregator ): self.metric = metric self.label_set = label_set @@ -297,6 +302,7 @@ class Meter(metrics_api.Meter): Args: batcher: The `Batcher` used for this meter. """ + def __init__(self, batcher: Batcher = UngroupedBatcher(True)): self.batcher = batcher # label_sets is a map of the labelset's encoded value to the label set diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 2af7dfb305d..76ac4e66114 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -23,7 +23,6 @@ class MetricsExportResult(Enum): class MetricRecord: - def __init__(self, aggregator, label_set, metric): self.aggregator = aggregator self.label_set = label_set @@ -72,7 +71,10 @@ def export( for record in metric_records: print( '{}(data="{}", label_set="{}", value={})'.format( - type(self).__name__, record.metric, record.label_set.labels, record.aggregator.check_point + type(self).__name__, + record.metric, + record.label_set.labels, + record.aggregator.check_point, ) ) return MetricsExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 55962267499..8c8112231ac 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -15,6 +15,7 @@ import abc import threading + class Aggregator(abc.ABC): """Base class for aggregators. @@ -39,8 +40,9 @@ def checkpoint(self): def merge(self, other): """Combines two aggregator values.""" + class CounterAggregator(Aggregator): - """Aggregator for `Counter` metrics.""" + """Aggregator for Counter metrics.""" def __init__(self): super().__init__() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 711d07a89e7..d2cf53708a6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -17,8 +17,10 @@ from typing import Sequence, Type from opentelemetry.metrics import Counter, MetricT from opentelemetry.sdk.metrics.export import MetricRecord -from opentelemetry.sdk.metrics \ - .export.aggregate import Aggregator, CounterAggregator +from opentelemetry.sdk.metrics.export.aggregate import ( + Aggregator, + CounterAggregator, +) class Batcher(abc.ABC): @@ -28,6 +30,7 @@ class Batcher(abc.ABC): received from updates from metrics in the meter. The stored values will be sent to an exporter for exporting. """ + def __init__(self, keep_state: bool): self.batch_map = {} # keep_state=True indicates the batcher computes checkpoints from over @@ -48,9 +51,9 @@ def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: return CounterAggregator() def check_point_set(self) -> Sequence[MetricRecord]: - """Returns a list of `MetricRecord` s used for exporting. + """Returns a list of MetricRecords used for exporting. - The list of `MetricRecord` s is a snapshot created from the current + The list of MetricRecords is a snapshot created from the current data in all of the aggregators in this batcher. """ metric_records = [] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 1a315fbc902..2045d79fff7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -14,6 +14,7 @@ import threading + class PushController(threading.Thread): """A push based controller, used for exporting. @@ -28,7 +29,7 @@ def __init__(self, meter, exporter, interval): self.interval = interval self.finished = threading.Event() self.start() - + def run(self): while not self.finished.wait(self.interval): self.tick() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 3035ae7ef9f..22d795912b8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -378,9 +378,7 @@ class Tracer(trace_api.Tracer): """ def __init__( - self, - source: "TracerSource", - instrumentation_info: InstrumentationInfo, + self, source: "TracerSource", instrumentation_info: InstrumentationInfo ) -> None: self.source = source self.instrumentation_info = instrumentation_info diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 6dabecf753c..cd8fc5569bd 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -12,14 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +import time import unittest from unittest import mock from opentelemetry.sdk import metrics -from opentelemetry.sdk.metrics \ - .export import ConsoleMetricsExporter, MetricRecord +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricsExporter, + MetricRecord, +) from opentelemetry.sdk.metrics.export.aggregate import CounterAggregator from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController class TestConsoleMetricsExporter(unittest.TestCase): @@ -40,7 +44,10 @@ def test_export(self): aggregator = CounterAggregator() record = MetricRecord(aggregator, label_set, metric) result = '{}(data="{}", label_set="{}", value={})'.format( - ConsoleMetricsExporter.__name__, metric, label_set.labels, aggregator.check_point + ConsoleMetricsExporter.__name__, + metric, + label_set.labels, + aggregator.check_point, ) with mock.patch("sys.stdout") as mock_stdout: exporter.export([record]) @@ -50,8 +57,12 @@ def test_export(self): class TestBatcher(unittest.TestCase): def test_aggregator_for_counter(self): batcher = UngroupedBatcher(True) - self.assertTrue(isinstance(batcher.aggregator_for(metrics.Counter), - CounterAggregator)) + self.assertTrue( + isinstance( + batcher.aggregator_for(metrics.Counter), CounterAggregator + ) + ) + # TODO: Add other aggregator tests def test_check_point_set(self): @@ -146,7 +157,9 @@ def test_ungrouped_batcher_process_exists(self): self.assertEqual(len(batcher.batch_map), 1) self.assertIsNotNone(batcher.batch_map.get((metric, ""))) self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) - self.assertEqual(batcher.batch_map.get((metric, ""))[0].check_point, 1.0) + self.assertEqual( + batcher.batch_map.get((metric, ""))[0].check_point, 1.0 + ) self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) def test_ungrouped_batcher_process_not_exists(self): @@ -170,7 +183,9 @@ def test_ungrouped_batcher_process_not_exists(self): self.assertEqual(len(batcher.batch_map), 1) self.assertIsNotNone(batcher.batch_map.get((metric, ""))) self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) - self.assertEqual(batcher.batch_map.get((metric, ""))[0].check_point, 1.0) + self.assertEqual( + batcher.batch_map.get((metric, ""))[0].check_point, 1.0 + ) self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) def test_ungrouped_batcher_process_not_stateful(self): @@ -194,9 +209,12 @@ def test_ungrouped_batcher_process_not_stateful(self): self.assertEqual(len(batcher.batch_map), 1) self.assertIsNotNone(batcher.batch_map.get((metric, ""))) self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) - self.assertEqual(batcher.batch_map.get((metric, ""))[0].check_point, 1.0) + self.assertEqual( + batcher.batch_map.get((metric, ""))[0].check_point, 1.0 + ) self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) + class TestAggregator(unittest.TestCase): # TODO: test other aggregators once implemented def test_counter_update(self): @@ -219,3 +237,14 @@ def test_counter_merge(self): counter2.check_point = 3.0 counter.merge(counter2) self.assertEqual(counter.check_point, 4.0) + + +class TestController(unittest.TestCase): + def test_push_controller(self): + meter = mock.Mock() + exporter = mock.Mock() + controller = PushController(meter, exporter, 5.0) + meter.collect.assert_not_called() + exporter.export.assert_not_called() + + controller.cancel() From 248a7934a6b6dc9ec138844f0403d54ff85e03e7 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 31 Dec 2019 11:43:48 -0800 Subject: [PATCH 10/41] fix lint --- docs/conf.py | 1 - .../src/opentelemetry_example_app/metrics_example.py | 2 +- .../src/opentelemetry/sdk/metrics/export/batcher.py | 5 +++-- scripts/eachdist.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1fb46af3e20..aa0b4096bc2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -77,7 +77,6 @@ ("py:class", "ValueT"), ("py:class", "MetricT"), ("py:class", "typing.Tuple"), - ("py:class", "Record"), ("py:class", "pymongo.monitoring.CommandListener"), ] diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py index 2ea8b370515..2ae483b9d9c 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py @@ -31,7 +31,7 @@ "bytes", int, Counter, - ("environment",) + ("environment",), ) label_set = meter.get_label_set({"environment": "staging"}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index d2cf53708a6..e49502ae9d4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -16,6 +16,7 @@ from typing import Sequence, Type from opentelemetry.metrics import Counter, MetricT +from opentelemetry.sdk import metrics from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, @@ -70,7 +71,7 @@ def finished_collection(self): self.batch_map = {} @abc.abstractmethod - def process(self, record: "Record") -> None: + def process(self, record: "metrics.Record") -> None: """Stores record information to be ready for exporting. Depending on type of batcher, performs pre-export logic, such as @@ -81,7 +82,7 @@ def process(self, record: "Record") -> None: class UngroupedBatcher(Batcher): """Accepts all records and passes them for exporting""" - def process(self, record: "Record"): + def process(self, record: "metrics.Record"): # Checkpoints the current aggregator value to be collected for export record.aggregator.checkpoint() batch_key = (record.metric, record.label_set.encoded) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 8d41315fc7a..406afb6ebfd 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -479,7 +479,7 @@ def lint_args(args): runsubprocess(args.dry_run, ("flake8", rootdir), check=True) execute_args( parse_subargs( - args, ("exec", "pylint {}", "--all", "--mode", "lintroots",), + args, ("exec", "pylint {}", "--all", "--mode", "lintroots") ) ) From 4ce4ae1a64ed67bfec62a4d161a3529b735bad93 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 31 Dec 2019 12:00:31 -0800 Subject: [PATCH 11/41] fix lint --- .../src/opentelemetry/sdk/metrics/export/batcher.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index e49502ae9d4..8da5e2dc5e6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -13,10 +13,9 @@ # limitations under the License. import abc - from typing import Sequence, Type + from opentelemetry.metrics import Counter, MetricT -from opentelemetry.sdk import metrics from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, @@ -71,7 +70,7 @@ def finished_collection(self): self.batch_map = {} @abc.abstractmethod - def process(self, record: "metrics.Record") -> None: + def process(self) -> None: """Stores record information to be ready for exporting. Depending on type of batcher, performs pre-export logic, such as @@ -82,7 +81,7 @@ def process(self, record: "metrics.Record") -> None: class UngroupedBatcher(Batcher): """Accepts all records and passes them for exporting""" - def process(self, record: "metrics.Record"): + def process(self, record): # Checkpoints the current aggregator value to be collected for export record.aggregator.checkpoint() batch_key = (record.metric, record.label_set.encoded) From 9be693cf179ce72b3263b8f7f2a35669ac295537 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 31 Dec 2019 12:12:27 -0800 Subject: [PATCH 12/41] fix lint --- opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index d5322d52f8d..638fc264987 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -298,7 +298,7 @@ def __init__( class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`. - + Args: batcher: The `Batcher` used for this meter. """ @@ -311,7 +311,7 @@ def __init__(self, batcher: Batcher = UngroupedBatcher(True)): def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. - + Utilizes the batcher to create checkpoints of the current values in each aggregator belonging to the metrics that were created with this meter instance. From f0f302e09f36ec9bb24496b2634c79f865b927ec Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 31 Dec 2019 12:34:55 -0800 Subject: [PATCH 13/41] fix lint --- .../src/opentelemetry/sdk/metrics/export/batcher.py | 8 ++++---- opentelemetry-sdk/tests/metrics/export/test_export.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 8da5e2dc5e6..f3e2e71cdac 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -44,11 +44,11 @@ def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: Aggregators keep track of and updates values when metrics get updated. """ + # pylint:disable=R0201 if metric_type == Counter: return CounterAggregator() - else: - # TODO: Add other aggregators - return CounterAggregator() + # TODO: Add other aggregators + return CounterAggregator() def check_point_set(self) -> Sequence[MetricRecord]: """Returns a list of MetricRecords used for exporting. @@ -70,7 +70,7 @@ def finished_collection(self): self.batch_map = {} @abc.abstractmethod - def process(self) -> None: + def process(self, record) -> None: """Stores record information to be ready for exporting. Depending on type of batcher, performs pre-export logic, such as diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index cd8fc5569bd..5c4d5266db4 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import time import unittest from unittest import mock @@ -246,5 +245,5 @@ def test_push_controller(self): controller = PushController(meter, exporter, 5.0) meter.collect.assert_not_called() exporter.export.assert_not_called() - controller.cancel() + self.assertTrue(controller.finished.isSet()) From 8351ef9dceea55b0bee05548a48801424cb35ee9 Mon Sep 17 00:00:00 2001 From: Leighton Date: Mon, 6 Jan 2020 16:33:49 -0800 Subject: [PATCH 14/41] address comments --- .../metrics_example.py | 2 +- .../src/opentelemetry/metrics/__init__.py | 21 +++-- .../tests/metrics/test_metrics.py | 6 +- .../src/opentelemetry/sdk/metrics/__init__.py | 91 +++++++++---------- .../sdk/metrics/export/aggregate.py | 2 +- .../sdk/metrics/export/batcher.py | 14 +-- .../tests/metrics/test_metrics.py | 42 ++++----- 7 files changed, 87 insertions(+), 91 deletions(-) diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py index 2ae483b9d9c..2f423619021 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py @@ -37,7 +37,7 @@ label_set = meter.get_label_set({"environment": "staging"}) # Direct metric usage -counter.add(label_set, 25) +counter.add(25, label_set) # Handle usage counter_handle = counter.get_handle(label_set) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index c45f3c02702..65b684a7053 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -128,12 +128,12 @@ def get_handle(self, label_set: LabelSet) -> "CounterHandle": """Gets a `CounterHandle`.""" return CounterHandle() - def add(self, label_set: LabelSet, value: ValueT) -> None: + def add(self, value: ValueT, label_set: LabelSet) -> None: """Increases the value of the counter by ``value``. Args: - label_set: `LabelSet` to associate with the returned handle. value: The value to add to the counter metric. + label_set: `LabelSet` to associate with the returned handle. """ @@ -150,12 +150,12 @@ def get_handle(self, label_set: LabelSet) -> "GaugeHandle": """Gets a `GaugeHandle`.""" return GaugeHandle() - def set(self, label_set: LabelSet, value: ValueT) -> None: + def set(self, value: ValueT, label_set: LabelSet) -> None: """Sets the value of the gauge to ``value``. Args: - label_set: `LabelSet` to associate with the returned handle. value: The value to set the gauge metric to. + label_set: `LabelSet` to associate with the returned handle. """ @@ -171,12 +171,12 @@ def get_handle(self, label_set: LabelSet) -> "MeasureHandle": """Gets a `MeasureHandle` with a float value.""" return MeasureHandle() - def record(self, label_set: LabelSet, value: ValueT) -> None: + def record(self, value: ValueT, label_set: LabelSet) -> None: """Records the ``value`` to the measure. Args: - label_set: `LabelSet` to associate with the returned handle. value: The value to record to this measure metric. + label_set: `LabelSet` to associate with the returned handle. """ @@ -220,7 +220,8 @@ def create_metric( metric_type: Type[MetricT], label_keys: Sequence[str] = (), enabled: bool = True, - alternate: bool = False, + monotonic: bool = False, + absolute: bool = True ) -> "Metric": """Creates a ``metric_kind`` metric with type ``value_type``. @@ -232,8 +233,10 @@ def create_metric( metric_type: The type of metric being created. label_keys: The keys for the labels with dynamic values. enabled: Whether to report the metric by default. - alternate: Whether to only allow non-negative values. - + monotonic: Configure a counter or gauge that accepts only + monotonic/non-monotonic updates. + absolute: Configure a measure that does or does not accept negative + updates. Returns: A new ``metric_type`` metric with values of ``value_type``. """ # pylint: disable=no-self-use diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index f8610a6fa4c..ccdbfcaa8e8 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -52,7 +52,7 @@ def test_counter(self): def test_counter_add(self): counter = metrics.Counter() label_set = metrics.LabelSet() - counter.add(label_set, 1) + counter.add(1, label_set) def test_gauge(self): gauge = metrics.Gauge() @@ -63,7 +63,7 @@ def test_gauge(self): def test_gauge_set(self): gauge = metrics.Gauge() label_set = metrics.LabelSet() - gauge.set(label_set, 1) + gauge.set(1, label_set) def test_measure(self): measure = metrics.Measure() @@ -74,7 +74,7 @@ def test_measure(self): def test_measure_record(self): measure = metrics.Measure() label_set = metrics.LabelSet() - measure.record(label_set, 1) + measure.record(1, label_set) def test_default_handle(self): metrics.DefaultMetricHandle() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 638fc264987..8b10bc78329 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -32,6 +32,12 @@ def __init__(self, labels: Dict[str, str] = None, encoded=""): self.labels = labels self.encoded = encoded + def __hash__(self): + return hash(self.encoded) + + def __eq__(self, other): + return self.encoded == other.encoded + class BaseHandle: """The base handle class containing common behavior for all handles. @@ -42,7 +48,9 @@ class BaseHandle: Args: value_type: The type of values this handle holds (int, float). enabled: True if the originating instrument is enabled. - alternate: Indicates applying default behavior for updating values. + monotonic: Indicates acceptance of only monotonic/non-monotonic values + for updating counter and gauge handles. + absolute: Indicates acceptance of negative updates to measure handles. aggregator: The aggregator for this handle. Will handle aggregation upon updates and checkpointing of values for exporting. """ @@ -51,12 +59,14 @@ def __init__( self, value_type: Type[metrics_api.ValueT], enabled: bool, - alternate: bool, + monotonic: bool, + absolute: bool, aggregator: Aggregator, ): self.value_type = value_type self.enabled = enabled - self.alternate = alternate + self.monotonic = monotonic + self.absolute = absolute self.aggregator = aggregator self.last_update_timestamp = time_ns() @@ -86,7 +96,7 @@ class CounterHandle(metrics_api.CounterHandle, BaseHandle): def add(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.CounterHandle.add`.""" if self._validate_update(value): - if self.alternate and value < 0: + if self.monotonic and value < 0: logger.warning("Monotonic counter cannot descend.") return self.update(value) @@ -96,7 +106,7 @@ class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): def set(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.GaugeHandle.set`.""" if self._validate_update(value): - if self.alternate and value < self.aggregator.current: + if self.monotonic and value < self.aggregator.current: logger.warning("Monotonic gauge cannot descend.") return self.update(value) @@ -106,7 +116,7 @@ class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): def record(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.MeasureHandle.record`.""" if self._validate_update(value): - if self.alternate and value < 0: + if self.absolute and value < 0: logger.warning("Absolute measure cannot accept negatives.") return self.update(value) @@ -132,7 +142,8 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, - alternate: bool = False, + monotonic: bool = False, + absolute: bool = True ): self.name = name self.description = description @@ -141,7 +152,8 @@ def __init__( self.meter = meter self.label_keys = label_keys self.enabled = enabled - self.alternate = alternate + self.monotonic = monotonic + self.absolute = absolute self.handles = {} def get_handle(self, label_set: LabelSet) -> BaseHandle: @@ -151,7 +163,8 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: handle = self.HANDLE_TYPE( self.value_type, self.enabled, - self.alternate, + self.monotonic, + self.absolute, # Aggregator will be created based off type of metric self.meter.batcher.aggregator_for(self.__class__), ) @@ -170,8 +183,8 @@ class Counter(Metric, metrics_api.Counter): """See `opentelemetry.metrics.Counter`. By default, counter values can only go up (monotonic). Negative inputs - will be discarded for monotonic counter metrics. Counter metrics that - have a monotonic option set to False allows negative inputs. + will be rejected for monotonic counter metrics. Counter metrics that have a + monotonic option set to False allows negative inputs. """ HANDLE_TYPE = CounterHandle @@ -185,7 +198,8 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, - alternate: bool = True, + monotonic: bool = True, + absolute: bool = False ): super().__init__( name, @@ -195,10 +209,11 @@ def __init__( meter, label_keys=label_keys, enabled=enabled, - alternate=alternate, + monotonic=monotonic, + absolute=absolute ) - def add(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: + def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Counter.add`.""" self.get_handle(label_set).add(value) @@ -209,7 +224,7 @@ class Gauge(Metric, metrics_api.Gauge): """See `opentelemetry.metrics.Gauge`. By default, gauge values can go both up and down (non-monotonic). - Negative inputs will be discarded for monotonic gauge metrics. + Negative inputs will be rejected for monotonic gauge metrics. """ HANDLE_TYPE = GaugeHandle @@ -223,7 +238,8 @@ def __init__( meter: "Meter", label_keys: Sequence[str] = (), enabled: bool = True, - alternate: bool = False, + monotonic: bool = False, + absolute: bool = False ): super().__init__( name, @@ -233,10 +249,11 @@ def __init__( meter, label_keys=label_keys, enabled=enabled, - alternate=alternate, + monotonic=monotonic, + absolute=absolute ) - def set(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: + def set(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Gauge.set`.""" self.get_handle(label_set).set(value) @@ -244,37 +261,11 @@ def set(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: class Measure(Metric, metrics_api.Measure): - """See `opentelemetry.metrics.Measure`. - - By default, measure metrics can accept both positive and negatives. - Negative inputs will be discarded when alternate is True. - """ + """See `opentelemetry.metrics.Measure`.""" HANDLE_TYPE = MeasureHandle - def __init__( - self, - name: str, - description: str, - unit: str, - value_type: Type[metrics_api.ValueT], - meter: "Meter", - label_keys: Sequence[str] = (), - enabled: bool = True, - alternate: bool = False, - ): - super().__init__( - name, - description, - unit, - value_type, - meter, - label_keys=label_keys, - enabled=enabled, - alternate=alternate, - ) - - def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None: + def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: """See `opentelemetry.metrics.Measure.record`.""" self.get_handle(label_set).record(value) @@ -332,7 +323,7 @@ def record_batch( ) -> None: """See `opentelemetry.metrics.Meter.record_batch`.""" for metric, value in record_tuples: - metric.UPDATE_FUNCTION(label_set, value) + metric.UPDATE_FUNCTION(value, label_set) def create_metric( self, @@ -343,7 +334,8 @@ def create_metric( metric_type: Type[metrics_api.MetricT], label_keys: Sequence[str] = (), enabled: bool = True, - alternate: bool = False, + monotonic: bool = False, + absolute: bool = True ) -> metrics_api.MetricT: """See `opentelemetry.metrics.Meter.create_metric`.""" # Ignore type b/c of mypy bug in addition to missing annotations @@ -355,7 +347,8 @@ def create_metric( self, label_keys=label_keys, enabled=enabled, - alternate=alternate, + monotonic=monotonic, + absolute=absolute ) self.metrics.add(metric) return metric diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 8c8112231ac..3976b9bbcfc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -53,7 +53,7 @@ def update(self, value): self.current += value def checkpoint(self): - # TODO: Implement lock-free algorithm for concurrency + # Lock-free algorithm? with self._lock: self.check_point = self.current self.current = 0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index f3e2e71cdac..d488653746f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -27,15 +27,15 @@ class Batcher(abc.ABC): """Base class for all batcher types. The batcher is responsible for storing the aggregators and aggregated - received from updates from metrics in the meter. The stored values will be + values received from updates from metrics in the meter. The stored values will be sent to an exporter for exporting. """ def __init__(self, keep_state: bool): - self.batch_map = {} + self._batch_map = {} # keep_state=True indicates the batcher computes checkpoints from over # the process lifetime. False indicates the batcher computes - # checkpoints which descrive the updates of a single collection period + # checkpoints which describe the updates of a single collection period # (deltas) self.keep_state = keep_state @@ -57,7 +57,7 @@ def check_point_set(self) -> Sequence[MetricRecord]: data in all of the aggregators in this batcher. """ metric_records = [] - for key, value in self.batch_map.items(): + for key, value in self._batch_map.items(): metric_records.append(MetricRecord(value[0], value[1], key[0])) return metric_records @@ -67,7 +67,7 @@ def finished_collection(self): For batchers that are stateless, resets the batch map. """ if not self.keep_state: - self.batch_map = {} + self._batch_map = {} @abc.abstractmethod def process(self, record) -> None: @@ -85,7 +85,7 @@ def process(self, record): # Checkpoints the current aggregator value to be collected for export record.aggregator.checkpoint() batch_key = (record.metric, record.label_set.encoded) - batch_value = self.batch_map.get(batch_key) + batch_value = self._batch_map.get(batch_key) aggregator = record.aggregator if batch_value: # Update the stored checkpointed value if exists. This is for cases @@ -97,4 +97,4 @@ def process(self, record): # it with the current checkpointed value for long-term storage aggregator = self.aggregator_for(record.metric.__class__) aggregator.merge(record.aggregator) - self.batch_map[batch_key] = (aggregator, record.label_set) + self._batch_map[batch_key] = (aggregator, record.label_set) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 199acf5b452..13521bf3efb 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -101,7 +101,7 @@ def test_record_batch_exists(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - counter.add(label_set, 1.0) + counter.add(1.0, label_set) handle = counter.get_handle(label_set) record_tuples = [(counter, 1.0)] meter.record_batch(label_set, record_tuples) @@ -175,8 +175,8 @@ def test_add(self): kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) - metric.add(label_set, 3) - metric.add(label_set, 2) + metric.add(3, label_set) + metric.add(2, label_set) self.assertEqual(handle.aggregator.current, 5) @@ -187,9 +187,9 @@ def test_set(self): kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) - metric.set(label_set, 3) + metric.set(3, label_set) self.assertEqual(handle.aggregator.current, 3) - metric.set(label_set, 2) + metric.set(2, label_set) # TODO: Fix once other aggregators implemented self.assertEqual(handle.aggregator.current, 5) @@ -201,7 +201,7 @@ def test_record(self): kvp = {"key": "value"} label_set = meter.get_label_set(kvp) handle = metric.get_handle(label_set) - metric.record(label_set, 3) + metric.record(3, label_set) # TODO: Fix once other aggregators implemented self.assertEqual(handle.aggregator.current, 3) @@ -209,20 +209,20 @@ def test_record(self): class TestCounterHandle(unittest.TestCase): def test_add(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, False, aggregator) + handle = metrics.CounterHandle(int, True, False, False, aggregator) handle.add(3) self.assertEqual(handle.aggregator.current, 3) def test_add_disabled(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, False, False, aggregator) + handle = metrics.CounterHandle(int, False, False, False, aggregator) handle.add(3) self.assertEqual(handle.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_monotonic(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, True, aggregator) + handle = metrics.CounterHandle(int, True, True, False, aggregator) handle.add(-3) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @@ -230,7 +230,7 @@ def test_add_monotonic(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.logger") def test_add_incorrect_type(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, False, aggregator) + handle = metrics.CounterHandle(int, True, False, False, aggregator) handle.add(3.0) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @@ -238,7 +238,7 @@ def test_add_incorrect_type(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.time_ns") def test_update(self, time_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.CounterHandle(int, True, False, aggregator) + handle = metrics.CounterHandle(int, True, False, False, aggregator) time_mock.return_value = 123 handle.update(4.0) self.assertEqual(handle.last_update_timestamp, 123) @@ -249,20 +249,20 @@ def test_update(self, time_mock): class TestGaugeHandle(unittest.TestCase): def test_set(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, False, aggregator) + handle = metrics.GaugeHandle(int, True, False, False, aggregator) handle.set(3) self.assertEqual(handle.aggregator.current, 3) def test_set_disabled(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, False, False, aggregator) + handle = metrics.GaugeHandle(int, False, False, False, aggregator) handle.set(3) self.assertEqual(handle.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_set_monotonic(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, True, aggregator) + handle = metrics.GaugeHandle(int, True, True, False, aggregator) handle.set(-3) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @@ -270,7 +270,7 @@ def test_set_monotonic(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.logger") def test_set_incorrect_type(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, False, aggregator) + handle = metrics.GaugeHandle(int, True, False, False, aggregator) handle.set(3.0) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @@ -278,7 +278,7 @@ def test_set_incorrect_type(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.time_ns") def test_update(self, time_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, False, aggregator) + handle = metrics.GaugeHandle(int, True, False, False, aggregator) time_mock.return_value = 123 handle.update(4.0) self.assertEqual(handle.last_update_timestamp, 123) @@ -289,20 +289,20 @@ def test_update(self, time_mock): class TestMeasureHandle(unittest.TestCase): def test_record(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, False, False, aggregator) + handle = metrics.MeasureHandle(int, False, False, False, aggregator) handle.record(3) self.assertEqual(handle.aggregator.current, 0) def test_record_disabled(self): aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, False, False, aggregator) + handle = metrics.MeasureHandle(int, False, False, False, aggregator) handle.record(3) self.assertEqual(handle.aggregator.current, 0) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_monotonic(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, True, True, aggregator) + handle = metrics.MeasureHandle(int, True, False, True, aggregator) handle.record(-3) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @@ -310,7 +310,7 @@ def test_record_monotonic(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, True, False, aggregator) + handle = metrics.MeasureHandle(int, True, False, False, aggregator) handle.record(3.0) self.assertEqual(handle.aggregator.current, 0) self.assertTrue(logger_mock.warning.called) @@ -318,7 +318,7 @@ def test_record_incorrect_type(self, logger_mock): @mock.patch("opentelemetry.sdk.metrics.time_ns") def test_update(self, time_mock): aggregator = export.aggregate.CounterAggregator() - handle = metrics.MeasureHandle(int, True, False, aggregator) + handle = metrics.MeasureHandle(int, True, False, False, aggregator) time_mock.return_value = 123 handle.update(4.0) self.assertEqual(handle.last_update_timestamp, 123) From 4408d94a49b7fea7acafc2d3257f21e18ddf7b74 Mon Sep 17 00:00:00 2001 From: Leighton Date: Mon, 6 Jan 2020 17:13:15 -0800 Subject: [PATCH 15/41] fix tests --- .../tests/metrics/export/test_export.py | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 5c4d5266db4..719cf87b612 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -80,7 +80,7 @@ def test_check_point_set(self): label_set = {} batch_map = {} batch_map[(metric, "")] = (aggregator, label_set) - batcher.batch_map = batch_map + batcher._batch_map = batch_map records = batcher.check_point_set() self.assertEqual(len(records), 1) self.assertEqual(records[0].metric, metric) @@ -108,9 +108,9 @@ def test_finished_collection_stateless(self): label_set = {} batch_map = {} batch_map[(metric, "")] = (aggregator, label_set) - batcher.batch_map = batch_map + batcher._batch_map = batch_map batcher.finished_collection() - self.assertEqual(len(batcher.batch_map), 0) + self.assertEqual(len(batcher._batch_map), 0) def test_finished_collection_stateful(self): meter = metrics.Meter() @@ -128,9 +128,9 @@ def test_finished_collection_stateful(self): label_set = {} batch_map = {} batch_map[(metric, "")] = (aggregator, label_set) - batcher.batch_map = batch_map + batcher._batch_map = batch_map batcher.finished_collection() - self.assertEqual(len(batcher.batch_map), 1) + self.assertEqual(len(batcher._batch_map), 1) # TODO: Abstract the logic once other batchers implemented def test_ungrouped_batcher_process_exists(self): @@ -150,16 +150,16 @@ def test_ungrouped_batcher_process_exists(self): batch_map = {} batch_map[(metric, "")] = (aggregator, label_set) aggregator2.update(1.0) - batcher.batch_map = batch_map + batcher._batch_map = batch_map record = metrics.Record(metric, label_set, aggregator2) batcher.process(record) - self.assertEqual(len(batcher.batch_map), 1) - self.assertIsNotNone(batcher.batch_map.get((metric, ""))) - self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) + self.assertEqual(len(batcher._batch_map), 1) + self.assertIsNotNone(batcher._batch_map.get((metric, ""))) + self.assertEqual(batcher._batch_map.get((metric, ""))[0].current, 0) self.assertEqual( - batcher.batch_map.get((metric, ""))[0].check_point, 1.0 + batcher._batch_map.get((metric, ""))[0].check_point, 1.0 ) - self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) + self.assertEqual(batcher._batch_map.get((metric, ""))[1], label_set) def test_ungrouped_batcher_process_not_exists(self): meter = metrics.Meter() @@ -176,16 +176,16 @@ def test_ungrouped_batcher_process_not_exists(self): label_set = metrics.LabelSet() batch_map = {} aggregator.update(1.0) - batcher.batch_map = batch_map + batcher._batch_map = batch_map record = metrics.Record(metric, label_set, aggregator) batcher.process(record) - self.assertEqual(len(batcher.batch_map), 1) - self.assertIsNotNone(batcher.batch_map.get((metric, ""))) - self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) + self.assertEqual(len(batcher._batch_map), 1) + self.assertIsNotNone(batcher._batch_map.get((metric, ""))) + self.assertEqual(batcher._batch_map.get((metric, ""))[0].current, 0) self.assertEqual( - batcher.batch_map.get((metric, ""))[0].check_point, 1.0 + batcher._batch_map.get((metric, ""))[0].check_point, 1.0 ) - self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) + self.assertEqual(batcher._batch_map.get((metric, ""))[1], label_set) def test_ungrouped_batcher_process_not_stateful(self): meter = metrics.Meter() @@ -202,16 +202,16 @@ def test_ungrouped_batcher_process_not_stateful(self): label_set = metrics.LabelSet() batch_map = {} aggregator.update(1.0) - batcher.batch_map = batch_map + batcher._batch_map = batch_map record = metrics.Record(metric, label_set, aggregator) batcher.process(record) - self.assertEqual(len(batcher.batch_map), 1) - self.assertIsNotNone(batcher.batch_map.get((metric, ""))) - self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) + self.assertEqual(len(batcher._batch_map), 1) + self.assertIsNotNone(batcher._batch_map.get((metric, ""))) + self.assertEqual(batcher._batch_map.get((metric, ""))[0].current, 0) self.assertEqual( - batcher.batch_map.get((metric, ""))[0].check_point, 1.0 + batcher._batch_map.get((metric, ""))[0].check_point, 1.0 ) - self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) + self.assertEqual(batcher._batch_map.get((metric, ""))[1], label_set) class TestAggregator(unittest.TestCase): From 9db15402fc45d5906f7becf69f724a45848aca89 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 7 Jan 2020 10:45:58 -0800 Subject: [PATCH 16/41] fix lint --- .../src/opentelemetry/metrics/__init__.py | 2 +- .../src/opentelemetry/sdk/metrics/__init__.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 65b684a7053..e38a3a7fd4d 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -221,7 +221,7 @@ def create_metric( label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, - absolute: bool = True + absolute: bool = True, ) -> "Metric": """Creates a ``metric_kind`` metric with type ``value_type``. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 8b10bc78329..63523256f3c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -143,7 +143,7 @@ def __init__( label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, - absolute: bool = True + absolute: bool = True, ): self.name = name self.description = description @@ -199,7 +199,7 @@ def __init__( label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = True, - absolute: bool = False + absolute: bool = False, ): super().__init__( name, @@ -210,7 +210,7 @@ def __init__( label_keys=label_keys, enabled=enabled, monotonic=monotonic, - absolute=absolute + absolute=absolute, ) def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: @@ -239,7 +239,7 @@ def __init__( label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, - absolute: bool = False + absolute: bool = False, ): super().__init__( name, @@ -250,7 +250,7 @@ def __init__( label_keys=label_keys, enabled=enabled, monotonic=monotonic, - absolute=absolute + absolute=absolute, ) def set(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: @@ -335,7 +335,7 @@ def create_metric( label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, - absolute: bool = True + absolute: bool = True, ) -> metrics_api.MetricT: """See `opentelemetry.metrics.Meter.create_metric`.""" # Ignore type b/c of mypy bug in addition to missing annotations @@ -348,7 +348,7 @@ def create_metric( label_keys=label_keys, enabled=enabled, monotonic=monotonic, - absolute=absolute + absolute=absolute, ) self.metrics.add(metric) return metric From cc862b96ddbae5b89b968f16f3d1774a3764e192 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 7 Jan 2020 11:11:14 -0800 Subject: [PATCH 17/41] Add setters --- .../sdk/metrics/export/batcher.py | 8 ++++ .../tests/metrics/export/test_export.py | 46 +++++++++---------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index d488653746f..2a1b2b1e35b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -39,6 +39,14 @@ def __init__(self, keep_state: bool): # (deltas) self.keep_state = keep_state + @property + def batch_map(self): + return self._batch_map + + @batch_map.setter + def batch_map(self, value): + self._batch_map = value + def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: """Returns an aggregator based off metric type. diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 719cf87b612..5c4d5266db4 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -80,7 +80,7 @@ def test_check_point_set(self): label_set = {} batch_map = {} batch_map[(metric, "")] = (aggregator, label_set) - batcher._batch_map = batch_map + batcher.batch_map = batch_map records = batcher.check_point_set() self.assertEqual(len(records), 1) self.assertEqual(records[0].metric, metric) @@ -108,9 +108,9 @@ def test_finished_collection_stateless(self): label_set = {} batch_map = {} batch_map[(metric, "")] = (aggregator, label_set) - batcher._batch_map = batch_map + batcher.batch_map = batch_map batcher.finished_collection() - self.assertEqual(len(batcher._batch_map), 0) + self.assertEqual(len(batcher.batch_map), 0) def test_finished_collection_stateful(self): meter = metrics.Meter() @@ -128,9 +128,9 @@ def test_finished_collection_stateful(self): label_set = {} batch_map = {} batch_map[(metric, "")] = (aggregator, label_set) - batcher._batch_map = batch_map + batcher.batch_map = batch_map batcher.finished_collection() - self.assertEqual(len(batcher._batch_map), 1) + self.assertEqual(len(batcher.batch_map), 1) # TODO: Abstract the logic once other batchers implemented def test_ungrouped_batcher_process_exists(self): @@ -150,16 +150,16 @@ def test_ungrouped_batcher_process_exists(self): batch_map = {} batch_map[(metric, "")] = (aggregator, label_set) aggregator2.update(1.0) - batcher._batch_map = batch_map + batcher.batch_map = batch_map record = metrics.Record(metric, label_set, aggregator2) batcher.process(record) - self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get((metric, ""))) - self.assertEqual(batcher._batch_map.get((metric, ""))[0].current, 0) + self.assertEqual(len(batcher.batch_map), 1) + self.assertIsNotNone(batcher.batch_map.get((metric, ""))) + self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) self.assertEqual( - batcher._batch_map.get((metric, ""))[0].check_point, 1.0 + batcher.batch_map.get((metric, ""))[0].check_point, 1.0 ) - self.assertEqual(batcher._batch_map.get((metric, ""))[1], label_set) + self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) def test_ungrouped_batcher_process_not_exists(self): meter = metrics.Meter() @@ -176,16 +176,16 @@ def test_ungrouped_batcher_process_not_exists(self): label_set = metrics.LabelSet() batch_map = {} aggregator.update(1.0) - batcher._batch_map = batch_map + batcher.batch_map = batch_map record = metrics.Record(metric, label_set, aggregator) batcher.process(record) - self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get((metric, ""))) - self.assertEqual(batcher._batch_map.get((metric, ""))[0].current, 0) + self.assertEqual(len(batcher.batch_map), 1) + self.assertIsNotNone(batcher.batch_map.get((metric, ""))) + self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) self.assertEqual( - batcher._batch_map.get((metric, ""))[0].check_point, 1.0 + batcher.batch_map.get((metric, ""))[0].check_point, 1.0 ) - self.assertEqual(batcher._batch_map.get((metric, ""))[1], label_set) + self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) def test_ungrouped_batcher_process_not_stateful(self): meter = metrics.Meter() @@ -202,16 +202,16 @@ def test_ungrouped_batcher_process_not_stateful(self): label_set = metrics.LabelSet() batch_map = {} aggregator.update(1.0) - batcher._batch_map = batch_map + batcher.batch_map = batch_map record = metrics.Record(metric, label_set, aggregator) batcher.process(record) - self.assertEqual(len(batcher._batch_map), 1) - self.assertIsNotNone(batcher._batch_map.get((metric, ""))) - self.assertEqual(batcher._batch_map.get((metric, ""))[0].current, 0) + self.assertEqual(len(batcher.batch_map), 1) + self.assertIsNotNone(batcher.batch_map.get((metric, ""))) + self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) self.assertEqual( - batcher._batch_map.get((metric, ""))[0].check_point, 1.0 + batcher.batch_map.get((metric, ""))[0].check_point, 1.0 ) - self.assertEqual(batcher._batch_map.get((metric, ""))[1], label_set) + self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) class TestAggregator(unittest.TestCase): From 7a5a14d2fc35374a25cb82567a1978e6b4540335 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 7 Jan 2020 15:57:04 -0800 Subject: [PATCH 18/41] add examples --- examples/metrics/non-stateful.py | 60 ++++++++++++++++++++++++ examples/metrics/record.py | 68 +++++++++++++++++++++++++++ examples/metrics/stateful.py | 79 ++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 examples/metrics/non-stateful.py create mode 100644 examples/metrics/record.py create mode 100644 examples/metrics/stateful.py diff --git a/examples/metrics/non-stateful.py b/examples/metrics/non-stateful.py new file mode 100644 index 00000000000..d8b7e725a36 --- /dev/null +++ b/examples/metrics/non-stateful.py @@ -0,0 +1,60 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example for a simple application using metrics +Examples show how to recording affects the collection of metrics to be exported +""" +import time + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController + +# Batcher used to collect all created metrics from meter ready for exporting +# Pass in false for non-stateful batcher. Indicates the batcher computes +# checkpoints which describe the updates of a single collection period (deltas) +batcher = UngroupedBatcher(False) +# Meter is responsible for creating and recording metrics +meter = Meter(batcher) +metrics.set_preferred_meter_implementation(meter) +# exporter to export metrics to the console +exporter = ConsoleMetricsExporter() +# controller collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, exporter, 5) + +counter = meter.create_metric( + "available memory", + "available memory", + "bytes", + int, + Counter, + ("environment",), +) + +label_set = meter.get_label_set({"environment": "staging"}) + +counter.add(25, label_set) +# We sleep for 5 seconds, exported value should be 25 +time.sleep(5) + +counter.add(50, label_set) +# exported value should be 50 due to non-stateful batcher +time.sleep(5) + +# Following exported values would be 0 + diff --git a/examples/metrics/record.py b/examples/metrics/record.py new file mode 100644 index 00000000000..7766f4bb619 --- /dev/null +++ b/examples/metrics/record.py @@ -0,0 +1,68 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example for a simple application using metrics. +It demonstrates the different ways you can record metrics via the meter. +""" + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.controller import PushController + +# Meter is responsible for creating and recording metrics +meter = Meter() +metrics.set_preferred_meter_implementation(meter) +# exporter to export metrics to the console +exporter = ConsoleMetricsExporter() +# controller collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, exporter, 5) + +## Example to show how to record using the meter +counter = meter.create_metric( + "available memory", + "available memory", + "bytes", + int, + Counter, + ("environment",), +) + +# Labelsets are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric + +# The meter takes a dictionary of key value pairs +label_set = meter.get_label_set({"environment": "staging"}) + +# Handle usage +# You can record metrics with metric handles. Handles are created by passing in +# a labelset. A handle is essentially metric data that corresponds to a specific +# set of labels. Therefore, getting a handle using the same set of labels will +# yield the same metric handle. +counter_handle = counter.get_handle(label_set) +counter_handle.add(100) + +# Direct metric usage +# You can record metrics directly using the metric instrument. You pass in a +# labelset that you would like to record for. +counter.add(25, label_set) + +# Record batch usage +# You can record metrics in a batch by passing in a labelset and a sequence of +# (metric, value) pairs. The value would be recorded for each metric using the +# specified labelset for each. +meter.record_batch(label_set, [(counter, 50)]) diff --git a/examples/metrics/stateful.py b/examples/metrics/stateful.py new file mode 100644 index 00000000000..56f7a87179f --- /dev/null +++ b/examples/metrics/stateful.py @@ -0,0 +1,79 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This module serves as an example for a simple application using metrics +Examples show how to recording affects the collection of metrics to be exported +""" +import time + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController + +# Batcher used to collect all created metrics from meter ready for exporting +# Pass in true/false to indicate whether the batcher is stateful. True +# indicates the batcher computes checkpoints from over the process lifetime. +# False indicates the batcher computes checkpoints which describe the updates +# of a single collection period (deltas) +batcher = UngroupedBatcher(True) +# If a batcher is not provded, a default batcher is used +# Meter is responsible for creating and recording metrics +meter = Meter(batcher) +metrics.set_preferred_meter_implementation(meter) +# exporter to export metrics to the console +exporter = ConsoleMetricsExporter() +# controller collects metrics created from meter and exports it via the +# exporter every interval +controller = PushController(meter, exporter, 5) + +counter = meter.create_metric( + "available memory", + "available memory", + "bytes", + int, + Counter, + ("environment",), +) + +counter2 = meter.create_metric( + "available memory2", + "available memory2", + "bytes2", + int, + Counter, + ("environment",), +) + +label_set = meter.get_label_set({"environment": "staging"}) +label_set2 = meter.get_label_set({"environment": "testing"}) + +counter.add(25, label_set) +# We sleep for 5 seconds, exported value should be 25 +time.sleep(5) + +counter.add(50, label_set) +# exported value should be 75 +time.sleep(5) + +counter.add(35, label_set2) +# should be two exported values 75 and 35, one for each labelset +time.sleep(5) + +counter2.add(5, label_set) +# should be three exported values, labelsets can be reused for different +# metrics but will be recorded seperately, 75, 35 and 5 +time.sleep(5) From f9fbd6d2fa7cc196db07d54cce10e57329bd6a00 Mon Sep 17 00:00:00 2001 From: Leighton Date: Wed, 8 Jan 2020 11:04:54 -0800 Subject: [PATCH 19/41] fix lint --- examples/metrics/non-stateful.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/metrics/non-stateful.py b/examples/metrics/non-stateful.py index d8b7e725a36..90240749b26 100644 --- a/examples/metrics/non-stateful.py +++ b/examples/metrics/non-stateful.py @@ -57,4 +57,3 @@ time.sleep(5) # Following exported values would be 0 - From ba41d3822a12371f807cecb595dd80e385f82e49 Mon Sep 17 00:00:00 2001 From: Leighton Date: Wed, 8 Jan 2020 11:26:46 -0800 Subject: [PATCH 20/41] fix lint --- examples/metrics/record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/metrics/record.py b/examples/metrics/record.py index 7766f4bb619..d2360f19c9b 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -31,7 +31,7 @@ # exporter every interval controller = PushController(meter, exporter, 5) -## Example to show how to record using the meter +# Example to show how to record using the meter counter = meter.create_metric( "available memory", "available memory", From 5c2b86e75bf510f37372147d0c26c6bc5c3447d4 Mon Sep 17 00:00:00 2001 From: Leighton Date: Wed, 8 Jan 2020 11:38:48 -0800 Subject: [PATCH 21/41] fix lint --- examples/metrics/{non-stateful.py => non_stateful.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/metrics/{non-stateful.py => non_stateful.py} (100%) diff --git a/examples/metrics/non-stateful.py b/examples/metrics/non_stateful.py similarity index 100% rename from examples/metrics/non-stateful.py rename to examples/metrics/non_stateful.py From 4e771d64ac85fedfa04fd29a28b269f7e932671f Mon Sep 17 00:00:00 2001 From: Leighton Date: Wed, 15 Jan 2020 11:17:46 -0800 Subject: [PATCH 22/41] Address comments --- examples/metrics/non_stateful.py | 3 +++ examples/metrics/stateful.py | 3 +++ opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/metrics/non_stateful.py b/examples/metrics/non_stateful.py index 90240749b26..fb172e798ee 100644 --- a/examples/metrics/non_stateful.py +++ b/examples/metrics/non_stateful.py @@ -46,6 +46,9 @@ ("environment",), ) +# Labelsets are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric label_set = meter.get_label_set({"environment": "staging"}) counter.add(25, label_set) diff --git a/examples/metrics/stateful.py b/examples/metrics/stateful.py index 56f7a87179f..e0944752678 100644 --- a/examples/metrics/stateful.py +++ b/examples/metrics/stateful.py @@ -58,6 +58,9 @@ ("environment",), ) +# Labelsets are used to identify key-values that are associated with a specific +# metric that you want to record. These are useful for pre-aggregation and can +# be used to store custom dimensions pertaining to a metric label_set = meter.get_label_set({"environment": "staging"}) label_set2 = meter.get_label_set({"environment": "testing"}) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 63523256f3c..e86ee5db7ae 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -310,7 +310,7 @@ def collect(self) -> None: for metric in self.metrics: if metric.enabled: for label_set, handle in metric.handles.items(): - # Consider storing records in memory? + # TODO: Consider storing records in memory? record = Record(metric, label_set, handle.aggregator) # Checkpoints the current aggregators # Applies different batching logic based on type of batcher From 303fefe815911a14c2e212668280fe0a90141354 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 28 Jan 2020 11:21:30 -0800 Subject: [PATCH 23/41] LabelSet change LabelSet to calculate encoding on construction, hash and eq simple comparison, change batch value in batch map to be just aggregation, remove labels cache in meter, fix DefaultMeter api --- .../src/opentelemetry/metrics/__init__.py | 1 + .../src/opentelemetry/sdk/metrics/__init__.py | 26 +++++++------- .../sdk/metrics/export/batcher.py | 9 ++--- .../tests/metrics/export/test_export.py | 35 +++++++++---------- .../tests/metrics/test_metrics.py | 13 +++---- 5 files changed, 38 insertions(+), 46 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index d64b70bb312..7ef974ebe23 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -273,6 +273,7 @@ def create_metric( label_keys: Sequence[str] = (), enabled: bool = True, monotonic: bool = False, + absolute: bool = True, ) -> "Metric": # pylint: disable=no-self-use return DefaultMetric() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index e86ee5db7ae..a7c74f850ea 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -28,15 +28,20 @@ class LabelSet(metrics_api.LabelSet): """See `opentelemetry.metrics.LabelSet`.""" - def __init__(self, labels: Dict[str, str] = None, encoded=""): - self.labels = labels - self.encoded = encoded + def __init__(self, labels: Dict[str, str] = {}): + # LabelSet properties used only in dictionaries for fast lookup + self._labels = tuple(labels.items()) + self._encoded = tuple(sorted(labels.items())) + + @property + def labels(self): + return self._labels def __hash__(self): - return hash(self.encoded) + return hash(self._encoded) def __eq__(self, other): - return self.encoded == other.encoded + return self._encoded == other._encoded class BaseHandle: @@ -168,7 +173,7 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: # Aggregator will be created based off type of metric self.meter.batcher.aggregator_for(self.__class__), ) - self.handles[label_set] = handle + self.handles[label_set] = handle return handle def __repr__(self): @@ -296,8 +301,6 @@ class Meter(metrics_api.Meter): def __init__(self, batcher: Batcher = UngroupedBatcher(True)): self.batcher = batcher - # label_sets is a map of the labelset's encoded value to the label set - self.label_sets = {} self.metrics = set() def collect(self) -> None: @@ -363,9 +366,4 @@ def get_label_set(self, labels: Dict[str, str]): """ if len(labels) == 0: return EMPTY_LABEL_SET - # Use simple encoding for now until encoding API is implemented - encoded = tuple(sorted(labels.items())) - # If LabelSet exists for this meter in memory, use existing one - if encoded not in self.label_sets: - self.label_sets[encoded] = LabelSet(labels=labels, encoded=encoded) - return self.label_sets[encoded] + return LabelSet(labels=labels) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 2a1b2b1e35b..39e0a6f4598 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -66,7 +66,8 @@ def check_point_set(self) -> Sequence[MetricRecord]: """ metric_records = [] for key, value in self._batch_map.items(): - metric_records.append(MetricRecord(value[0], value[1], key[0])) + # Batch map is in the format: (metric, label_set): aggregator + metric_records.append(MetricRecord(value, key[1], key[0])) return metric_records def finished_collection(self): @@ -92,17 +93,17 @@ class UngroupedBatcher(Batcher): def process(self, record): # Checkpoints the current aggregator value to be collected for export record.aggregator.checkpoint() - batch_key = (record.metric, record.label_set.encoded) + batch_key = (record.metric, record.label_set) batch_value = self._batch_map.get(batch_key) aggregator = record.aggregator if batch_value: # Update the stored checkpointed value if exists. This is for cases # when an update comes at the same time as a checkpoint call - batch_value[0].merge(aggregator) + batch_value.merge(aggregator) return if self.keep_state: # if stateful batcher, create a copy of the aggregator and update # it with the current checkpointed value for long-term storage aggregator = self.aggregator_for(record.metric.__class__) aggregator.merge(record.aggregator) - self._batch_map[batch_key] = (aggregator, record.label_set) + self._batch_map[batch_key] = aggregator diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 5c4d5266db4..1152953ecd3 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -77,9 +77,9 @@ def test_check_point_set(self): ("environment",), ) aggregator.update(1.0) - label_set = {} + label_set = metrics.LabelSet() batch_map = {} - batch_map[(metric, "")] = (aggregator, label_set) + batch_map[(metric, label_set)] = aggregator batcher.batch_map = batch_map records = batcher.check_point_set() self.assertEqual(len(records), 1) @@ -105,9 +105,9 @@ def test_finished_collection_stateless(self): ("environment",), ) aggregator.update(1.0) - label_set = {} + label_set = metrics.LabelSet() batch_map = {} - batch_map[(metric, "")] = (aggregator, label_set) + batch_map[(metric, label_set)] = aggregator batcher.batch_map = batch_map batcher.finished_collection() self.assertEqual(len(batcher.batch_map), 0) @@ -125,9 +125,9 @@ def test_finished_collection_stateful(self): ("environment",), ) aggregator.update(1.0) - label_set = {} + label_set = metrics.LabelSet() batch_map = {} - batch_map[(metric, "")] = (aggregator, label_set) + batch_map[(metric, label_set)] = aggregator batcher.batch_map = batch_map batcher.finished_collection() self.assertEqual(len(batcher.batch_map), 1) @@ -148,18 +148,17 @@ def test_ungrouped_batcher_process_exists(self): ) label_set = metrics.LabelSet() batch_map = {} - batch_map[(metric, "")] = (aggregator, label_set) + batch_map[(metric, label_set)] = aggregator aggregator2.update(1.0) batcher.batch_map = batch_map record = metrics.Record(metric, label_set, aggregator2) batcher.process(record) self.assertEqual(len(batcher.batch_map), 1) - self.assertIsNotNone(batcher.batch_map.get((metric, ""))) - self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) + self.assertIsNotNone(batcher.batch_map.get((metric, label_set))) + self.assertEqual(batcher.batch_map.get((metric, label_set)).current, 0) self.assertEqual( - batcher.batch_map.get((metric, ""))[0].check_point, 1.0 + batcher.batch_map.get((metric, label_set)).check_point, 1.0 ) - self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) def test_ungrouped_batcher_process_not_exists(self): meter = metrics.Meter() @@ -180,12 +179,11 @@ def test_ungrouped_batcher_process_not_exists(self): record = metrics.Record(metric, label_set, aggregator) batcher.process(record) self.assertEqual(len(batcher.batch_map), 1) - self.assertIsNotNone(batcher.batch_map.get((metric, ""))) - self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) + self.assertIsNotNone(batcher.batch_map.get((metric, label_set))) + self.assertEqual(batcher.batch_map.get((metric, label_set)).current, 0) self.assertEqual( - batcher.batch_map.get((metric, ""))[0].check_point, 1.0 + batcher.batch_map.get((metric, label_set)).check_point, 1.0 ) - self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) def test_ungrouped_batcher_process_not_stateful(self): meter = metrics.Meter() @@ -206,12 +204,11 @@ def test_ungrouped_batcher_process_not_stateful(self): record = metrics.Record(metric, label_set, aggregator) batcher.process(record) self.assertEqual(len(batcher.batch_map), 1) - self.assertIsNotNone(batcher.batch_map.get((metric, ""))) - self.assertEqual(batcher.batch_map.get((metric, ""))[0].current, 0) + self.assertIsNotNone(batcher.batch_map.get((metric, label_set))) + self.assertEqual(batcher.batch_map.get((metric, label_set)).current, 0) self.assertEqual( - batcher.batch_map.get((metric, ""))[0].check_point, 1.0 + batcher.batch_map.get((metric, label_set)).check_point, 1.0 ) - self.assertEqual(batcher.batch_map.get((metric, ""))[1], label_set) class TestAggregator(unittest.TestCase): diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 13521bf3efb..2407173507c 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -139,8 +139,10 @@ def test_get_label_set(self): meter = metrics.Meter() kvp = {"environment": "staging", "a": "z"} label_set = meter.get_label_set(kvp) - encoded = tuple(sorted(kvp.items())) - self.assertIs(meter.label_sets[encoded], label_set) + kvp2 = {"environment": "staging", "a": "z"} + label_set2 = meter.get_label_set(kvp) + labels = set([label_set, label_set2]) + self.assertEqual(len(labels), 1) def test_get_label_set_empty(self): meter = metrics.Meter() @@ -148,13 +150,6 @@ def test_get_label_set_empty(self): label_set = meter.get_label_set(kvp) self.assertEqual(label_set, metrics.EMPTY_LABEL_SET) - def test_get_label_set_exists(self): - meter = metrics.Meter() - kvp = {"environment": "staging", "a": "z"} - label_set = meter.get_label_set(kvp) - label_set2 = meter.get_label_set(kvp) - self.assertIs(label_set, label_set2) - class TestMetric(unittest.TestCase): def test_get_handle(self): From fcb46aa769c3283fb9159f56832c0eff4fad51cd Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 28 Jan 2020 12:09:13 -0800 Subject: [PATCH 24/41] Remove lock implementation from aggregate abc, fix comments, rename checkpoint --- .../src/opentelemetry/sdk/metrics/export/aggregate.py | 7 +++---- .../src/opentelemetry/sdk/metrics/export/batcher.py | 6 +++--- opentelemetry-sdk/tests/metrics/export/test_export.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 3976b9bbcfc..42d0da1b09c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -26,14 +26,13 @@ class Aggregator(abc.ABC): def __init__(self): self.current = None self.check_point = None - self._lock = threading.Lock() @abc.abstractmethod def update(self, value): """Updates the current with the new value.""" @abc.abstractmethod - def checkpoint(self): + def take_checkpoint(self): """Stores a snapshot of the current value.""" @abc.abstractmethod @@ -48,12 +47,12 @@ def __init__(self): super().__init__() self.current = 0 self.check_point = 0 + self._lock = threading.Lock() def update(self, value): self.current += value - def checkpoint(self): - # Lock-free algorithm? + def take_checkpoint(self): with self._lock: self.check_point = self.current self.current = 0 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 39e0a6f4598..32dd4613501 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -27,8 +27,8 @@ class Batcher(abc.ABC): """Base class for all batcher types. The batcher is responsible for storing the aggregators and aggregated - values received from updates from metrics in the meter. The stored values will be - sent to an exporter for exporting. + values received from updates from metrics in the meter. The stored values + will be sent to an exporter for exporting. """ def __init__(self, keep_state: bool): @@ -92,7 +92,7 @@ class UngroupedBatcher(Batcher): def process(self, record): # Checkpoints the current aggregator value to be collected for export - record.aggregator.checkpoint() + record.aggregator.take_checkpoint() batch_key = (record.metric, record.label_set) batch_value = self._batch_map.get(batch_key) aggregator = record.aggregator diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 1152953ecd3..6f2b0713f11 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -222,7 +222,7 @@ def test_counter_update(self): def test_counter_check_point(self): counter = CounterAggregator() counter.update(2.0) - counter.checkpoint() + counter.take_checkpoint() self.assertEqual(counter.current, 0) self.assertEqual(counter.check_point, 2.0) From 0ba6611d727210f95d06cb6c3fd3f8a91d14d7a5 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 28 Jan 2020 12:54:08 -0800 Subject: [PATCH 25/41] fix lint --- opentelemetry-sdk/tests/metrics/test_metrics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 2407173507c..3a08433e8da 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -139,7 +139,6 @@ def test_get_label_set(self): meter = metrics.Meter() kvp = {"environment": "staging", "a": "z"} label_set = meter.get_label_set(kvp) - kvp2 = {"environment": "staging", "a": "z"} label_set2 = meter.get_label_set(kvp) labels = set([label_set, label_set2]) self.assertEqual(len(labels), 1) From 35697d429bad1308094b987bb7263a6833a37977 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 28 Jan 2020 13:10:46 -0800 Subject: [PATCH 26/41] Fix lint --- opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index a7c74f850ea..ea16878a7bb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -28,7 +28,9 @@ class LabelSet(metrics_api.LabelSet): """See `opentelemetry.metrics.LabelSet`.""" - def __init__(self, labels: Dict[str, str] = {}): + def __init__(self, labels: Dict[str, str] = None): + if labels is None: + labels = {} # LabelSet properties used only in dictionaries for fast lookup self._labels = tuple(labels.items()) self._encoded = tuple(sorted(labels.items())) From 9578dbaa407457d3dc51866cf02826542d9918ce Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 4 Feb 2020 14:18:12 -0800 Subject: [PATCH 27/41] Add no-op implementations --- .../src/opentelemetry/metrics/__init__.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 37d317fab03..d3e52c4d922 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -40,6 +40,27 @@ class DefaultMetricHandle: Used when no MetricHandle implementation is available. """ + def add(self, value: ValueT) -> None: + """No-op implementation of `CounterHandle` add. + + Args: + value: The value to record to the handle. + """ + + def set(self, value: ValueT) -> None: + """No-op implementation of `GaugeHandle` set. + + Args: + value: The value to record to the handle. + """ + + def record(self, value: ValueT) -> None: + """No-op implementation of `MeasureHandle` record. + + Args: + value: The value to record to the handle. + """ + class CounterHandle: def add(self, value: ValueT) -> None: @@ -121,6 +142,30 @@ def get_handle(self, label_set: LabelSet) -> "DefaultMetricHandle": """ return DefaultMetricHandle() + def add(self, value: ValueT, label_set: LabelSet) -> None: + """No-op implementation of `Counter` add. + + Args: + value: The value to add to the counter metric. + label_set: `LabelSet` to associate with the returned handle. + """ + + def set(self, value: ValueT, label_set: LabelSet) -> None: + """No-op implementation of `Gauge` set. + + Args: + value: The value to set the gauge metric to. + label_set: `LabelSet` to associate with the returned handle. + """ + + def record(self, value: ValueT, label_set: LabelSet) -> None: + """No-op implementation of `Measure` record. + + Args: + value: The value to record to this measure metric. + label_set: `LabelSet` to associate with the returned handle. + """ + class Counter(Metric): """A counter type metric that expresses the computation of a sum.""" From f4d82e71ea0f9698006322bcf71e63d13ae57ef7 Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 4 Feb 2020 15:08:38 -0800 Subject: [PATCH 28/41] Fix examples --- examples/metrics/non_stateful.py | 4 ++-- examples/metrics/record.py | 15 ++++++++++++--- examples/metrics/stateful.py | 4 ++-- .../src/opentelemetry/metrics/__init__.py | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/metrics/non_stateful.py b/examples/metrics/non_stateful.py index fb172e798ee..87fbca6ae69 100644 --- a/examples/metrics/non_stateful.py +++ b/examples/metrics/non_stateful.py @@ -29,8 +29,8 @@ # checkpoints which describe the updates of a single collection period (deltas) batcher = UngroupedBatcher(False) # Meter is responsible for creating and recording metrics -meter = Meter(batcher) -metrics.set_preferred_meter_implementation(meter) +metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) +meter = metrics.meter() # exporter to export metrics to the console exporter = ConsoleMetricsExporter() # controller collects metrics created from meter and exports it via the diff --git a/examples/metrics/record.py b/examples/metrics/record.py index d2360f19c9b..1191cf54aac 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -23,8 +23,8 @@ from opentelemetry.sdk.metrics.export.controller import PushController # Meter is responsible for creating and recording metrics -meter = Meter() -metrics.set_preferred_meter_implementation(meter) +metrics.set_preferred_meter_implementation(lambda _: Meter()) +meter = metrics.meter() # exporter to export metrics to the console exporter = ConsoleMetricsExporter() # controller collects metrics created from meter and exports it via the @@ -41,6 +41,15 @@ ("environment",), ) +counter2 = meter.create_metric( + "available memory2", + "available memory2", + "bytes", + int, + Counter, + ("environment",), +) + # Labelsets are used to identify key-values that are associated with a specific # metric that you want to record. These are useful for pre-aggregation and can # be used to store custom dimensions pertaining to a metric @@ -65,4 +74,4 @@ # You can record metrics in a batch by passing in a labelset and a sequence of # (metric, value) pairs. The value would be recorded for each metric using the # specified labelset for each. -meter.record_batch(label_set, [(counter, 50)]) +meter.record_batch(label_set, [(counter, 50), (counter2, 70)]) diff --git a/examples/metrics/stateful.py b/examples/metrics/stateful.py index e0944752678..b4f200d098b 100644 --- a/examples/metrics/stateful.py +++ b/examples/metrics/stateful.py @@ -32,8 +32,8 @@ batcher = UngroupedBatcher(True) # If a batcher is not provded, a default batcher is used # Meter is responsible for creating and recording metrics -meter = Meter(batcher) -metrics.set_preferred_meter_implementation(meter) +metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) +meter = metrics.meter() # exporter to export metrics to the console exporter = ConsoleMetricsExporter() # controller collects metrics created from meter and exports it via the diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index d3e52c4d922..26ccd95c6a6 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -347,7 +347,7 @@ def meter() -> Meter: if _METER is None: # pylint:disable=protected-access - _METER = loader._load_impl(DefaultMeter, _METER_FACTORY) + _METER = loader._load_impl(Meter, _METER_FACTORY) del _METER_FACTORY return _METER From a7a9c54e7715ad1bfbdfc91c56b905d7f64e424d Mon Sep 17 00:00:00 2001 From: Leighton Date: Tue, 4 Feb 2020 16:37:53 -0800 Subject: [PATCH 29/41] re-add default --- opentelemetry-api/src/opentelemetry/metrics/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 26ccd95c6a6..d3e52c4d922 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -347,7 +347,7 @@ def meter() -> Meter: if _METER is None: # pylint:disable=protected-access - _METER = loader._load_impl(Meter, _METER_FACTORY) + _METER = loader._load_impl(DefaultMeter, _METER_FACTORY) del _METER_FACTORY return _METER From fe44402790b8314f35fb978712f50c4f607e278b Mon Sep 17 00:00:00 2001 From: Leighton Date: Wed, 5 Feb 2020 11:08:09 -0800 Subject: [PATCH 30/41] Build --- .../src/opentelemetry_example_app/metrics_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py index 2f423619021..f4ccc35d069 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py @@ -13,7 +13,7 @@ # limitations under the License. # """ -This module serves as an example for a simple application using metrics +This module serves as an example for a simple application using metrics. """ from opentelemetry import metrics From 6588aeea3605fb293b2d0b0bc00d71c8a0d8e7e8 Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 6 Feb 2020 16:13:12 -0800 Subject: [PATCH 31/41] address comments --- examples/metrics/non_stateful.py | 6 +- examples/metrics/record.py | 12 ++-- examples/metrics/stateful.py | 12 ++-- .../sdk/metrics/export/aggregate.py | 15 ++--- .../sdk/metrics/export/batcher.py | 31 ++++------ .../sdk/metrics/export/controller.py | 2 +- .../tests/metrics/export/test_export.py | 61 ++++++++++--------- 7 files changed, 64 insertions(+), 75 deletions(-) diff --git a/examples/metrics/non_stateful.py b/examples/metrics/non_stateful.py index 87fbca6ae69..af60b511972 100644 --- a/examples/metrics/non_stateful.py +++ b/examples/metrics/non_stateful.py @@ -38,9 +38,9 @@ controller = PushController(meter, exporter, 5) counter = meter.create_metric( - "available memory", - "available memory", - "bytes", + "requests", + "number of requests", + "requests", int, Counter, ("environment",), diff --git a/examples/metrics/record.py b/examples/metrics/record.py index 1191cf54aac..a12ad498b59 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -33,18 +33,18 @@ # Example to show how to record using the meter counter = meter.create_metric( - "available memory", - "available memory", - "bytes", + "requests", + "number of requests", + "requests", int, Counter, ("environment",), ) counter2 = meter.create_metric( - "available memory2", - "available memory2", - "bytes", + "clicks", + "number of clicks", + "clicks", int, Counter, ("environment",), diff --git a/examples/metrics/stateful.py b/examples/metrics/stateful.py index b4f200d098b..2cd47dcd906 100644 --- a/examples/metrics/stateful.py +++ b/examples/metrics/stateful.py @@ -41,18 +41,18 @@ controller = PushController(meter, exporter, 5) counter = meter.create_metric( - "available memory", - "available memory", - "bytes", + "requests", + "number of requests", + "requests", int, Counter, ("environment",), ) counter2 = meter.create_metric( - "available memory2", - "available memory2", - "bytes2", + "clicks", + "number of clicks", + "clicks", int, Counter, ("environment",), diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 42d0da1b09c..642fe1cdfe4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -13,19 +13,18 @@ # limitations under the License. import abc -import threading class Aggregator(abc.ABC): """Base class for aggregators. Aggregators are responsible for holding aggregated values and taking a - snapshot of these values upon export (check_point). + snapshot of these values upon export (checkpoint). """ def __init__(self): self.current = None - self.check_point = None + self.checkpoint = None @abc.abstractmethod def update(self, value): @@ -46,16 +45,14 @@ class CounterAggregator(Aggregator): def __init__(self): super().__init__() self.current = 0 - self.check_point = 0 - self._lock = threading.Lock() + self.checkpoint = 0 def update(self, value): self.current += value def take_checkpoint(self): - with self._lock: - self.check_point = self.current - self.current = 0 + self.checkpoint = self.current + self.current = 0 def merge(self, other): - self.check_point += other.check_point + self.checkpoint += other.checkpoint diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 32dd4613501..c81db0fe740 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -31,24 +31,16 @@ class Batcher(abc.ABC): will be sent to an exporter for exporting. """ - def __init__(self, keep_state: bool): + def __init__(self, stateful: bool): self._batch_map = {} - # keep_state=True indicates the batcher computes checkpoints from over + # stateful=True indicates the batcher computes checkpoints from over # the process lifetime. False indicates the batcher computes # checkpoints which describe the updates of a single collection period # (deltas) - self.keep_state = keep_state - - @property - def batch_map(self): - return self._batch_map - - @batch_map.setter - def batch_map(self, value): - self._batch_map = value + self.stateful = stateful def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: - """Returns an aggregator based off metric type. + """Returns an aggregator based on metric type. Aggregators keep track of and updates values when metrics get updated. """ @@ -58,16 +50,15 @@ def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: # TODO: Add other aggregators return CounterAggregator() - def check_point_set(self) -> Sequence[MetricRecord]: + def checkpoint_set(self) -> Sequence[MetricRecord]: """Returns a list of MetricRecords used for exporting. The list of MetricRecords is a snapshot created from the current data in all of the aggregators in this batcher. """ metric_records = [] - for key, value in self._batch_map.items(): - # Batch map is in the format: (metric, label_set): aggregator - metric_records.append(MetricRecord(value, key[1], key[0])) + for (metric, label_set), aggregator in self._batch_map.items(): + metric_records.append(MetricRecord(aggregator, label_set, metric)) return metric_records def finished_collection(self): @@ -75,7 +66,7 @@ def finished_collection(self): For batchers that are stateless, resets the batch map. """ - if not self.keep_state: + if not self.stateful: self._batch_map = {} @abc.abstractmethod @@ -97,11 +88,11 @@ def process(self, record): batch_value = self._batch_map.get(batch_key) aggregator = record.aggregator if batch_value: - # Update the stored checkpointed value if exists. This is for cases - # when an update comes at the same time as a checkpoint call + # Update the stored checkpointed value if exists. The call to merge + # here combines only identical records (same key). batch_value.merge(aggregator) return - if self.keep_state: + if self.stateful: # if stateful batcher, create a copy of the aggregator and update # it with the current checkpointed value for long-term storage aggregator = self.aggregator_for(record.metric.__class__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 2045d79fff7..235c4e4b8e6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -34,7 +34,7 @@ def run(self): while not self.finished.wait(self.interval): self.tick() - def cancel(self): + def shutdown(self): self.finished.set() def tick(self): diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 6f2b0713f11..7b288d39ee6 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -25,6 +25,7 @@ from opentelemetry.sdk.metrics.export.controller import PushController +# pylint: disable=protected-access class TestConsoleMetricsExporter(unittest.TestCase): # pylint: disable=no-self-use def test_export(self): @@ -78,9 +79,9 @@ def test_check_point_set(self): ) aggregator.update(1.0) label_set = metrics.LabelSet() - batch_map = {} - batch_map[(metric, label_set)] = aggregator - batcher.batch_map = batch_map + _batch_map = {} + _batch_map[(metric, label_set)] = aggregator + batcher._batch_map = _batch_map records = batcher.check_point_set() self.assertEqual(len(records), 1) self.assertEqual(records[0].metric, metric) @@ -106,11 +107,11 @@ def test_finished_collection_stateless(self): ) aggregator.update(1.0) label_set = metrics.LabelSet() - batch_map = {} - batch_map[(metric, label_set)] = aggregator - batcher.batch_map = batch_map + _batch_map = {} + _batch_map[(metric, label_set)] = aggregator + batcher._batch_map = _batch_map batcher.finished_collection() - self.assertEqual(len(batcher.batch_map), 0) + self.assertEqual(len(batcher._batch_map), 0) def test_finished_collection_stateful(self): meter = metrics.Meter() @@ -126,11 +127,11 @@ def test_finished_collection_stateful(self): ) aggregator.update(1.0) label_set = metrics.LabelSet() - batch_map = {} - batch_map[(metric, label_set)] = aggregator - batcher.batch_map = batch_map + _batch_map = {} + _batch_map[(metric, label_set)] = aggregator + batcher._batch_map = _batch_map batcher.finished_collection() - self.assertEqual(len(batcher.batch_map), 1) + self.assertEqual(len(batcher._batch_map), 1) # TODO: Abstract the logic once other batchers implemented def test_ungrouped_batcher_process_exists(self): @@ -147,17 +148,17 @@ def test_ungrouped_batcher_process_exists(self): ("environment",), ) label_set = metrics.LabelSet() - batch_map = {} - batch_map[(metric, label_set)] = aggregator + _batch_map = {} + _batch_map[(metric, label_set)] = aggregator aggregator2.update(1.0) - batcher.batch_map = batch_map + batcher._batch_map = _batch_map record = metrics.Record(metric, label_set, aggregator2) batcher.process(record) - self.assertEqual(len(batcher.batch_map), 1) - self.assertIsNotNone(batcher.batch_map.get((metric, label_set))) - self.assertEqual(batcher.batch_map.get((metric, label_set)).current, 0) + self.assertEqual(len(batcher._batch_map), 1) + self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) + self.assertEqual(batcher._batch_map.get((metric, label_set)).current, 0) self.assertEqual( - batcher.batch_map.get((metric, label_set)).check_point, 1.0 + batcher._batch_map.get((metric, label_set)).check_point, 1.0 ) def test_ungrouped_batcher_process_not_exists(self): @@ -173,16 +174,16 @@ def test_ungrouped_batcher_process_not_exists(self): ("environment",), ) label_set = metrics.LabelSet() - batch_map = {} + _batch_map = {} aggregator.update(1.0) - batcher.batch_map = batch_map + batcher._batch_map = _batch_map record = metrics.Record(metric, label_set, aggregator) batcher.process(record) - self.assertEqual(len(batcher.batch_map), 1) - self.assertIsNotNone(batcher.batch_map.get((metric, label_set))) - self.assertEqual(batcher.batch_map.get((metric, label_set)).current, 0) + self.assertEqual(len(batcher._batch_map), 1) + self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) + self.assertEqual(batcher._batch_map.get((metric, label_set)).current, 0) self.assertEqual( - batcher.batch_map.get((metric, label_set)).check_point, 1.0 + batcher._batch_map.get((metric, label_set)).check_point, 1.0 ) def test_ungrouped_batcher_process_not_stateful(self): @@ -198,16 +199,16 @@ def test_ungrouped_batcher_process_not_stateful(self): ("environment",), ) label_set = metrics.LabelSet() - batch_map = {} + _batch_map = {} aggregator.update(1.0) - batcher.batch_map = batch_map + batcher._batch_map = _batch_map record = metrics.Record(metric, label_set, aggregator) batcher.process(record) - self.assertEqual(len(batcher.batch_map), 1) - self.assertIsNotNone(batcher.batch_map.get((metric, label_set))) - self.assertEqual(batcher.batch_map.get((metric, label_set)).current, 0) + self.assertEqual(len(batcher._batch_map), 1) + self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) + self.assertEqual(batcher._batch_map.get((metric, label_set)).current, 0) self.assertEqual( - batcher.batch_map.get((metric, label_set)).check_point, 1.0 + batcher._batch_map.get((metric, label_set)).check_point, 1.0 ) From d99a2a35078aa8a6d05a5f79bdb926e2949e5704 Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 6 Feb 2020 16:43:00 -0800 Subject: [PATCH 32/41] Shutdown exporter on exit --- .../src/opentelemetry/metrics/__init__.py | 2 +- .../opentelemetry/sdk/metrics/export/__init__.py | 2 +- .../opentelemetry/sdk/metrics/export/controller.py | 14 +++++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index d3e52c4d922..26ccd95c6a6 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -347,7 +347,7 @@ def meter() -> Meter: if _METER is None: # pylint:disable=protected-access - _METER = loader._load_impl(DefaultMeter, _METER_FACTORY) + _METER = loader._load_impl(Meter, _METER_FACTORY) del _METER_FACTORY return _METER diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py index 76ac4e66114..6901a4efe46 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -74,7 +74,7 @@ def export( type(self).__name__, record.metric, record.label_set.labels, - record.aggregator.check_point, + record.aggregator.checkpoint, ) ) return MetricsExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 235c4e4b8e6..29906bf5808 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import atexit import threading @@ -21,13 +22,16 @@ class PushController(threading.Thread): Uses a worker thread that periodically collects metrics for exporting, exports them and performs some post-processing. """ - - def __init__(self, meter, exporter, interval): + daemon = True + def __init__(self, meter, exporter, interval, shutdown_on_exit=True): super().__init__() self.meter = meter self.exporter = exporter self.interval = interval self.finished = threading.Event() + self._atexit_handler = None + if shutdown_on_exit: + self._atexit_handler = atexit.register(self.shutdown) self.start() def run(self): @@ -36,11 +40,15 @@ def run(self): def shutdown(self): self.finished.set() + self.exporter.shutdown() + if self._atexit_handler is not None: + atexit.unregister(self._atexit_handler) + self._atexit_handler = None def tick(self): # Collect all of the meter's metrics to be exported self.meter.collect() # Export the given metrics in the batcher - self.exporter.export(self.meter.batcher.check_point_set()) + self.exporter.export(self.meter.batcher.checkpoint_set()) # Perform post-exporting logic based on batcher configuration self.meter.batcher.finished_collection() From 1c9d44dae2832397d0a78035c8e2e167592ccc8f Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 6 Feb 2020 17:00:10 -0800 Subject: [PATCH 33/41] fix tests --- .../tests/metrics/export/test_export.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 7b288d39ee6..216a4aa5afe 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -47,7 +47,7 @@ def test_export(self): ConsoleMetricsExporter.__name__, metric, label_set.labels, - aggregator.check_point, + aggregator.checkpoint, ) with mock.patch("sys.stdout") as mock_stdout: exporter.export([record]) @@ -65,7 +65,7 @@ def test_aggregator_for_counter(self): # TODO: Add other aggregator tests - def test_check_point_set(self): + def test_checkpoint_set(self): meter = metrics.Meter() batcher = UngroupedBatcher(True) aggregator = CounterAggregator() @@ -82,15 +82,15 @@ def test_check_point_set(self): _batch_map = {} _batch_map[(metric, label_set)] = aggregator batcher._batch_map = _batch_map - records = batcher.check_point_set() + records = batcher.checkpoint_set() self.assertEqual(len(records), 1) self.assertEqual(records[0].metric, metric) self.assertEqual(records[0].label_set, label_set) self.assertEqual(records[0].aggregator, aggregator) - def test_check_point_set_empty(self): + def test_checkpoint_set_empty(self): batcher = UngroupedBatcher(True) - records = batcher.check_point_set() + records = batcher.checkpoint_set() self.assertEqual(len(records), 0) def test_finished_collection_stateless(self): @@ -158,7 +158,7 @@ def test_ungrouped_batcher_process_exists(self): self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) self.assertEqual(batcher._batch_map.get((metric, label_set)).current, 0) self.assertEqual( - batcher._batch_map.get((metric, label_set)).check_point, 1.0 + batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 ) def test_ungrouped_batcher_process_not_exists(self): @@ -183,7 +183,7 @@ def test_ungrouped_batcher_process_not_exists(self): self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) self.assertEqual(batcher._batch_map.get((metric, label_set)).current, 0) self.assertEqual( - batcher._batch_map.get((metric, label_set)).check_point, 1.0 + batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 ) def test_ungrouped_batcher_process_not_stateful(self): @@ -208,7 +208,7 @@ def test_ungrouped_batcher_process_not_stateful(self): self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) self.assertEqual(batcher._batch_map.get((metric, label_set)).current, 0) self.assertEqual( - batcher._batch_map.get((metric, label_set)).check_point, 1.0 + batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 ) @@ -220,20 +220,20 @@ def test_counter_update(self): counter.update(2.0) self.assertEqual(counter.current, 3.0) - def test_counter_check_point(self): + def test_counter_checkpoint(self): counter = CounterAggregator() counter.update(2.0) counter.take_checkpoint() self.assertEqual(counter.current, 0) - self.assertEqual(counter.check_point, 2.0) + self.assertEqual(counter.checkpoint, 2.0) def test_counter_merge(self): counter = CounterAggregator() counter2 = CounterAggregator() - counter.check_point = 1.0 - counter2.check_point = 3.0 + counter.checkpoint = 1.0 + counter2.checkpoint = 3.0 counter.merge(counter2) - self.assertEqual(counter.check_point, 4.0) + self.assertEqual(counter.checkpoint, 4.0) class TestController(unittest.TestCase): From 56f68e85c941842d13cb331d52996ab219a172fc Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 6 Feb 2020 17:14:06 -0800 Subject: [PATCH 34/41] fix test --- opentelemetry-sdk/tests/metrics/export/test_export.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 216a4aa5afe..f24474f904a 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -243,5 +243,6 @@ def test_push_controller(self): controller = PushController(meter, exporter, 5.0) meter.collect.assert_not_called() exporter.export.assert_not_called() - controller.cancel() + controller.shutdown() self.assertTrue(controller.finished.isSet()) + exporter.shutdown.assert_any_call() From cb51341fb4b320f7ee04058253d64f4f28a50b28 Mon Sep 17 00:00:00 2001 From: Leighton Date: Thu, 6 Feb 2020 17:33:12 -0800 Subject: [PATCH 35/41] black --- examples/metrics/record.py | 7 +------ examples/metrics/stateful.py | 7 +------ .../tests/test_flask_integration.py | 5 +---- .../opentelemetry/sdk/metrics/export/controller.py | 2 ++ .../tests/metrics/export/test_export.py | 12 +++++++++--- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/examples/metrics/record.py b/examples/metrics/record.py index a12ad498b59..ac4a195945a 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -42,12 +42,7 @@ ) counter2 = meter.create_metric( - "clicks", - "number of clicks", - "clicks", - int, - Counter, - ("environment",), + "clicks", "number of clicks", "clicks", int, Counter, ("environment",) ) # Labelsets are used to identify key-values that are associated with a specific diff --git a/examples/metrics/stateful.py b/examples/metrics/stateful.py index 2cd47dcd906..41b49a13523 100644 --- a/examples/metrics/stateful.py +++ b/examples/metrics/stateful.py @@ -50,12 +50,7 @@ ) counter2 = meter.create_metric( - "clicks", - "number of clicks", - "clicks", - int, - Counter, - ("environment",), + "clicks", "number of clicks", "clicks", int, Counter, ("environment",) ) # Labelsets are used to identify key-values that are associated with a specific diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index 9d2f2560118..b9454b94472 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -78,10 +78,7 @@ def assert_environ(): def test_simple(self): expected_attrs = expected_attributes( - { - "http.target": "/hello/123", - "http.route": "/hello/", - } + {"http.target": "/hello/123", "http.route": "/hello/"} ) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py index 29906bf5808..03c857f04d9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/controller.py @@ -22,7 +22,9 @@ class PushController(threading.Thread): Uses a worker thread that periodically collects metrics for exporting, exports them and performs some post-processing. """ + daemon = True + def __init__(self, meter, exporter, interval, shutdown_on_exit=True): super().__init__() self.meter = meter diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index f24474f904a..816bfcfca9c 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -156,7 +156,9 @@ def test_ungrouped_batcher_process_exists(self): batcher.process(record) self.assertEqual(len(batcher._batch_map), 1) self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) - self.assertEqual(batcher._batch_map.get((metric, label_set)).current, 0) + self.assertEqual( + batcher._batch_map.get((metric, label_set)).current, 0 + ) self.assertEqual( batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 ) @@ -181,7 +183,9 @@ def test_ungrouped_batcher_process_not_exists(self): batcher.process(record) self.assertEqual(len(batcher._batch_map), 1) self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) - self.assertEqual(batcher._batch_map.get((metric, label_set)).current, 0) + self.assertEqual( + batcher._batch_map.get((metric, label_set)).current, 0 + ) self.assertEqual( batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 ) @@ -206,7 +210,9 @@ def test_ungrouped_batcher_process_not_stateful(self): batcher.process(record) self.assertEqual(len(batcher._batch_map), 1) self.assertIsNotNone(batcher._batch_map.get((metric, label_set))) - self.assertEqual(batcher._batch_map.get((metric, label_set)).current, 0) + self.assertEqual( + batcher._batch_map.get((metric, label_set)).current, 0 + ) self.assertEqual( batcher._batch_map.get((metric, label_set)).checkpoint, 1.0 ) From b1bfa38cc2da833f9bb44a8a6950ded1dc2f8a96 Mon Sep 17 00:00:00 2001 From: Leighton Date: Fri, 7 Feb 2020 16:45:25 -0800 Subject: [PATCH 36/41] Address comments --- examples/metrics/{non_stateful.py => stateless.py} | 2 +- .../src/opentelemetry_example_app/metrics_example.py | 2 +- .../tests/test_flask_integration.py | 5 ++++- .../tests/test_jaeger_exporter.py | 4 ++-- .../src/opentelemetry/ext/zipkin/__init__.py | 5 +---- .../tests/test_zipkin_exporter.py | 9 ++------- opentelemetry-api/src/opentelemetry/metrics/__init__.py | 8 ++++---- opentelemetry-sdk/tests/trace/test_trace.py | 5 ++--- 8 files changed, 17 insertions(+), 23 deletions(-) rename examples/metrics/{non_stateful.py => stateless.py} (99%) diff --git a/examples/metrics/non_stateful.py b/examples/metrics/stateless.py similarity index 99% rename from examples/metrics/non_stateful.py rename to examples/metrics/stateless.py index af60b511972..042d9efc204 100644 --- a/examples/metrics/non_stateful.py +++ b/examples/metrics/stateless.py @@ -57,6 +57,6 @@ counter.add(50, label_set) # exported value should be 50 due to non-stateful batcher -time.sleep(5) +time.sleep(20) # Following exported values would be 0 diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py index f4ccc35d069..2f423619021 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py @@ -13,7 +13,7 @@ # limitations under the License. # """ -This module serves as an example for a simple application using metrics. +This module serves as an example for a simple application using metrics """ from opentelemetry import metrics diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index b9454b94472..9d2f2560118 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -78,7 +78,10 @@ def assert_environ(): def test_simple(self): expected_attrs = expected_attributes( - {"http.target": "/hello/123", "http.route": "/hello/"} + { + "http.target": "/hello/123", + "http.route": "/hello/", + } ) resp = self.client.get("/hello/123") self.assertEqual(200, resp.status_code) diff --git a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py index 23fce98b79a..08c5a4adeda 100644 --- a/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py +++ b/ext/opentelemetry-ext-jaeger/tests/test_jaeger_exporter.py @@ -163,7 +163,7 @@ def test_translate_to_jaeger(self): vLong=StatusCanonicalCode.OK.value, ), jaeger.Tag( - key="status.message", vType=jaeger.TagType.STRING, vStr=None, + key="status.message", vType=jaeger.TagType.STRING, vStr=None ), jaeger.Tag( key="span.kind", @@ -246,7 +246,7 @@ def test_translate_to_jaeger(self): vStr=trace_api.SpanKind.CLIENT.name, ), jaeger.Tag( - key="error", vType=jaeger.TagType.BOOL, vBool=True, + key="error", vType=jaeger.TagType.BOOL, vBool=True ), ], references=[ diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index e0b5791d1e1..fec4da8c3ed 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -101,10 +101,7 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: def _translate_to_zipkin(self, spans: Sequence[Span]): - local_endpoint = { - "serviceName": self.service_name, - "port": self.port, - } + local_endpoint = {"serviceName": self.service_name, "port": self.port} if self.ipv4 is not None: local_endpoint["ipv4"] = self.ipv4 diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index e2bdb413052..467bc610bd8 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -114,9 +114,7 @@ def test_export(self): ) span_context = trace_api.SpanContext( - trace_id, - span_id, - trace_options=TraceOptions(TraceOptions.SAMPLED), + trace_id, span_id, trace_options=TraceOptions(TraceOptions.SAMPLED) ) parent_context = trace_api.SpanContext(trace_id, parent_id) other_context = trace_api.SpanContext(trace_id, other_id) @@ -168,10 +166,7 @@ def test_export(self): otel_spans[2].end(end_time=end_times[2]) service_name = "test-service" - local_endpoint = { - "serviceName": service_name, - "port": 9411, - } + local_endpoint = {"serviceName": service_name, "port": 9411} exporter = ZipkinSpanExporter(service_name) expected = [ diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 26ccd95c6a6..2185bfb7659 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -44,14 +44,14 @@ def add(self, value: ValueT) -> None: """No-op implementation of `CounterHandle` add. Args: - value: The value to record to the handle. + value: The value to add to the handle. """ def set(self, value: ValueT) -> None: """No-op implementation of `GaugeHandle` set. Args: - value: The value to record to the handle. + value: The value to set to the handle. """ def record(self, value: ValueT) -> None: @@ -67,7 +67,7 @@ def add(self, value: ValueT) -> None: """Increases the value of the handle by ``value``. Args: - value: The value to record to the handle. + value: The value to add to the handle. """ @@ -76,7 +76,7 @@ def set(self, value: ValueT) -> None: """Sets the current value of the handle to ``value``. Args: - value: The value to record to the handle. + value: The value to set to the handle. """ diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 449fb51e8ee..fff520bcb2a 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -630,14 +630,13 @@ def test_ended_span(self): self.assertEqual(root.name, "root") new_status = trace_api.status.Status( - trace_api.status.StatusCanonicalCode.CANCELLED, "Test description", + trace_api.status.StatusCanonicalCode.CANCELLED, "Test description" ) with self.assertLogs(level=WARNING): root.set_status(new_status) self.assertEqual( - root.status.canonical_code, - trace_api.status.StatusCanonicalCode.OK, + root.status.canonical_code, trace_api.status.StatusCanonicalCode.OK ) def test_error_status(self): From f135cc856ae7fa663428c5d0e7d6008fc1695b98 Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Sun, 9 Feb 2020 22:55:28 -0800 Subject: [PATCH 37/41] Make default meter a DefaultMeter --- .../src/opentelemetry/metrics/__init__.py | 7 +++- .../tests/metrics/test_metrics.py | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 2185bfb7659..596d3ec70ab 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -347,7 +347,12 @@ def meter() -> Meter: if _METER is None: # pylint:disable=protected-access - _METER = loader._load_impl(Meter, _METER_FACTORY) + try: + _METER = loader._load_impl(Meter, _METER_FACTORY) + except TypeError: + # if we raised an exception trying to instantiate an + # abstract class, default to no-op tracer impl + _METER = DefaultMeter() del _METER_FACTORY return _METER diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 3d37a58b6ae..4ecdffecb97 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -13,6 +13,8 @@ # limitations under the License. import unittest +from contextlib import contextmanager +from unittest import mock from opentelemetry import metrics @@ -90,3 +92,33 @@ def test_gauge_handle(self): def test_measure_handle(self): handle = metrics.MeasureHandle() handle.record(1) + + +@contextmanager +def patch_metrics_globals(meter=None, meter_factory=None): + """Mock metrics._METER and metrics._METER_FACTORY. + + This prevents previous changes to these values from affecting the code in + this scope, and prevents changes in this scope from leaking out and + affecting other tests. + """ + with mock.patch("opentelemetry.metrics._METER", meter): + with mock.patch("opentelemetry.metrics._METER_FACTORY", meter_factory): + yield + + +class TestGlobals(unittest.TestCase): + def test_meter_default_factory(self): + """Check that the default meter is a DefaultMeter.""" + with patch_metrics_globals(): + meter = metrics.meter() + self.assertIsInstance(meter, metrics.DefaultMeter) + # Check that we don't create a new instance on each call + self.assertIs(meter, metrics.meter()) + + def test_meter_custom_factory(self): + """Check that we use the provided factory for custom global meters.""" + mock_meter = mock.Mock(metrics.Meter) + with patch_metrics_globals(meter_factory=lambda _: mock_meter): + meter = metrics.meter() + self.assertIs(meter, mock_meter) From f73da8d635c3966a7e538e8e5d3e257abad40f6d Mon Sep 17 00:00:00 2001 From: Leighton Date: Mon, 10 Feb 2020 10:13:35 -0800 Subject: [PATCH 38/41] Fix typing --- opentelemetry-api/src/opentelemetry/metrics/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 596d3ec70ab..66f4118917c 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -348,7 +348,7 @@ def meter() -> Meter: if _METER is None: # pylint:disable=protected-access try: - _METER = loader._load_impl(Meter, _METER_FACTORY) + _METER = loader._load_impl(Meter, _METER_FACTORY) # type: ignore except TypeError: # if we raised an exception trying to instantiate an # abstract class, default to no-op tracer impl From 1619575838d0016ed2c16b60498df492e9352a9b Mon Sep 17 00:00:00 2001 From: Leighton Date: Mon, 10 Feb 2020 10:16:14 -0800 Subject: [PATCH 39/41] add sleep --- examples/metrics/record.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/metrics/record.py b/examples/metrics/record.py index ac4a195945a..ef88a1e07ac 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -16,6 +16,7 @@ This module serves as an example for a simple application using metrics. It demonstrates the different ways you can record metrics via the meter. """ +import time from opentelemetry import metrics from opentelemetry.sdk.metrics import Counter, Meter @@ -70,3 +71,4 @@ # (metric, value) pairs. The value would be recorded for each metric using the # specified labelset for each. meter.record_batch(label_set, [(counter, 50), (counter2, 70)]) +time.sleep(100) From 6136987afc7f0a99670f72969ce005dba0d89696 Mon Sep 17 00:00:00 2001 From: Leighton Date: Mon, 10 Feb 2020 10:17:57 -0800 Subject: [PATCH 40/41] fix units --- examples/metrics/record.py | 4 ++-- examples/metrics/stateful.py | 4 ++-- examples/metrics/stateless.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/metrics/record.py b/examples/metrics/record.py index ef88a1e07ac..558350b405d 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -36,14 +36,14 @@ counter = meter.create_metric( "requests", "number of requests", - "requests", + 1, int, Counter, ("environment",), ) counter2 = meter.create_metric( - "clicks", "number of clicks", "clicks", int, Counter, ("environment",) + "clicks", "number of clicks", 1, int, Counter, ("environment",) ) # Labelsets are used to identify key-values that are associated with a specific diff --git a/examples/metrics/stateful.py b/examples/metrics/stateful.py index 41b49a13523..0c393830df8 100644 --- a/examples/metrics/stateful.py +++ b/examples/metrics/stateful.py @@ -43,14 +43,14 @@ counter = meter.create_metric( "requests", "number of requests", - "requests", + 1, int, Counter, ("environment",), ) counter2 = meter.create_metric( - "clicks", "number of clicks", "clicks", int, Counter, ("environment",) + "clicks", "number of clicks", 1, int, Counter, ("environment",) ) # Labelsets are used to identify key-values that are associated with a specific diff --git a/examples/metrics/stateless.py b/examples/metrics/stateless.py index 042d9efc204..84a2a2039c6 100644 --- a/examples/metrics/stateless.py +++ b/examples/metrics/stateless.py @@ -40,7 +40,7 @@ counter = meter.create_metric( "requests", "number of requests", - "requests", + 1, int, Counter, ("environment",), From 5f6883264880a01c823fa8b3972c72bb56b5e9ed Mon Sep 17 00:00:00 2001 From: Leighton Date: Mon, 10 Feb 2020 20:04:58 -0800 Subject: [PATCH 41/41] Fix lint --- examples/metrics/record.py | 7 +------ examples/metrics/stateful.py | 7 +------ examples/metrics/stateless.py | 7 +------ opentelemetry-api/src/opentelemetry/metrics/__init__.py | 2 +- opentelemetry-api/tests/metrics/test_metrics.py | 1 + 5 files changed, 5 insertions(+), 19 deletions(-) diff --git a/examples/metrics/record.py b/examples/metrics/record.py index 558350b405d..be68c8083ff 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -34,12 +34,7 @@ # Example to show how to record using the meter counter = meter.create_metric( - "requests", - "number of requests", - 1, - int, - Counter, - ("environment",), + "requests", "number of requests", 1, int, Counter, ("environment",) ) counter2 = meter.create_metric( diff --git a/examples/metrics/stateful.py b/examples/metrics/stateful.py index 0c393830df8..c43f795e228 100644 --- a/examples/metrics/stateful.py +++ b/examples/metrics/stateful.py @@ -41,12 +41,7 @@ controller = PushController(meter, exporter, 5) counter = meter.create_metric( - "requests", - "number of requests", - 1, - int, - Counter, - ("environment",), + "requests", "number of requests", 1, int, Counter, ("environment",) ) counter2 = meter.create_metric( diff --git a/examples/metrics/stateless.py b/examples/metrics/stateless.py index 84a2a2039c6..69213cbddd3 100644 --- a/examples/metrics/stateless.py +++ b/examples/metrics/stateless.py @@ -38,12 +38,7 @@ controller = PushController(meter, exporter, 5) counter = meter.create_metric( - "requests", - "number of requests", - 1, - int, - Counter, - ("environment",), + "requests", "number of requests", 1, int, Counter, ("environment",) ) # Labelsets are used to identify key-values that are associated with a specific diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 66f4118917c..3e04354d3c5 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -348,7 +348,7 @@ def meter() -> Meter: if _METER is None: # pylint:disable=protected-access try: - _METER = loader._load_impl(Meter, _METER_FACTORY) # type: ignore + _METER = loader._load_impl(Meter, _METER_FACTORY) # type: ignore except TypeError: # if we raised an exception trying to instantiate an # abstract class, default to no-op tracer impl diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 4ecdffecb97..3ec0f81c718 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -95,6 +95,7 @@ def test_measure_handle(self): @contextmanager +# type: ignore def patch_metrics_globals(meter=None, meter_factory=None): """Mock metrics._METER and metrics._METER_FACTORY.