From fb115680682a68df141cb42e3fa6af7bdf4c115d Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 12 Sep 2019 14:25:29 -0700 Subject: [PATCH] Metrics API with RFC 0003 (#87) * Create functions Comments for Meter More comments Add more comments Fix typos * fix lint * Fix lint * fix typing * Remove options, constructors, seperate labels * Consistent naming for float and int * Abstract time series * Use ABC * Fix typo * Fix docs * seperate measure classes * Add examples * fix lint * Update to RFC 0003 * Add spancontext, measurebatch * Fix docs * Fix comments * fix lint * fix lint * fix lint * skip examples * white space * fix spacing * fix imports * fix imports * LabelValues to str * Black formatting * fix isort * Remove aggregation * Fix names * Remove aggregation from docs * Fix lint --- docs/index.rst | 1 + docs/opentelemetry.metrics.rst | 14 + docs/opentelemetry.metrics.time_series.rst | 5 + .../src/opentelemetry/metrics/__init__.py | 304 ++++++++++++++++++ .../metrics/examples/pre_aggregated.py | 36 +++ .../src/opentelemetry/metrics/examples/raw.py | 45 +++ .../src/opentelemetry/metrics/time_series.py | 39 +++ 7 files changed, 444 insertions(+) create mode 100644 docs/opentelemetry.metrics.rst create mode 100644 docs/opentelemetry.metrics.time_series.rst create mode 100644 opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py create mode 100644 opentelemetry-api/src/opentelemetry/metrics/examples/raw.py create mode 100644 opentelemetry-api/src/opentelemetry/metrics/time_series.py diff --git a/docs/index.rst b/docs/index.rst index 8991f2b5df..4d968ccd64 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ abstract types for OpenTelemetry implementations. opentelemetry.context opentelemetry.loader + opentelemetry.metrics opentelemetry.trace diff --git a/docs/opentelemetry.metrics.rst b/docs/opentelemetry.metrics.rst new file mode 100644 index 0000000000..2d025d3197 --- /dev/null +++ b/docs/opentelemetry.metrics.rst @@ -0,0 +1,14 @@ +opentelemetry.metrics package +============================= + +Submodules +---------- + +.. toctree:: + + opentelemetry.metrics.time_series + +Module contents +--------------- + +.. automodule:: opentelemetry.metrics diff --git a/docs/opentelemetry.metrics.time_series.rst b/docs/opentelemetry.metrics.time_series.rst new file mode 100644 index 0000000000..16297d7eac --- /dev/null +++ b/docs/opentelemetry.metrics.time_series.rst @@ -0,0 +1,5 @@ +opentelemetry.metrics.time\_series module +========================================== + +.. automodule:: opentelemetry.metrics.time_series + diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index d853a7bcf6..68563bd492 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -11,3 +11,307 @@ # 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. + +""" +The OpenTelemetry metrics API describes the classes used to report raw +measurements, as well as metrics with known aggregation and labels. + +The `Meter` class is used to construct `Metric` s to record raw statistics +as well as metrics with predefined aggregation. + +See the `metrics api`_ spec for terminology and context clarification. + +.. _metrics api: + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-metrics.md + + +""" + +from abc import ABC, abstractmethod +from typing import List + +from opentelemetry.metrics.time_series import ( + CounterTimeSeries, + GaugeTimeSeries, + MeasureTimeSeries, +) +from opentelemetry.trace import SpanContext + +LabelKeys = List["LabelKey"] +LabelValues = List[str] + + +class Meter: + """An interface to allow the recording of metrics. + + `Metric` s are used for recording pre-defined aggregation (gauge and + counter), or raw values (measure) in which the aggregation and labels + for the exported metric are deferred. + """ + + def create_float_counter( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "FloatCounter": + """Creates a counter type metric that contains float values. + + Args: + name: The name of the counter. + description: Human readable description of the metric. + unit: Unit of the metric values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: A new `FloatCounter` + """ + + def create_int_counter( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "IntCounter": + """Creates a counter type metric that contains int values. + + Args: + name: The name of the counter. + description: Human readable description of the metric. + unit: Unit of the metric values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `IntCounter` + """ + + def create_float_gauge( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "FloatGauge": + """Creates a gauge type metric that contains float values. + + Args: + name: The name of the counter. + description: Human readable description of the metric. + unit: Unit of the metric values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `FloatGauge` + """ + + def create_int_gauge( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "IntGauge": + """Creates a gauge type metric that contains int values. + + Args: + name: The name of the counter. + description: Human readable description of the metric. + unit: Unit of the metric values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `IntGauge` + """ + + def create_int_measure( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "IntMeasure": + """Creates a measure used to record raw int values. + + Args: + name: The name of the measure. + description: Human readable description of this measure. + unit: Unit of the measure values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `IntMeasure` + """ + + def create_float_measure( + self, + name: str, + description: str, + unit: str, + label_keys: LabelKeys, + span_context: SpanContext = None, + ) -> "FloatMeasure": + """Creates a Measure used to record raw float values. + + Args: + name: the name of the measure + description: Human readable description of this measure. + unit: Unit of the measure values. + label_keys: list of keys for the labels with dynamic values. + Order of the list is important as the same order MUST be used + on recording when suppling values for these labels. + span_context: The `SpanContext` that identifies the `Span` + that the metric is associated with. + + Returns: + A new `FloatMeasure` + """ + + +class Metric(ABC): + """Base class for various types of metrics. + + Metric class that inherit from this class are specialized with the type of + time series that the metric holds. Metric is constructed from the meter. + """ + + @abstractmethod + def get_or_create_time_series(self, label_values: LabelValues) -> "object": + """Gets and returns a timeseries, a container for a cumulative value. + + If the provided label values are not already associated with this + metric, a new timeseries is returned, otherwise it returns the existing + timeseries with the exact label values. The timeseries returned + contains logic and behaviour specific to the type of metric that + overrides this function. + + Args: + label_values: A list of label values that will be associated + with the return timeseries. + """ + + def remove_time_series(self, label_values: LabelValues) -> None: + """Removes the timeseries from the Metric, if present. + + The timeseries with matching label values will be removed. + + args: + label_values: The list of label values to match against. + """ + + def clear(self) -> None: + """Removes all timeseries from the `Metric`.""" + + +class FloatCounter(Metric): + """A counter type metric that holds float values. + + Cumulative values can go up or stay the same, but can never go down. + Cumulative values cannot be negative. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "CounterTimeSeries": + """Gets a `CounterTimeSeries` with a cumulative float value.""" + + +class IntCounter(Metric): + """A counter type metric that holds int values. + + Cumulative values can go up or stay the same, but can never go down. + Cumulative values cannot be negative. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "CounterTimeSeries": + """Gets a `CounterTimeSeries` with a cumulative int value.""" + + +class FloatGauge(Metric): + """A gauge type metric that holds float values. + + Cumulative value can go both up and down. Values can be negative. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "GaugeTimeSeries": + """Gets a `GaugeTimeSeries` with a cumulative float value.""" + + +class IntGauge(Metric): + """A gauge type metric that holds int values. + + Cumulative value can go both up and down. Values can be negative. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "GaugeTimeSeries": + """Gets a `GaugeTimeSeries` with a cumulative int value.""" + + +class FloatMeasure(Metric): + """A measure type metric that holds float values. + + Measure metrics represent raw statistics that are recorded. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "MeasureTimeSeries": + """Gets a `MeasureTimeSeries` with a cumulated float value.""" + + +class IntMeasure(Metric): + """A measure type metric that holds int values. + + Measure metrics represent raw statistics that are recorded. + """ + + def get_or_create_time_series( + self, label_values: LabelValues + ) -> "MeasureTimeSeries": + """Gets a `MeasureTimeSeries` with a cumulated int value.""" + + +class LabelKey: + """The label keys associated with the metric. + + :type key: str + :param key: the key for the label + + :type description: str + :param description: description of the label + """ + + def __init__(self, key: str, description: str) -> None: + self.key = key + self.description = description diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py b/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py new file mode 100644 index 0000000000..c9c55f01b8 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/metrics/examples/pre_aggregated.py @@ -0,0 +1,36 @@ +# 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. + +# pylint: skip-file +from opentelemetry.metrics import LabelKey, LabelValue, Meter + +METER = Meter() +LABEL_KEYS = [ + LabelKey("environment", "the environment the application is running in") +] +COUNTER = METER.create_int_counter( + "sum numbers", # pragma: no cover + "sum numbers over time", + "number", + LABEL_KEYS, +) +LABEL_VALUE_TESTING = [LabelValue("Testing")] +LABEL_VALUE_STAGING = [LabelValue("Staging")] + +# Metrics sent to some exporter +METRIC_TESTING = COUNTER.get_or_create_time_series(LABEL_VALUE_TESTING) +METRIC_STAGING = COUNTER.get_or_create_time_series(LABEL_VALUE_STAGING) + +for i in range(100): + METRIC_STAGING.add(i) diff --git a/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py b/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py new file mode 100644 index 0000000000..3c82e14d53 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/metrics/examples/raw.py @@ -0,0 +1,45 @@ +# 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. + +# pylint: skip-file +from opentelemetry.metrics import LabelKey, LabelValue, MeasureBatch, Meter +from opentelemetry.metrics.aggregation import LastValueAggregation + +METER = Meter() +LABEL_KEYS = [ + LabelKey("environment", "the environment the application is running in") +] +MEASURE = METER.create_float_measure( + "idle_cpu_percentage", + "cpu idle over time", + "percentage", + LABEL_KEYS, + LastValueAggregation, +) +LABEL_VALUE_TESTING = [LabelValue("Testing")] +LABEL_VALUE_STAGING = [LabelValue("Staging")] + +# Metrics sent to some exporter +MEASURE_METRIC_TESTING = MEASURE.get_or_create_time_series(LABEL_VALUE_TESTING) +MEASURE_METRIC_STAGING = MEASURE.get_or_create_time_series(LABEL_VALUE_STAGING) + +# record individual measures +STATISTIC = 100 +MEASURE_METRIC_STAGING.record(STATISTIC) + +# record multiple observed values +BATCH = MeasureBatch() +BATCH.record( + [(MEASURE_METRIC_TESTING, STATISTIC), (MEASURE_METRIC_STAGING, STATISTIC)] +) diff --git a/opentelemetry-api/src/opentelemetry/metrics/time_series.py b/opentelemetry-api/src/opentelemetry/metrics/time_series.py new file mode 100644 index 0000000000..b14ef973ad --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/metrics/time_series.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 typing + + +class CounterTimeSeries: + def add(self, value: typing.Union[float, int]) -> None: + """Adds the given value to the current value. Cannot be negative.""" + + def set(self, value: typing.Union[float, int]) -> None: + """Sets the current value to the given value. + + The given value must be larger than the current recorded value. + """ + + +class GaugeTimeSeries: + def set(self, value: typing.Union[float, int]) -> None: + """Sets the current value to the given value. Can be negative.""" + + +class MeasureTimeSeries: + def record(self, value: typing.Union[float, int]) -> None: + """Records the given value to this measure. + + Logic depends on type of aggregation used for this measure. + """