diff --git a/examples/basic_tracer/README.md b/examples/basic_tracer/README.md index 4dc0e96bea6..ae9e4ca8959 100644 --- a/examples/basic_tracer/README.md +++ b/examples/basic_tracer/README.md @@ -53,6 +53,26 @@ Click on the trace to view its details.

+### Collector + +* Start Collector + +```sh +$ pip install docker-compose +$ cd docker +$ docker-compose up + +* Run the sample + +$ pip install opentelemetry-ext-otcollector +$ # from this directory +$ EXPORTER=collector python tracer.py +``` + +Collector is configured to export to Jaeger, follow Jaeger UI isntructions to find the traces. + + + ## Useful links - For more information on OpenTelemetry, visit: - For more information on tracing in Python, visit: diff --git a/examples/basic_tracer/docker/collector-config.yaml b/examples/basic_tracer/docker/collector-config.yaml new file mode 100644 index 00000000000..bcf59c58024 --- /dev/null +++ b/examples/basic_tracer/docker/collector-config.yaml @@ -0,0 +1,19 @@ +receivers: + opencensus: + endpoint: "0.0.0.0:55678" + +exporters: + jaeger_grpc: + endpoint: jaeger-all-in-one:14250 + logging: {} + +processors: + batch: + queued_retry: + +service: + pipelines: + traces: + receivers: [opencensus] + exporters: [jaeger_grpc, logging] + processors: [batch, queued_retry] diff --git a/examples/basic_tracer/docker/docker-compose.yaml b/examples/basic_tracer/docker/docker-compose.yaml new file mode 100644 index 00000000000..71d7ccd5a11 --- /dev/null +++ b/examples/basic_tracer/docker/docker-compose.yaml @@ -0,0 +1,20 @@ +version: "2" +services: + + # Collector + collector: + image: omnition/opentelemetry-collector-contrib:latest + command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] + volumes: + - ./collector-config.yaml:/conf/collector-config.yaml + ports: + - "55678:55678" + + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "6831:6831/udp" + - "6832:6832/udp" + - "14268" + - "14250" diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py index bfb50a98907..a454eab7a96 100755 --- a/examples/basic_tracer/tracer.py +++ b/examples/basic_tracer/tracer.py @@ -26,12 +26,23 @@ if os.getenv("EXPORTER") == "jaeger": from opentelemetry.ext.jaeger import JaegerSpanExporter + print("Using JaegerSpanExporter") exporter = JaegerSpanExporter( service_name="basic-service", agent_host_name="localhost", agent_port=6831, ) +elif os.getenv("EXPORTER") == "collector": + from opentelemetry.ext.otcollector.trace_exporter import ( + CollectorSpanExporter, + ) + + print("Using CollectorSpanExporter") + exporter = CollectorSpanExporter( + service_name="basic-service", endpoint="localhost:55678" + ) else: + print("Using ConsoleSpanExporter") exporter = ConsoleSpanExporter() # The preferred tracer implementation must be set, as the opentelemetry-api diff --git a/ext/opentelemetry-ext-otcollector/CHANGELOG.md b/ext/opentelemetry-ext-otcollector/CHANGELOG.md new file mode 100644 index 00000000000..617d979ab29 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## Unreleased + diff --git a/ext/opentelemetry-ext-otcollector/README.rst b/ext/opentelemetry-ext-otcollector/README.rst new file mode 100644 index 00000000000..33d8d587479 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/README.rst @@ -0,0 +1,55 @@ +OpenTelemetry Collector Exporter +================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-otcollector.svg + :target: https://pypi.org/project/opentelemetry-ext-otcollector/ + +This library allows to export data to `OpenTelemetry Collector `_. + +Installation +------------ + +:: + + pip install opentelemetry-ext-otcollector + + +Usage +----- + +The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ traces to `OpenTelemetry Collector`_. + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + + # create a CollectorSpanExporter + collector_exporter = CollectorSpanExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(collector_exporter) + + # Configure the tracer to use the collector exporter + tracer_provider = TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = TracerProvider().get_tracer(__name__) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +References +---------- + +* `OpenTelemetry Collector `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg new file mode 100644 index 00000000000..acc5b37723c --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -0,0 +1,49 @@ +# Copyright 2020, 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. +# +[metadata] +name = opentelemetry-ext-otcollector +description = OpenTelemetry Collector Exporter +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-otcollector +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + grpcio >= 1.0.0, < 2.0.0 + opencensus-proto >= 0.1.0, < 1.0.0 + opentelemetry-api >= 0.5.dev0 + opentelemetry-sdk >= 0.5.dev0 + protobuf >= 3.8.0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-otcollector/setup.py b/ext/opentelemetry-ext-otcollector/setup.py new file mode 100644 index 00000000000..ecd84195115 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/setup.py @@ -0,0 +1,26 @@ +# Copyright 2020, 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 os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "otcollector", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py new file mode 100644 index 00000000000..6ab2e961ec4 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020, 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/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py new file mode 100644 index 00000000000..8712682ecfd --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py @@ -0,0 +1,187 @@ +# Copyright 2020, 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. + +"""OpenTelemetry Collector Exporter.""" + +import logging +from typing import Optional, Sequence + +import grpc +from opencensus.proto.agent.trace.v1 import ( + trace_service_pb2, + trace_service_pb2_grpc, +) +from opencensus.proto.trace.v1 import trace_pb2 + +import opentelemetry.ext.otcollector.util as utils +import opentelemetry.trace as trace_api +from opentelemetry.sdk.trace import Span, SpanContext +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace import SpanKind, TraceState + +DEFAULT_ENDPOINT = "localhost:55678" + +logger = logging.getLogger(__name__) + + +# pylint: disable=no-member +class CollectorSpanExporter(SpanExporter): + """OpenTelemetry Collector span exporter. + + Args: + endpoint: OpenTelemetry Collector OpenCensus receiver endpoint. + service_name: Name of Collector service. + host_name: Host name. + client: TraceService client stub. + """ + + def __init__( + self, + endpoint=DEFAULT_ENDPOINT, + service_name=None, + host_name=None, + client=None, + ): + self.endpoint = endpoint + if client is None: + self.channel = grpc.insecure_channel(self.endpoint) + self.client = trace_service_pb2_grpc.TraceServiceStub( + channel=self.channel + ) + else: + self.client = client + + self.node = utils.get_node(service_name, host_name) + + def export(self, spans: Sequence[Span]) -> SpanExportResult: + try: + responses = self.client.Export(self.generate_span_requests(spans)) + + # Read response + for _ in responses: + pass + + except grpc.RpcError: + return SpanExportResult.FAILED_NOT_RETRYABLE + + return SpanExportResult.SUCCESS + + def shutdown(self) -> None: + pass + + def generate_span_requests(self, spans): + collector_spans = translate_to_collector(spans) + service_request = trace_service_pb2.ExportTraceServiceRequest( + node=self.node, spans=collector_spans + ) + yield service_request + + +# pylint: disable=too-many-branches +def translate_to_collector(spans: Sequence[Span]): + collector_spans = [] + for span in spans: + status = None + if span.status is not None: + status = trace_pb2.Status( + code=span.status.canonical_code.value, + message=span.status.description, + ) + collector_span = trace_pb2.Span( + name=trace_pb2.TruncatableString(value=span.name), + kind=utils.get_collector_span_kind(span.kind), + trace_id=span.context.trace_id.to_bytes(16, "big"), + span_id=span.context.span_id.to_bytes(8, "big"), + start_time=utils.proto_timestamp_from_time_ns(span.start_time), + end_time=utils.proto_timestamp_from_time_ns(span.end_time), + status=status, + ) + + parent_id = 0 + if isinstance(span.parent, trace_api.Span): + parent_id = span.parent.get_context().span_id + elif isinstance(span.parent, trace_api.SpanContext): + parent_id = span.parent.span_id + + collector_span.parent_span_id = parent_id.to_bytes(8, "big") + + if span.context.trace_state is not None: + for (key, value) in span.context.trace_state.items(): + collector_span.tracestate.entries.add(key=key, value=value) + + if span.attributes: + for (key, value) in span.attributes.items(): + utils.add_proto_attribute_value( + collector_span.attributes, key, value + ) + + if span.events: + for event in span.events: + + collector_annotation = trace_pb2.Span.TimeEvent.Annotation( + description=trace_pb2.TruncatableString(value=event.name) + ) + + if event.attributes: + for (key, value) in event.attributes.items(): + utils.add_proto_attribute_value( + collector_annotation.attributes, key, value + ) + + collector_span.time_events.time_event.add( + time=utils.proto_timestamp_from_time_ns(event.timestamp), + annotation=collector_annotation, + ) + + if span.links: + for link in span.links: + collector_span_link = collector_span.links.link.add() + collector_span_link.trace_id = link.context.trace_id.to_bytes( + 16, "big" + ) + collector_span_link.span_id = link.context.span_id.to_bytes( + 8, "big" + ) + + collector_span_link.type = ( + trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED + ) + + if isinstance(span.parent, trace_api.Span): + if ( + link.context.span_id + == span.parent.get_context().span_id + and link.context.trace_id + == span.parent.get_context().trace_id + ): + collector_span_link.type = ( + trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN + ) + elif isinstance(span.parent, trace_api.SpanContext): + if ( + link.context.span_id == span.parent.span_id + and link.context.trace_id == span.parent.trace_id + ): + collector_span_link.type = ( + trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN + ) + + if link.attributes: + for (key, value) in link.attributes.items(): + utils.add_proto_attribute_value( + collector_span_link.attributes, key, value + ) + + collector_spans.append(collector_span) + return collector_spans diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py new file mode 100644 index 00000000000..7d605ab8f90 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py @@ -0,0 +1,99 @@ +# Copyright 2020, 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 os +import socket +import time + +from google.protobuf.timestamp_pb2 import Timestamp +from opencensus.proto.agent.common.v1 import common_pb2 +from opencensus.proto.trace.v1 import trace_pb2 + +from opentelemetry.ext.otcollector.version import ( + __version__ as otcollector_exporter_version, +) +from opentelemetry.trace import SpanKind +from opentelemetry.util.version import __version__ as opentelemetry_version + + +def proto_timestamp_from_time_ns(time_ns): + """Converts datetime to protobuf timestamp. + + Args: + time_ns: Time in nanoseconds + + Returns: + Returns protobuf timestamp. + """ + ts = Timestamp() + if time_ns is not None: + # pylint: disable=no-member + ts.FromNanoseconds(time_ns) + return ts + + +# pylint: disable=no-member +def get_collector_span_kind(kind: SpanKind): + if kind is SpanKind.SERVER: + return trace_pb2.Span.SpanKind.SERVER + if kind is SpanKind.CLIENT: + return trace_pb2.Span.SpanKind.CLIENT + return trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED + + +def add_proto_attribute_value(pb_attributes, key, value): + """Sets string, int, boolean or float value on protobuf + span, link or annotation attributes. + + Args: + pb_attributes: protobuf Span's attributes property. + key: attribute key to set. + value: attribute value + """ + + if isinstance(value, bool): + pb_attributes.attribute_map[key].bool_value = value + elif isinstance(value, int): + pb_attributes.attribute_map[key].int_value = value + elif isinstance(value, str): + pb_attributes.attribute_map[key].string_value.value = value + elif isinstance(value, float): + pb_attributes.attribute_map[key].double_value = value + else: + pb_attributes.attribute_map[key].string_value.value = str(value) + + +# pylint: disable=no-member +def get_node(service_name, host_name): + """Generates Node message from params and system information. + + Args: + service_name: Name of Collector service. + host_name: Host name. + """ + return common_pb2.Node( + identifier=common_pb2.ProcessIdentifier( + host_name=socket.gethostname() if host_name is None else host_name, + pid=os.getpid(), + start_timestamp=proto_timestamp_from_time_ns( + int(time.time() * 1e9) + ), + ), + library_info=common_pb2.LibraryInfo( + language=common_pb2.LibraryInfo.Language.Value("PYTHON"), + exporter_version=otcollector_exporter_version, + core_library_version=opentelemetry_version, + ), + service_info=common_pb2.ServiceInfo(name=service_name), + ) diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py new file mode 100644 index 00000000000..f48cb5bee5c --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py @@ -0,0 +1,15 @@ +# Copyright 2020, 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. + +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-otcollector/tests/__init__.py b/ext/opentelemetry-ext-otcollector/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py new file mode 100644 index 00000000000..0e83b038bed --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py @@ -0,0 +1,305 @@ +# Copyright 2020, 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 + +import grpc +from google.protobuf.timestamp_pb2 import Timestamp +from opencensus.proto.trace.v1 import trace_pb2 + +import opentelemetry.ext.otcollector.util as utils +from opentelemetry import trace as trace_api +from opentelemetry.ext.otcollector.trace_exporter import ( + CollectorSpanExporter, + translate_to_collector, +) +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace.export import SpanExportResult +from opentelemetry.trace import TraceOptions + + +# pylint: disable=no-member +class TestCollectorSpanExporter(unittest.TestCase): + def test_constructor(self): + mock_get_node = mock.Mock() + patch = mock.patch( + "opentelemetry.ext.otcollector.util.get_node", + side_effect=mock_get_node, + ) + service_name = "testServiceName" + host_name = "testHostName" + client = grpc.insecure_channel("") + endpoint = "testEndpoint" + with patch: + exporter = CollectorSpanExporter( + service_name=service_name, + host_name=host_name, + endpoint=endpoint, + client=client, + ) + + self.assertIs(exporter.client, client) + self.assertEqual(exporter.endpoint, endpoint) + mock_get_node.assert_called_with(service_name, host_name) + + def test_get_collector_span_kind(self): + result = utils.get_collector_span_kind(trace_api.SpanKind.SERVER) + self.assertIs(result, trace_pb2.Span.SpanKind.SERVER) + result = utils.get_collector_span_kind(trace_api.SpanKind.CLIENT) + self.assertIs(result, trace_pb2.Span.SpanKind.CLIENT) + result = utils.get_collector_span_kind(trace_api.SpanKind.CONSUMER) + self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) + result = utils.get_collector_span_kind(trace_api.SpanKind.PRODUCER) + self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) + result = utils.get_collector_span_kind(trace_api.SpanKind.INTERNAL) + self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) + + def test_proto_timestamp_from_time_ns(self): + result = utils.proto_timestamp_from_time_ns(12345) + self.assertIsInstance(result, Timestamp) + self.assertEqual(result.nanos, 12345) + + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + def test_translate_to_collector(self): + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + parent_id = 0x1111111111111111 + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + ) + durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + end_times = ( + start_times[0] + durations[0], + start_times[1] + durations[1], + start_times[2] + durations[2], + ) + span_context = trace_api.SpanContext( + trace_id, + span_id, + trace_options=TraceOptions(TraceOptions.SAMPLED), + trace_state=trace_api.TraceState({"testKey": "testValue"}), + ) + parent_context = trace_api.SpanContext(trace_id, parent_id) + other_context = trace_api.SpanContext(trace_id, span_id) + event_attributes = { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + event_timestamp = base_time + 50 * 10 ** 6 + event = trace_api.Event( + name="event0", + timestamp=event_timestamp, + attributes=event_attributes, + ) + link_attributes = {"key_bool": True} + link_1 = trace_api.Link( + context=other_context, attributes=link_attributes + ) + link_2 = trace_api.Link( + context=parent_context, attributes=link_attributes + ) + span_1 = trace.Span( + name="test1", + context=span_context, + parent=parent_context, + events=(event,), + links=(link_1,), + kind=trace_api.SpanKind.CLIENT, + ) + span_2 = trace.Span( + name="test2", + context=parent_context, + parent=None, + kind=trace_api.SpanKind.SERVER, + ) + span_3 = trace.Span( + name="test3", context=other_context, links=(link_2,), parent=span_2 + ) + otel_spans = [span_1, span_2, span_3] + otel_spans[0].start(start_time=start_times[0]) + otel_spans[0].set_attribute("key_bool", False) + otel_spans[0].set_attribute("key_string", "hello_world") + otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].set_attribute("key_int", 333) + otel_spans[0].set_status( + trace_api.Status( + trace_api.status.StatusCanonicalCode.INTERNAL, + "test description", + ) + ) + otel_spans[0].end(end_time=end_times[0]) + otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].end(end_time=end_times[1]) + otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].end(end_time=end_times[2]) + output_spans = translate_to_collector(otel_spans) + + self.assertEqual(len(output_spans), 3) + self.assertEqual( + output_spans[0].trace_id, b"n\x0cc%}\xe3L\x92o\x9e\xfc\xd09''." + ) + self.assertEqual( + output_spans[0].span_id, b"4\xbf\x92\xde\xef\xc5\x8c\x92" + ) + self.assertEqual( + output_spans[0].name, trace_pb2.TruncatableString(value="test1") + ) + self.assertEqual( + output_spans[1].name, trace_pb2.TruncatableString(value="test2") + ) + self.assertEqual( + output_spans[2].name, trace_pb2.TruncatableString(value="test3") + ) + self.assertEqual( + output_spans[0].start_time.seconds, + int(start_times[0] / 1000000000), + ) + self.assertEqual( + output_spans[0].end_time.seconds, int(end_times[0] / 1000000000) + ) + self.assertEqual(output_spans[0].kind, trace_api.SpanKind.CLIENT.value) + self.assertEqual(output_spans[1].kind, trace_api.SpanKind.SERVER.value) + + self.assertEqual( + output_spans[0].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11" + ) + self.assertEqual( + output_spans[2].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11" + ) + self.assertEqual( + output_spans[0].status.code, + trace_api.status.StatusCanonicalCode.INTERNAL.value, + ) + self.assertEqual(output_spans[0].status.message, "test description") + self.assertEqual(len(output_spans[0].tracestate.entries), 1) + self.assertEqual(output_spans[0].tracestate.entries[0].key, "testKey") + self.assertEqual( + output_spans[0].tracestate.entries[0].value, "testValue" + ) + + self.assertEqual( + output_spans[0].attributes.attribute_map["key_bool"].bool_value, + False, + ) + self.assertEqual( + output_spans[0] + .attributes.attribute_map["key_string"] + .string_value.value, + "hello_world", + ) + self.assertEqual( + output_spans[0].attributes.attribute_map["key_float"].double_value, + 111.22, + ) + self.assertEqual( + output_spans[0].attributes.attribute_map["key_int"].int_value, 333 + ) + + self.assertEqual( + output_spans[0].time_events.time_event[0].time.seconds, 683647322 + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.description.value, + "event0", + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.attributes.attribute_map["annotation_bool"] + .bool_value, + True, + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.attributes.attribute_map["annotation_string"] + .string_value.value, + "annotation_test", + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.attributes.attribute_map["key_float"] + .double_value, + 0.3, + ) + + self.assertEqual( + output_spans[0].links.link[0].trace_id, + b"n\x0cc%}\xe3L\x92o\x9e\xfc\xd09''.", + ) + self.assertEqual( + output_spans[0].links.link[0].span_id, + b"4\xbf\x92\xde\xef\xc5\x8c\x92", + ) + self.assertEqual( + output_spans[0].links.link[0].type, + trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED, + ) + self.assertEqual( + output_spans[2].links.link[0].type, + trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN, + ) + self.assertEqual( + output_spans[0] + .links.link[0] + .attributes.attribute_map["key_bool"] + .bool_value, + True, + ) + + def test_export(self): + mock_client = mock.MagicMock() + mock_export = mock.MagicMock() + mock_client.Export = mock_export + host_name = "testHostName" + collector_exporter = CollectorSpanExporter( + client=mock_client, host_name=host_name + ) + + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + span_context = trace_api.SpanContext( + trace_id, span_id, trace_options=TraceOptions(TraceOptions.SAMPLED) + ) + otel_spans = [ + trace.Span( + name="test1", + context=span_context, + kind=trace_api.SpanKind.CLIENT, + ) + ] + result_status = collector_exporter.export(otel_spans) + self.assertEqual(SpanExportResult.SUCCESS, result_status) + + # pylint: disable=unsubscriptable-object + export_arg = mock_export.call_args[0] + service_request = next(export_arg[0]) + output_spans = getattr(service_request, "spans") + output_node = getattr(service_request, "node") + self.assertEqual(len(output_spans), 1) + self.assertIsNotNone(getattr(output_node, "library_info")) + self.assertIsNotNone(getattr(output_node, "service_info")) + output_identifier = getattr(output_node, "identifier") + self.assertEqual( + getattr(output_identifier, "host_name"), "testHostName" + ) diff --git a/tox.ini b/tox.ini index be7f1db9f73..23f8dbce050 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ skipsdist = True skip_missing_interpreters = True envlist = - ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. ; opentelemetry-api @@ -44,7 +43,10 @@ envlist = ; opentelemetry-ext-mysql py3{4,5,6,7,8}-test-ext-mysql pypy3-test-ext-mysql - + ; opentelemetry-ext-otcollector + py3{4,5,6,7,8}-test-ext-otcollector + ; ext-otcollector intentionally excluded from pypy3 + ; opentelemetry-ext-prometheus py3{4,5,6,7,8}-test-ext-prometheus pypy3-test-ext-prometheus @@ -103,6 +105,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests + test-ext-otcollector: ext/opentelemetry-ext-otcollector/tests test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests @@ -140,6 +143,8 @@ commands_pre = dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql + otcollector: pip install {toxinidir}/opentelemetry-sdk + otcollector: pip install {toxinidir}/ext/opentelemetry-ext-otcollector prometheus: pip install {toxinidir}/opentelemetry-sdk prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo @@ -243,4 +248,4 @@ commands = pytest {posargs} commands_post = - docker-compose down \ No newline at end of file + docker-compose down