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