diff --git a/README.md b/README.md index 0d4548c9234..8d45feb8ad3 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ pip install -e ./ext/opentelemetry-ext-{integration} ## Quick Start +### Tracing + ```python from opentelemetry import trace from opentelemetry.context import Context @@ -67,6 +69,34 @@ with tracer.start_as_current_span('foo'): print(Context) ``` +### Metrics + +```python +from opentelemetry import metrics +from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + +metrics.set_preferred_meter_implementation(lambda T: Meter()) +meter = metrics.meter() +exporter = ConsoleMetricsExporter() + +counter = meter.create_metric( + "available memory", + "available memory", + "bytes", + int, + Counter, + ("environment",), +) + +label_values = ("staging",) +counter_handle = counter.get_handle(label_values) +counter_handle.add(100) + +exporter.export([(counter, label_values)]) +exporter.shutdown() +``` + See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 0a941cd0d18..e6f5d53166b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -16,6 +16,7 @@ from typing import Sequence, Tuple, Type from opentelemetry import metrics as metrics_api +from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -31,6 +32,7 @@ def __init__( self.value_type = value_type self.enabled = enabled self.monotonic = monotonic + self.last_update_timestamp = time_ns() def _validate_update(self, value: metrics_api.ValueT) -> bool: if not self.enabled: @@ -42,6 +44,11 @@ def _validate_update(self, value: metrics_api.ValueT) -> bool: return False return True + def __repr__(self): + return '{}(data="{}", last_update_timestamp={})'.format( + type(self).__name__, self.data, self.last_update_timestamp + ) + class CounterHandle(metrics_api.CounterHandle, BaseHandle): def add(self, value: metrics_api.ValueT) -> None: @@ -50,6 +57,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 @@ -60,6 +68,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 @@ -70,6 +79,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 @@ -107,6 +117,11 @@ def get_handle(self, label_values: Sequence[str]) -> BaseHandle: self.handles[label_values] = handle return handle + def __repr__(self): + return '{}(name="{}", description={})'.format( + type(self).__name__, self.name, self.description + ) + UPDATE_FUNCTION = lambda x, y: None # noqa: E731 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py new file mode 100644 index 00000000000..b6cb396331a --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py @@ -0,0 +1,73 @@ +# 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. + +from enum import Enum +from typing import Sequence, Tuple + +from .. import Metric + + +class MetricsExportResult(Enum): + SUCCESS = 0 + FAILED_RETRYABLE = 1 + FAILED_NOT_RETRYABLE = 2 + + +class MetricsExporter: + """Interface for exporting metrics. + + Interface to be implemented by services that want to export recorded + metrics in its own format. + """ + + def export( + self, metric_tuples: Sequence[Tuple[Metric, Sequence[str]]] + ) -> "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. + + Returns: + The result of the export + """ + + def shutdown(self) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + + +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. + """ + + def export( + self, metric_tuples: Sequence[Tuple[Metric, Sequence[str]]] + ) -> "MetricsExportResult": + for metric, label_values in metric_tuples: + handle = metric.get_handle(label_values) + print( + '{}(data="{}", label_values="{}", metric_data={})'.format( + type(self).__name__, metric, label_values, handle + ) + ) + return MetricsExportResult.SUCCESS diff --git a/opentelemetry-sdk/tests/metrics/export/__init__.py b/opentelemetry-sdk/tests/metrics/export/__init__.py new file mode 100644 index 00000000000..d853a7bcf65 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/export/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py new file mode 100644 index 00000000000..ca8e8a36311 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -0,0 +1,40 @@ +# 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 unittest +from unittest import mock + +from opentelemetry.sdk import metrics +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + + +class TestConsoleMetricsExporter(unittest.TestCase): + # pylint: disable=no-self-use + def test_export(self): + exporter = ConsoleMetricsExporter() + metric = metrics.Counter( + "available memory", + "available memory", + "bytes", + int, + ("environment",), + ) + label_values = ("staging",) + handle = metric.get_handle(label_values) + result = '{}(data="{}", label_values="{}", metric_data={})'.format( + ConsoleMetricsExporter.__name__, metric, label_values, handle + ) + with mock.patch("sys.stdout") as mock_stdout: + exporter.export([(metric, label_values)]) + mock_stdout.write.assert_any_call(result)