Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metrics Console Exporter + Add last_updated_timestamp to metrics #192

Merged
merged 23 commits into from
Nov 5, 2019
Merged
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pip install -e ./ext/opentelemetry-ext-{integration}

## Quick Start

### Tracing

```python
from opentelemetry import trace
from opentelemetry.context import Context
Expand All @@ -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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be great if the SDK meter implementation didn't need this additional lambda. Feel a little weird that this pattern will have to be added in every codebase that wants to use the standard meter implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I think as part of [#195], there will be an easier way to initialize these implementations (perhaps a default implementation is none is specified).

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
Expand Down
15 changes: 15 additions & 0 deletions opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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


Expand All @@ -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


Expand All @@ -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


Expand Down Expand Up @@ -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


Expand Down
73 changes: 73 additions & 0 deletions opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright 2019, OpenTelemetry Authors
lzchen marked this conversation as resolved.
Show resolved Hide resolved
#
# 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random aside, but it feels weird to have to import from a parent module like this.

Maybe a style thing but I prefer importing from the submodule it is defined in:

from ..metric import Metric

prevents someone adding a cyclic dependency by importing a value in here into the top level module as well.



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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote for the ability to add a format string here, similar to logging. e.g. I may want to emit things as JSON on the console.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd want to be consistent with the ConsoleSpanExporter for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay. Random question, maybe for you and @Oberon00: any issues if I do add the ability to format output to both of these in a followup PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a problem with that. Go for it!

"""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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a way to pass the stream in the init? that way I could also choose to log in other streams, such as stderr.

Copy link
Contributor Author

@lzchen lzchen Oct 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thought as above: any issue with me adding this flexibility in a followup PR?

'{}(data="{}", label_values="{}", metric_data={})'.format(
type(self).__name__, metric, label_values, handle
)
)
return MetricsExportResult.SUCCESS
13 changes: 13 additions & 0 deletions opentelemetry-sdk/tests/metrics/export/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
40 changes: 40 additions & 0 deletions opentelemetry-sdk/tests/metrics/export/test_export.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding tests!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we've made the decision to use pytest now, we no longer need to inherit from unittest.TestCase and can just use the general assert. I guess after #128 gets merged in we could discuss refactoring these.

# 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)