From 1576a0d6cfa862dcbf28995bc7f5f83a9e4b0568 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Mon, 17 Aug 2020 11:41:20 -0400 Subject: [PATCH 1/3] in types.Attributes, change Dict -> Mapping (#989) --- opentelemetry-api/src/opentelemetry/util/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/util/types.py b/opentelemetry-api/src/opentelemetry/util/types.py index 6830a0dcd1..4221c7f1b9 100644 --- a/opentelemetry-api/src/opentelemetry/util/types.py +++ b/opentelemetry-api/src/opentelemetry/util/types.py @@ -13,7 +13,7 @@ # limitations under the License. -from typing import Callable, Dict, Optional, Sequence, Union +from typing import Callable, Mapping, Optional, Sequence, Union AttributeValue = Union[ str, @@ -25,5 +25,5 @@ Sequence[int], Sequence[float], ] -Attributes = Optional[Dict[str, AttributeValue]] -AttributesFormatter = Callable[[], Optional[Dict[str, AttributeValue]]] +Attributes = Optional[Mapping[str, AttributeValue]] +AttributesFormatter = Callable[[], Attributes] From 5aa1d811039de774f4a84db4c9c656afe11827dd Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Mon, 17 Aug 2020 18:06:33 +0200 Subject: [PATCH 2/3] Remove redundant circleci matrix parameter (#993) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bebfc4dbc1..fd45dce81f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -141,7 +141,7 @@ workflows: - build: matrix: parameters: - version: ["py38", "py37", "py36", "py35", "pypy3"] + version: ["py37", "py36", "py35", "pypy3"] package: ["core", "exporter", "instrumentation"] - build-py34: matrix: From 452be59bebe20c81b64c4dbc391ee77472498360 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 17 Aug 2020 22:06:05 -0600 Subject: [PATCH 3/3] exporter/otlp: Add OTLP metric exporter (#835) --- .pylintrc | 1 + docs/getting-started.rst | 4 +- ...or_example.py => otlpcollector_example.py} | 28 ++- .../opentelemetry-exporter-otlp/CHANGELOG.md | 5 +- .../opentelemetry/exporter/otlp/exporter.py | 194 +++++++++++++++++ .../otlp/metrics_exporter/__init__.py | 198 ++++++++++++++++++ .../exporter/otlp/trace_exporter/__init__.py | 172 ++------------- .../tests/test_otlp_metric_exporter.py | 116 ++++++++++ .../tests/test_otlp_trace_exporter.py | 10 +- 9 files changed, 550 insertions(+), 178 deletions(-) rename docs/getting_started/{otcollector_example.py => otlpcollector_example.py} (78%) create mode 100644 exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py create mode 100644 exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py diff --git a/.pylintrc b/.pylintrc index 5f9463df7d..8f29b634f1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -65,6 +65,7 @@ disable=missing-docstring, too-few-public-methods, # Might be good to re-enable this later. too-many-instance-attributes, too-many-arguments, + duplicate-code, ungrouped-imports, # Leave this up to isort wrong-import-order, # Leave this up to isort bad-continuation, # Leave this up to black diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 198bd0f1e3..8c27ddfa4d 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -312,10 +312,10 @@ Install the OpenTelemetry Collector exporter: .. code-block:: sh - pip install opentelemetry-instrumentation-otcollector + pip install opentelemetry-exporter-otlp And execute the following script: -.. literalinclude:: getting_started/otcollector_example.py +.. literalinclude:: getting_started/otlpcollector_example.py :language: python :lines: 15- diff --git a/docs/getting_started/otcollector_example.py b/docs/getting_started/otlpcollector_example.py similarity index 78% rename from docs/getting_started/otcollector_example.py rename to docs/getting_started/otlpcollector_example.py index b1887c3d0c..15254c7cc5 100644 --- a/docs/getting_started/otcollector_example.py +++ b/docs/getting_started/otlpcollector_example.py @@ -16,33 +16,29 @@ import time from opentelemetry import metrics, trace -from opentelemetry.ext.otcollector.metrics_exporter import ( - CollectorMetricsExporter, -) -from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter +from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter +from opentelemetry.exporter.otlp.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor -# create a CollectorSpanExporter -span_exporter = CollectorSpanExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", +span_exporter = OTLPSpanExporter( + # optional + # endpoint:="myCollectorURL:55678", + # credentials=ChannelCredentials(credentials), + # metadata=(("metadata", "metadata")), ) tracer_provider = TracerProvider() trace.set_tracer_provider(tracer_provider) span_processor = BatchExportSpanProcessor(span_exporter) tracer_provider.add_span_processor(span_processor) -# create a CollectorMetricsExporter -metric_exporter = CollectorMetricsExporter( - # optional: - # endpoint="myCollectorUrl:55678", - # service_name="test_service", - # host_name="machine/container name", +metric_exporter = OTLPMetricsExporter( + # optional + # endpoint:="myCollectorURL:55678", + # credentials=ChannelCredentials(credentials), + # metadata=(("metadata", "metadata")), ) # Meter is responsible for creating and recording metrics diff --git a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md index 24eb8bcf58..ed6aee3b4c 100644 --- a/exporter/opentelemetry-exporter-otlp/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-otlp/CHANGELOG.md @@ -2,9 +2,10 @@ ## Unreleased -## Version 0.12b0 +- Add metric OTLP exporter + ([#835](https://github.com/open-telemetry/opentelemetry-python/pull/835)) -Released 2020-08-14 +## Version 0.12b0 - Change package name to opentelemetry-exporter-otlp ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py new file mode 100644 index 0000000000..0ce7ef6617 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/exporter.py @@ -0,0 +1,194 @@ +# Copyright The 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. + +"""OTLP Exporter""" + +import logging +from abc import ABC, abstractmethod +from collections.abc import Mapping, Sequence +from time import sleep + +from backoff import expo +from google.rpc.error_details_pb2 import RetryInfo +from grpc import ( + ChannelCredentials, + RpcError, + StatusCode, + insecure_channel, + secure_channel, +) + +from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue +from opentelemetry.proto.resource.v1.resource_pb2 import Resource + +logger = logging.getLogger(__name__) + + +def _translate_key_values(key, value): + + if isinstance(value, bool): + any_value = AnyValue(bool_value=value) + + elif isinstance(value, str): + any_value = AnyValue(string_value=value) + + elif isinstance(value, int): + any_value = AnyValue(int_value=value) + + elif isinstance(value, float): + any_value = AnyValue(double_value=value) + + elif isinstance(value, Sequence): + any_value = AnyValue(array_value=value) + + elif isinstance(value, Mapping): + any_value = AnyValue(kvlist_value=value) + + else: + raise Exception( + "Invalid type {} of value {}".format(type(value), value) + ) + + return KeyValue(key=key, value=any_value) + + +def _get_resource_data( + sdk_resource_instrumentation_library_data, resource_class, name +): + + resource_data = [] + + for ( + sdk_resource, + instrumentation_library_data, + ) in sdk_resource_instrumentation_library_data.items(): + + collector_resource = Resource() + + for key, value in sdk_resource.labels.items(): + + try: + # pylint: disable=no-member + collector_resource.attributes.append( + _translate_key_values(key, value) + ) + except Exception as error: # pylint: disable=broad-except + logger.exception(error) + + resource_data.append( + resource_class( + **{ + "resource": collector_resource, + "instrumentation_library_{}".format(name): [ + instrumentation_library_data + ], + } + ) + ) + + return resource_data + + +# pylint: disable=no-member +class OTLPExporterMixin(ABC): + """OTLP span/metric exporter + + Args: + endpoint: OpenTelemetry Collector receiver endpoint + credentials: ChannelCredentials object for server authentication + metadata: Metadata to send when exporting + """ + + def __init__( + self, + endpoint: str = "localhost:55680", + credentials: ChannelCredentials = None, + metadata: tuple = None, + ): + super().__init__() + + self._metadata = metadata + self._collector_span_kwargs = None + + if credentials is None: + self._client = self._stub(insecure_channel(endpoint)) + else: + self._client = self._stub(secure_channel(endpoint, credentials)) + + @abstractmethod + def _translate_data(self, data): + pass + + def _export(self, data): + # expo returns a generator that yields delay values which grow + # exponentially. Once delay is greater than max_value, the yielded + # value will remain constant. + # max_value is set to 900 (900 seconds is 15 minutes) to use the same + # value as used in the Go implementation. + + max_value = 900 + + for delay in expo(max_value=max_value): + + if delay == max_value: + return self._result.FAILURE + + try: + self._client.Export( + request=self._translate_data(data), + metadata=self._metadata, + ) + + return self._result.SUCCESS + + except RpcError as error: + + if error.code() in [ + StatusCode.CANCELLED, + StatusCode.DEADLINE_EXCEEDED, + StatusCode.PERMISSION_DENIED, + StatusCode.UNAUTHENTICATED, + StatusCode.RESOURCE_EXHAUSTED, + StatusCode.ABORTED, + StatusCode.OUT_OF_RANGE, + StatusCode.UNAVAILABLE, + StatusCode.DATA_LOSS, + ]: + + retry_info_bin = dict(error.trailing_metadata()).get( + "google.rpc.retryinfo-bin" + ) + if retry_info_bin is not None: + retry_info = RetryInfo() + retry_info.ParseFromString(retry_info_bin) + delay = ( + retry_info.retry_delay.seconds + + retry_info.retry_delay.nanos / 1.0e9 + ) + + logger.debug( + "Waiting %ss before retrying export of span", delay + ) + sleep(delay) + continue + + if error.code() == StatusCode.OK: + return self._result.SUCCESS + + return self.result.FAILURE + + return self._result.FAILURE + + def shutdown(self): + pass diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py new file mode 100644 index 0000000000..944428e37d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/metrics_exporter/__init__.py @@ -0,0 +1,198 @@ +# Copyright The 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. + +"""OTLP Metrics Exporter""" + +import logging +from typing import Sequence + +# pylint: disable=duplicate-code +from opentelemetry.exporter.otlp.exporter import ( + OTLPExporterMixin, + _get_resource_data, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( + MetricsServiceStub, +) +from opentelemetry.proto.common.v1.common_pb2 import StringKeyValue +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + DoubleDataPoint, + InstrumentationLibraryMetrics, + Int64DataPoint, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + Metric as CollectorMetric, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + MetricDescriptor, + ResourceMetrics, +) +from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Metric as SDKMetric +from opentelemetry.sdk.metrics import ( + SumObserver, + UpDownCounter, + UpDownSumObserver, + ValueObserver, + ValueRecorder, +) +from opentelemetry.sdk.metrics.export import ( + MetricsExporter, + MetricsExportResult, +) + +logger = logging.getLogger(__name__) + + +def _get_data_points(sdk_metric, data_point_class): + + data_points = [] + + for ( + label, + bound_counter, + ) in sdk_metric.instrument.bound_instruments.items(): + + string_key_values = [] + + for label_key, label_value in label: + string_key_values.append( + StringKeyValue(key=label_key, value=label_value) + ) + + for view_data in bound_counter.view_datas: + + if view_data.labels == label: + + data_points.append( + data_point_class( + labels=string_key_values, + value=view_data.aggregator.current, + ) + ) + break + + return data_points + + +def _get_temporality(instrument): + # pylint: disable=no-member + if isinstance(instrument, (Counter, UpDownCounter)): + temporality = MetricDescriptor.Temporality.DELTA + elif isinstance(instrument, (ValueRecorder, ValueObserver)): + temporality = MetricDescriptor.Temporality.INSTANTANEOUS + elif isinstance(instrument, (SumObserver, UpDownSumObserver)): + temporality = MetricDescriptor.Temporality.CUMULATIVE + else: + raise Exception( + "No temporality defined for instrument type {}".format( + type(instrument) + ) + ) + + return temporality + + +def _get_type(value_type): + # pylint: disable=no-member + if value_type is int: + type_ = MetricDescriptor.Type.INT64 + + elif value_type is float: + type_ = MetricDescriptor.Type.DOUBLE + + # FIXME What are the types that correspond with + # MetricDescriptor.Type.HISTOGRAM and + # MetricDescriptor.Type.SUMMARY? + else: + raise Exception( + "No type defined for valie type {}".format(type(value_type)) + ) + + return type_ + + +class OTLPMetricsExporter(MetricsExporter, OTLPExporterMixin): + """OTLP metrics exporter + + Args: + endpoint: OpenTelemetry Collector receiver endpoint + credentials: Credentials object for server authentication + metadata: Metadata to send when exporting + """ + + _stub = MetricsServiceStub + _result = MetricsExportResult + + def _translate_data(self, data): + # pylint: disable=too-many-locals,no-member + # pylint: disable=attribute-defined-outside-init + + sdk_resource_instrumentation_library_metrics = {} + + for sdk_metric in data: + + if sdk_metric.instrument.meter.resource not in ( + sdk_resource_instrumentation_library_metrics.keys() + ): + sdk_resource_instrumentation_library_metrics[ + sdk_metric.instrument.meter.resource + ] = InstrumentationLibraryMetrics() + + self._metric_descriptor_kwargs = {} + + metric_descriptor = MetricDescriptor( + name=sdk_metric.instrument.name, + description=sdk_metric.instrument.description, + unit=sdk_metric.instrument.unit, + type=_get_type(sdk_metric.instrument.value_type), + temporality=_get_temporality(sdk_metric.instrument), + ) + + if metric_descriptor.type == MetricDescriptor.Type.INT64: + + collector_metric = CollectorMetric( + metric_descriptor=metric_descriptor, + int64_data_points=_get_data_points( + sdk_metric, Int64DataPoint + ), + ) + + elif metric_descriptor.type == MetricDescriptor.Type.DOUBLE: + + collector_metric = CollectorMetric( + metric_descriptor=metric_descriptor, + double_data_points=_get_data_points( + sdk_metric, DoubleDataPoint + ), + ) + + sdk_resource_instrumentation_library_metrics[ + sdk_metric.instrument.meter.resource + ].metrics.append(collector_metric) + + return ExportMetricsServiceRequest( + resource_metrics=_get_resource_data( + sdk_resource_instrumentation_library_metrics, + ResourceMetrics, + "metrics", + ) + ) + + def export(self, metrics: Sequence[SDKMetric]) -> MetricsExportResult: + # pylint: disable=arguments-differ + return self._export(metrics) diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py index be1419fa39..5a9a74a304 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/trace_exporter/__init__.py @@ -1,5 +1,4 @@ # Copyright The 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 @@ -15,28 +14,19 @@ """OTLP Span Exporter""" import logging -from collections.abc import Mapping, Sequence -from time import sleep -from typing import Sequence as TypingSequence - -from backoff import expo -from google.rpc.error_details_pb2 import RetryInfo -from grpc import ( - ChannelCredentials, - RpcError, - StatusCode, - insecure_channel, - secure_channel, -) +from typing import Sequence +from opentelemetry.exporter.otlp.exporter import ( + OTLPExporterMixin, + _get_resource_data, + _translate_key_values, +) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( TraceServiceStub, ) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue -from opentelemetry.proto.resource.v1.resource_pb2 import Resource from opentelemetry.proto.trace.v1.trace_pb2 import ( InstrumentationLibrarySpans, ResourceSpans, @@ -49,36 +39,8 @@ logger = logging.getLogger(__name__) -def _translate_key_values(key, value): - - if isinstance(value, bool): - any_value = AnyValue(bool_value=value) - - elif isinstance(value, str): - any_value = AnyValue(string_value=value) - - elif isinstance(value, int): - any_value = AnyValue(int_value=value) - - elif isinstance(value, float): - any_value = AnyValue(double_value=value) - - elif isinstance(value, Sequence): - any_value = AnyValue(array_value=value) - - elif isinstance(value, Mapping): - any_value = AnyValue(kvlist_value=value) - - else: - raise Exception( - "Invalid type {} of value {}".format(type(value), value) - ) - - return KeyValue(key=key, value=any_value) - - # pylint: disable=no-member -class OTLPSpanExporter(SpanExporter): +class OTLPSpanExporter(SpanExporter, OTLPExporterMixin): """OTLP span exporter Args: @@ -87,23 +49,8 @@ class OTLPSpanExporter(SpanExporter): metadata: Metadata to send when exporting """ - def __init__( - self, - endpoint="localhost:55680", - credentials: ChannelCredentials = None, - metadata=None, - ): - super().__init__() - - self._metadata = metadata - self._collector_span_kwargs = None - - if credentials is None: - self._client = TraceServiceStub(insecure_channel(endpoint)) - else: - self._client = TraceServiceStub( - secure_channel(endpoint, credentials) - ) + _result = SpanExportResult + _stub = TraceServiceStub def _translate_name(self, sdk_span): self._collector_span_kwargs["name"] = sdk_span.name @@ -212,13 +159,11 @@ def _translate_status(self, sdk_span): message=sdk_span.status.description, ) - def _translate_spans( - self, sdk_spans: TypingSequence[SDKSpan], - ) -> ExportTraceServiceRequest: + def _translate_data(self, data) -> ExportTraceServiceRequest: sdk_resource_instrumentation_library_spans = {} - for sdk_span in sdk_spans: + for sdk_span in data: if sdk_span.resource not in ( sdk_resource_instrumentation_library_spans.keys() @@ -249,92 +194,13 @@ def _translate_spans( sdk_span.resource ].spans.append(CollectorSpan(**self._collector_span_kwargs)) - resource_spans = [] - - for ( - sdk_resource, - instrumentation_library_spans, - ) in sdk_resource_instrumentation_library_spans.items(): - - collector_resource = Resource() - - for key, value in sdk_resource.labels.items(): - - try: - collector_resource.attributes.append( - _translate_key_values(key, value) - ) - except Exception as error: # pylint: disable=broad-except - logger.exception(error) - - resource_spans.append( - ResourceSpans( - resource=collector_resource, - instrumentation_library_spans=[ - instrumentation_library_spans - ], - ) + return ExportTraceServiceRequest( + resource_spans=_get_resource_data( + sdk_resource_instrumentation_library_spans, + ResourceSpans, + "spans", ) + ) - return ExportTraceServiceRequest(resource_spans=resource_spans) - - def export(self, spans: TypingSequence[SDKSpan]) -> SpanExportResult: - # expo returns a generator that yields delay values which grow - # exponentially. Once delay is greater than max_value, the yielded - # value will remain constant. - # max_value is set to 900 (900 seconds is 15 minutes) to use the same - # value as used in the Go implementation. - - max_value = 900 - - for delay in expo(max_value=max_value): - - if delay == max_value: - return SpanExportResult.FAILURE - - try: - self._client.Export( - request=self._translate_spans(spans), - metadata=self._metadata, - ) - - return SpanExportResult.SUCCESS - - except RpcError as error: - - if error.code() in [ - StatusCode.CANCELLED, - StatusCode.DEADLINE_EXCEEDED, - StatusCode.PERMISSION_DENIED, - StatusCode.UNAUTHENTICATED, - StatusCode.RESOURCE_EXHAUSTED, - StatusCode.ABORTED, - StatusCode.OUT_OF_RANGE, - StatusCode.UNAVAILABLE, - StatusCode.DATA_LOSS, - ]: - - retry_info_bin = dict(error.trailing_metadata()).get( - "google.rpc.retryinfo-bin" - ) - if retry_info_bin is not None: - retry_info = RetryInfo() - retry_info.ParseFromString(retry_info_bin) - delay = ( - retry_info.retry_delay.seconds - + retry_info.retry_delay.nanos / 1.0e9 - ) - - logger.debug("Waiting %ss before retrying export of span") - sleep(delay) - continue - - if error.code() == StatusCode.OK: - return SpanExportResult.SUCCESS - - return SpanExportResult.FAILURE - - return SpanExportResult.FAILURE - - def shutdown(self): - pass + def export(self, spans: Sequence[SDKSpan]) -> SpanExportResult: + return self._export(spans) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py new file mode 100644 index 0000000000..20fecd44a2 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_metric_exporter.py @@ -0,0 +1,116 @@ +# Copyright The 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 collections import OrderedDict +from unittest import TestCase + +from opentelemetry.exporter.otlp.metrics_exporter import OTLPMetricsExporter +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + KeyValue, + StringKeyValue, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + InstrumentationLibraryMetrics, + Int64DataPoint, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + Metric as CollectorMetric, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + MetricDescriptor, + ResourceMetrics, +) +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as CollectorResource, +) +from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics.export import MetricRecord +from opentelemetry.sdk.metrics.export.aggregate import SumAggregator +from opentelemetry.sdk.resources import Resource as SDKResource + + +class TestOTLPMetricExporter(TestCase): + def setUp(self): + self.exporter = OTLPMetricsExporter() + + self.counter_metric_record = MetricRecord( + Counter( + "a", + "b", + "c", + int, + MeterProvider( + resource=SDKResource(OrderedDict([("a", 1), ("b", False)])) + ).get_meter(__name__), + ("d",), + ), + OrderedDict([("e", "f")]), + SumAggregator(), + ) + + def test_translate_metrics(self): + # pylint: disable=no-member + + self.counter_metric_record.instrument.add(1, OrderedDict([("a", "b")])) + + expected = ExportMetricsServiceRequest( + resource_metrics=[ + ResourceMetrics( + resource=CollectorResource( + attributes=[ + KeyValue(key="a", value=AnyValue(int_value=1)), + KeyValue( + key="b", value=AnyValue(bool_value=False) + ), + ] + ), + instrumentation_library_metrics=[ + InstrumentationLibraryMetrics( + metrics=[ + CollectorMetric( + metric_descriptor=MetricDescriptor( + name="a", + description="b", + unit="c", + type=MetricDescriptor.Type.INT64, + temporality=( + MetricDescriptor.Temporality.DELTA + ), + ), + int64_data_points=[ + Int64DataPoint( + labels=[ + StringKeyValue( + key="a", value="b" + ) + ], + value=1, + ) + ], + ) + ] + ) + ], + ) + ] + ) + + # pylint: disable=protected-access + actual = self.exporter._translate_data([self.counter_metric_record]) + + self.assertEqual(expected, actual) diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py index 9058937f87..b0ec8e4517 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp_trace_exporter.py @@ -142,8 +142,8 @@ def setUp(self): def tearDown(self): self.server.stop(None) - @patch("opentelemetry.exporter.otlp.trace_exporter.expo") - @patch("opentelemetry.exporter.otlp.trace_exporter.sleep") + @patch("opentelemetry.exporter.otlp.exporter.expo") + @patch("opentelemetry.exporter.otlp.exporter.sleep") def test_unavailable(self, mock_sleep, mock_expo): mock_expo.configure_mock(**{"return_value": [1]}) @@ -156,8 +156,8 @@ def test_unavailable(self, mock_sleep, mock_expo): ) mock_sleep.assert_called_with(1) - @patch("opentelemetry.exporter.otlp.trace_exporter.expo") - @patch("opentelemetry.exporter.otlp.trace_exporter.sleep") + @patch("opentelemetry.exporter.otlp.exporter.expo") + @patch("opentelemetry.exporter.otlp.exporter.sleep") def test_unavailable_delay(self, mock_sleep, mock_expo): mock_expo.configure_mock(**{"return_value": [1]}) @@ -274,4 +274,4 @@ def test_translate_spans(self): ) # pylint: disable=protected-access - self.assertEqual(expected, self.exporter._translate_spans([self.span])) + self.assertEqual(expected, self.exporter._translate_data([self.span]))