From 28c6463c20509ba09ab3de2dc853ae116a8f418c Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 6 Dec 2019 11:28:30 -0800 Subject: [PATCH 01/11] adding zipkin exporter Signed-off-by: Alex Boten --- ext/opentelemetry-ext-zipkin/setup.cfg | 47 ++++ ext/opentelemetry-ext-zipkin/setup.py | 26 ++ .../src/opentelemetry/ext/zipkin/__init__.py | 188 ++++++++++++++ .../src/opentelemetry/ext/zipkin/version.py | 16 ++ .../tests/__init__.py | 14 ++ .../tests/test_zipkin_exporter.py | 237 ++++++++++++++++++ tox.ini | 14 +- 7 files changed, 538 insertions(+), 4 deletions(-) create mode 100644 ext/opentelemetry-ext-zipkin/setup.cfg create mode 100644 ext/opentelemetry-ext-zipkin/setup.py create mode 100644 ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py create mode 100644 ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py create mode 100644 ext/opentelemetry-ext-zipkin/tests/__init__.py create mode 100644 ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py diff --git a/ext/opentelemetry-ext-zipkin/setup.cfg b/ext/opentelemetry-ext-zipkin/setup.cfg new file mode 100644 index 00000000000..89d60d149a7 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/setup.cfg @@ -0,0 +1,47 @@ +# 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. +# +[metadata] +name = opentelemetry-ext-zipkin +description = Zipkin Span Exporter for OpenTelemetry +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-zipkin +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 = + requests~=2.7 + opentelemetry-api + opentelemetry-sdk + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-zipkin/setup.py b/ext/opentelemetry-ext-zipkin/setup.py new file mode 100644 index 00000000000..f93bbad4490 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/setup.py @@ -0,0 +1,26 @@ +# 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 os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "zipkin", "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-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py new file mode 100644 index 00000000000..0246e36b32c --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -0,0 +1,188 @@ +# Copyright 2018, OpenCensus Authors +# 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. + +"""Zipkin Span Exporter for OpenTelemetry.""" + +import json +import logging +from typing import Optional, Sequence + +import requests + +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace import Span, SpanContext, SpanKind + +DEFAULT_ENDPOINT = "/api/v2/spans" +DEFAULT_HOST_NAME = "localhost" +DEFAULT_PORT = 9411 +DEFAULT_PROTOCOL = "http" +ZIPKIN_HEADERS = {"Content-Type": "application/json"} + +SPAN_KIND_MAP = { + SpanKind.INTERNAL: None, + SpanKind.SERVER: "SERVER", + SpanKind.CLIENT: "CLIENT", + SpanKind.PRODUCER: "PRODUCER", + SpanKind.CONSUMER: "CONSUMER", +} + +SUCCESS_STATUS_CODE = (200, 202) + +logger = logging.getLogger(__name__) + + +class ZipkinSpanExporter(SpanExporter): + """Zipkin span exporter for OpenTelemetry. + + Args: + service_name: Service that logged an annotation in a trace.Classifier + when query for spans. + host_name: The host name of the Zipkin server + port: The port of the Zipkin server + endpoint: The endpoint of the Zipkin server + protocol: The protocol used for the request. + ipv4: Primary IPv4 address associated with this connection. + ipv6: Primary IPv6 address associated with this connection. + """ + + def __init__( + self, + service_name: str, + host_name: Optional[str] = DEFAULT_HOST_NAME, + port: Optional[int] = DEFAULT_PORT, + endpoint: Optional[str] = DEFAULT_ENDPOINT, + protocol: Optional[str] = DEFAULT_PROTOCOL, + ipv4: Optional[str] = None, + ipv6: Optional[str] = None, + ): + self.service_name = service_name + self.host_name = host_name + self.port = port + self.endpoint = endpoint + self.protocol = protocol + self.url = self.get_url + self.ipv4 = ipv4 + self.ipv6 = ipv6 + + @property + def get_url(self): + return "{}://{}:{}{}".format( + self.protocol, self.host_name, self.port, self.endpoint + ) + + def export(self, spans: Sequence[Span]) -> "SpanExportResult": + zipkin_spans = self._translate_to_zipkin(spans) + print("about to call") + result = requests.post( + url=self.url, data=json.dumps(zipkin_spans), headers=ZIPKIN_HEADERS + ) + + if result.status_code not in SUCCESS_STATUS_CODE: + logger.error( + "Traces cannot be uploaded; status code: %s, message %s", + result.status_code, + result.text, + ) + # TODO: should we retry here? + return SpanExportResult.FAILED_NOT_RETRYABLE + return SpanExportResult.SUCCESS + + def _translate_to_zipkin(self, spans: Sequence[Span]): + + local_endpoint = { + "serviceName": self.service_name, + "port": self.port, + } + + if self.ipv4 is not None: + local_endpoint["ipv4"] = self.ipv4 + + if self.ipv6 is not None: + local_endpoint["ipv6"] = self.ipv6 + + zipkin_spans = [] + for span in spans: + ctx = span.get_context() + trace_id = ctx.trace_id + span_id = ctx.span_id + + # Timestamp in zipkin spans is int of microseconds. + start_timestamp_mus = _nsec_to_usec_round(span.start_time) + duration_mus = _nsec_to_usec_round(span.end_time - span.start_time) + + zipkin_span = { + "traceId": _format(trace_id), + "id": _format(span_id), + "name": span.name, + "timestamp": start_timestamp_mus, + "duration": duration_mus, + "localEndpoint": local_endpoint, + "kind": SPAN_KIND_MAP[span.kind], + "tags": _extract_tags_from_span(span.attributes), + "annotations": _extract_annotations_from_events(span.events), + } + + if isinstance(span.parent, Span): + zipkin_span["parentId"] = _format( + span.parent.get_context().span_id + ) + elif isinstance(span.parent, SpanContext): + zipkin_span["parentId"] = _format(span.parent.span_id) + + zipkin_spans.append(zipkin_span) + return zipkin_spans + + def shutdown(self) -> None: + pass + + +def _format(unformatted_id): + return format(unformatted_id, "x") + + +def _extract_tags_from_span(attr): + if not attr: + return None + tags = {} + for attribute_key, attribute_value in attr.items(): + if isinstance(attribute_value, (int, bool, float)): + value = str(attribute_value) + elif isinstance(attribute_value, str): + res = attribute_value[:128] + value = res + else: + logger.warning("Could not serialize tag %s", attribute_key) + continue + tags[attribute_key] = value + return tags + + +def _extract_annotations_from_events(events): + if not events: + return None + annotations = [] + for event in events: + annotations.append( + { + "timestamp": _nsec_to_usec_round(event.timestamp), + "value": event.name, + } + ) + return annotations + + +def _nsec_to_usec_round(nsec): + """Round nanoseconds to microseconds""" + return (nsec + 500) // 10 ** 3 diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py new file mode 100644 index 00000000000..1f3a27dc85e --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -0,0 +1,16 @@ +# Copyright 2019, OpenCensus Authors +# 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. + +__version__ = "0.3.dev0" diff --git a/ext/opentelemetry-ext-zipkin/tests/__init__.py b/ext/opentelemetry-ext-zipkin/tests/__init__.py new file mode 100644 index 00000000000..8c7aaf8e47e --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/tests/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2019, OpenCensus Authors +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py new file mode 100644 index 00000000000..c1a04f5691d --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -0,0 +1,237 @@ +# Copyright 2018, OpenCensus Authors +# 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 json +import unittest +from unittest.mock import MagicMock, patch + +from opentelemetry import trace as trace_api +from opentelemetry.ext.zipkin import ZipkinSpanExporter +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace.export import SpanExportResult + + +class MockResponse: + def __init__(self, status_code): + self.status_code = status_code + self.text = status_code + + +class TestZipkinSpanExporter(unittest.TestCase): + def setUp(self): + # create and save span to be used in tests + context = trace_api.SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + ) + + self._test_span = trace.Span("test_span", context=context) + self._test_span.start() + self._test_span.end() + + def test_constructor_default(self): + """Test the default values assigned by constructor.""" + service_name = "my-service-name" + host_name = "localhost" + port = 9411 + endpoint = "/api/v2/spans" + exporter = ZipkinSpanExporter(service_name) + ipv4 = None + ipv6 = None + protocol = "http" + url = "http://localhost:9411/api/v2/spans" + + self.assertEqual(exporter.service_name, service_name) + self.assertEqual(exporter.host_name, host_name) + self.assertEqual(exporter.port, port) + self.assertEqual(exporter.endpoint, endpoint) + self.assertEqual(exporter.ipv4, ipv4) + self.assertEqual(exporter.ipv6, ipv6) + self.assertEqual(exporter.protocol, protocol) + self.assertEqual(exporter.url, url) + + def test_constructor_explicit(self): + """Test the constructor passing all the options.""" + service_name = "my-opentelemetry-zipkin" + host_name = "opentelemetry.io" + port = 15875 + endpoint = "/myapi/traces?format=zipkin" + ipv4 = "1.2.3.4" + ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + protocol = "https" + url = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" + exporter = ZipkinSpanExporter( + service_name=service_name, + host_name=host_name, + port=port, + endpoint=endpoint, + ipv4=ipv4, + ipv6=ipv6, + protocol=protocol, + ) + + self.assertEqual(exporter.service_name, service_name) + self.assertEqual(exporter.host_name, host_name) + self.assertEqual(exporter.port, port) + self.assertEqual(exporter.endpoint, endpoint) + self.assertEqual(exporter.ipv4, ipv4) + self.assertEqual(exporter.ipv6, ipv6) + self.assertEqual(exporter.protocol, protocol) + self.assertEqual(exporter.url, url) + + # pylint: disable=too-many-locals + def test_export(self): + + span_names = ("test1", "test2", "test3") + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + parent_id = 0x1111111111111111 + other_id = 0x2222222222222222 + + 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) + parent_context = trace_api.SpanContext(trace_id, parent_id) + other_context = trace_api.SpanContext(trace_id, other_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 = trace_api.Link( + context=other_context, attributes=link_attributes + ) + + otel_spans = [ + trace.Span( + name=span_names[0], + context=span_context, + parent=parent_context, + events=(event,), + links=(link,), + ), + trace.Span( + name=span_names[1], context=parent_context, parent=None + ), + trace.Span(name=span_names[2], context=other_context, parent=None), + ] + + otel_spans[0].start_time = start_times[0] + # added here to preserve order + 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].end_time = end_times[0] + + otel_spans[1].start_time = start_times[1] + otel_spans[1].end_time = end_times[1] + + otel_spans[2].start_time = start_times[2] + otel_spans[2].end_time = end_times[2] + + service_name = "test-service" + local_endpoint = { + "serviceName": service_name, + "port": 9411, + } + + exporter = ZipkinSpanExporter(service_name) + expected = [ + { + "traceId": format(trace_id, "x"), + "id": format(span_id, "x"), + "name": span_names[0], + "timestamp": start_times[0] // 10 ** 3, + "duration": durations[0] // 10 ** 3, + "localEndpoint": local_endpoint, + "kind": None, + "tags": { + "key_bool": "False", + "key_string": "hello_world", + "key_float": "111.22", + }, + "annotations": [ + { + "timestamp": event_timestamp // 10 ** 3, + "value": "event0", + } + ], + "parentId": format(parent_id, "x"), + }, + { + "traceId": format(trace_id, "x"), + "id": format(parent_id, "x"), + "name": span_names[1], + "timestamp": start_times[1] // 10 ** 3, + "duration": durations[1] // 10 ** 3, + "localEndpoint": local_endpoint, + "kind": None, + "tags": None, + "annotations": None, + }, + { + "traceId": format(trace_id, "x"), + "id": format(other_id, "x"), + "name": span_names[2], + "timestamp": start_times[2] // 10 ** 3, + "duration": durations[2] // 10 ** 3, + "localEndpoint": local_endpoint, + "kind": None, + "tags": None, + "annotations": None, + }, + ] + + mock_post = MagicMock() + with patch("requests.post", mock_post): + mock_post.return_value = MockResponse(200) + status = exporter.export(otel_spans) + self.assertEqual(SpanExportResult.SUCCESS, status) + + mock_post.assert_called_with( + url="http://localhost:9411/api/v2/spans", + data=json.dumps(expected), + headers={"Content-Type": "application/json"}, + ) + + @patch("requests.post") + def test_invalid_response(self, mock_post): + mock_post.return_value = MockResponse(404) + spans = [] + exporter = ZipkinSpanExporter("test-service") + status = exporter.export(spans) + self.assertEqual(SpanExportResult.FAILED_NOT_RETRYABLE, status) diff --git a/tox.ini b/tox.ini index 9898094ca3d..363f67ed19a 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,10 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} - py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} - pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} + pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim} py3{4,5,6,7,8}-coverage ; Coverage is temporarily disabled for pypy3 due to the pytest bug. @@ -39,6 +39,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests + test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests test-ext-flask: ext/opentelemetry-ext-flask/tests test-example-app: examples/opentelemetry-example-app/tests test-example-basic-tracer: examples/basic_tracer/tests @@ -72,6 +73,7 @@ commands_pre = jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger opentracing-shim: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-opentracing-shim + zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin ; In order to get a healthy coverage report, ; we have to install packages in editable mode. @@ -82,6 +84,7 @@ commands_pre = coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-testutil coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-zipkin coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-flask[test] coverage: pip install -e {toxinidir}/examples/opentelemetry-example-app @@ -119,6 +122,7 @@ commands_pre = pip install -e {toxinidir}/ext/opentelemetry-ext-pymongo pip install -e {toxinidir}/ext/opentelemetry-ext-testutil pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi + pip install -e {toxinidir}/ext/opentelemetry-ext-zipkin pip install -e {toxinidir}/ext/opentelemetry-ext-flask[test] pip install -e {toxinidir}/examples/opentelemetry-example-app pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim @@ -142,6 +146,8 @@ commands = ext/opentelemetry-ext-testutil/src/opentelemetry \ ext/opentelemetry-ext-wsgi/src/ \ ext/opentelemetry-ext-wsgi/tests/ \ + ext/opentelemetry-ext-zipkin/src/opentelemetry \ + ext/opentelemetry-ext-zipkin/tests/ \ ext/opentelemetry-ext-flask/src/ \ ext/opentelemetry-ext-flask/tests/ \ examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ From ec63dec323d6e0105b45090b029cdbeea998425c Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 6 Dec 2019 11:47:31 -0800 Subject: [PATCH 02/11] add coverage for zipkin and flask --- scripts/coverage.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index bddf39a90cd..9b981b0817c 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -17,10 +17,12 @@ coverage erase cov opentelemetry-api cov opentelemetry-sdk +cov ext/opentelemetry-ext-flask cov ext/opentelemetry-ext-http-requests cov ext/opentelemetry-ext-jaeger cov ext/opentelemetry-ext-opentracing-shim cov ext/opentelemetry-ext-wsgi +cov ext/opentelemetry-ext-zipkin cov examples/opentelemetry-example-app coverage report From 9d8d8078b2fe46ae8b09caf5868e0e4fa1051434 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 16 Dec 2019 13:56:11 -0800 Subject: [PATCH 03/11] cleaning up copyright Signed-off-by: Alex Boten --- .../src/opentelemetry/ext/zipkin/__init__.py | 1 - .../src/opentelemetry/ext/zipkin/version.py | 1 - ext/opentelemetry-ext-zipkin/tests/__init__.py | 1 - ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py | 1 - 4 files changed, 4 deletions(-) diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index 0246e36b32c..5918b581b7f 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -1,4 +1,3 @@ -# Copyright 2018, OpenCensus Authors # Copyright 2019, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 1f3a27dc85e..93ef792d051 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -1,4 +1,3 @@ -# Copyright 2019, OpenCensus Authors # Copyright 2019, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/ext/opentelemetry-ext-zipkin/tests/__init__.py b/ext/opentelemetry-ext-zipkin/tests/__init__.py index 8c7aaf8e47e..d853a7bcf65 100644 --- a/ext/opentelemetry-ext-zipkin/tests/__init__.py +++ b/ext/opentelemetry-ext-zipkin/tests/__init__.py @@ -1,4 +1,3 @@ -# Copyright 2019, OpenCensus Authors # Copyright 2019, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index c1a04f5691d..97fd3ec04dd 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -1,4 +1,3 @@ -# Copyright 2018, OpenCensus Authors # Copyright 2019, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); From c3e415e32634b6e010d2681da56d35e9aef07f04 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 16 Dec 2019 14:02:08 -0800 Subject: [PATCH 04/11] removing get_url --- .../src/opentelemetry/ext/zipkin/__init__.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index 5918b581b7f..6e32b750a40 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -71,19 +71,14 @@ def __init__( self.port = port self.endpoint = endpoint self.protocol = protocol - self.url = self.get_url - self.ipv4 = ipv4 - self.ipv6 = ipv6 - - @property - def get_url(self): - return "{}://{}:{}{}".format( + self.url = "{}://{}:{}{}".format( self.protocol, self.host_name, self.port, self.endpoint ) + self.ipv4 = ipv4 + self.ipv6 = ipv6 def export(self, spans: Sequence[Span]) -> "SpanExportResult": zipkin_spans = self._translate_to_zipkin(spans) - print("about to call") result = requests.post( url=self.url, data=json.dumps(zipkin_spans), headers=ZIPKIN_HEADERS ) From cb06b40f24e7e461249c2dc139eda22e2bc58d24 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 16 Dec 2019 14:06:54 -0800 Subject: [PATCH 05/11] remove Optional from required params Signed-off-by: Alex Boten --- .../src/opentelemetry/ext/zipkin/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index 6e32b750a40..83a94f63bd8 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -59,10 +59,10 @@ class ZipkinSpanExporter(SpanExporter): def __init__( self, service_name: str, - host_name: Optional[str] = DEFAULT_HOST_NAME, - port: Optional[int] = DEFAULT_PORT, - endpoint: Optional[str] = DEFAULT_ENDPOINT, - protocol: Optional[str] = DEFAULT_PROTOCOL, + host_name: str = DEFAULT_HOST_NAME, + port: int = DEFAULT_PORT, + endpoint: str = DEFAULT_ENDPOINT, + protocol: str = DEFAULT_PROTOCOL, ipv4: Optional[str] = None, ipv6: Optional[str] = None, ): From 93331ff73905ab0dad11110bdac45737098c2d26 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 16 Dec 2019 14:11:46 -0800 Subject: [PATCH 06/11] forgot to save tox.ini Signed-off-by: Alex Boten --- tox.ini | 57 --------------------------------------------------------- 1 file changed, 57 deletions(-) diff --git a/tox.ini b/tox.ini index 7e4fdf5a992..8a4321a95c5 100644 --- a/tox.ini +++ b/tox.ini @@ -76,20 +76,7 @@ commands_pre = ; In order to get a healthy coverage report, ; we have to install packages in editable mode. -<<<<<<< HEAD - coverage: pip install -e {toxinidir}/opentelemetry-api - coverage: pip install -e {toxinidir}/opentelemetry-sdk - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-testutil - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-zipkin - coverage: pip install -e {toxinidir}/ext/opentelemetry-ext-flask[test] - coverage: pip install -e {toxinidir}/examples/opentelemetry-example-app -======= coverage: python {toxinidir}/scripts/eachdist.py install --editable ->>>>>>> origin/master ; Using file:// here because otherwise tox invokes just "pip install ; opentelemetry-api", leading to an error @@ -119,54 +106,10 @@ deps = black commands_pre = -<<<<<<< HEAD - pip install -e {toxinidir}/opentelemetry-api - pip install -e {toxinidir}/opentelemetry-sdk - pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests - pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger - pip install -e {toxinidir}/ext/opentelemetry-ext-pymongo - pip install -e {toxinidir}/ext/opentelemetry-ext-testutil - pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi - pip install -e {toxinidir}/ext/opentelemetry-ext-zipkin - pip install -e {toxinidir}/ext/opentelemetry-ext-flask[test] - pip install -e {toxinidir}/examples/opentelemetry-example-app - pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim - -commands = -; Prefer putting everything in one pylint command to profit from duplication -; warnings. - black --check --diff . - pylint opentelemetry-api/src/opentelemetry \ - opentelemetry-api/tests/ \ - opentelemetry-sdk/src/opentelemetry \ - opentelemetry-sdk/tests/ \ - ext/opentelemetry-ext-http-requests/src/ \ - ext/opentelemetry-ext-http-requests/tests/ \ - ext/opentelemetry-ext-jaeger/src/opentelemetry \ - ext/opentelemetry-ext-jaeger/tests/ \ - ext/opentelemetry-ext-opentracing-shim/src/ \ - ext/opentelemetry-ext-opentracing-shim/tests/ \ - ext/opentelemetry-ext-pymongo/src/opentelemetry \ - ext/opentelemetry-ext-pymongo/tests/ \ - ext/opentelemetry-ext-testutil/src/opentelemetry \ - ext/opentelemetry-ext-wsgi/src/ \ - ext/opentelemetry-ext-wsgi/tests/ \ - ext/opentelemetry-ext-zipkin/src/opentelemetry \ - ext/opentelemetry-ext-zipkin/tests/ \ - ext/opentelemetry-ext-flask/src/ \ - ext/opentelemetry-ext-flask/tests/ \ - examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ - examples/opentelemetry-example-app/tests/ \ - examples/basic_tracer/ \ - examples/http/ - flake8 . - isort --check-only --diff --recursive . -======= python scripts/eachdist.py install --editable commands = python scripts/eachdist.py lint --check-only ->>>>>>> origin/master [testenv:docs] deps = From e93e7b059a63a23be7f823a8d6c022721aac1cfb Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 20 Dec 2019 08:21:03 -0800 Subject: [PATCH 07/11] adding readme + changelog Signed-off-by: Alex Boten --- ext/opentelemetry-ext-zipkin/CHANGELOG.md | 4 ++ ext/opentelemetry-ext-zipkin/README.rst | 66 +++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 ext/opentelemetry-ext-zipkin/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-zipkin/README.rst diff --git a/ext/opentelemetry-ext-zipkin/CHANGELOG.md b/ext/opentelemetry-ext-zipkin/CHANGELOG.md new file mode 100644 index 00000000000..617d979ab29 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## Unreleased + diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst new file mode 100644 index 00000000000..eb9c9d34c42 --- /dev/null +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -0,0 +1,66 @@ +OpenTelemetry Zipkin Exporter +============================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-zipkin.svg + :target: https://pypi.org/project/opentelemetry-ext-zipkin/ + +This library allows to export tracing data to `Zipkin `_. + +Installation +------------ + +:: + + pip install opentelemetry-ext-zipkin + + +Usage +----- + +The **OpenTelemetry Zipkin Exporter** allows to export `OpenTelemetry`_ traces to `Zipkin`_. +This exporter always send traces to the configured Zipkin collector using HTTP. + + +.. _Zipkin: https://zipkin.io/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext import zipkin + from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + tracer = trace.tracer_source().get_tracer(__name__) + + # create a ZipkinSpanExporter + zipkin_exporter = zipkin.ZipkinSpanExporter( + service_name="my-helloworld-service", + # optional: + # host_name="localhost", + # port=9411, + # endpoint: str = "/api/v2/spans", + # protocol: str = "http", + # ipv4: Optional[str] = None, + # ipv6: Optional[str] = None, + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(zipkin_exporter) + + # add to the tracer + trace.tracer_source().add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +The `examples <./examples>`_ folder contains more elaborated examples. + +References +---------- + +* `Zipkin `_ +* `OpenTelemetry Project `_ From 8946f4ae42129a181461a9f2ceda79cf740f9753 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 20 Dec 2019 08:23:32 -0800 Subject: [PATCH 08/11] minor fix to readme Signed-off-by: Alex Boten --- ext/opentelemetry-ext-zipkin/README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst index eb9c9d34c42..bf46112a2ee 100644 --- a/ext/opentelemetry-ext-zipkin/README.rst +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -42,10 +42,10 @@ This exporter always send traces to the configured Zipkin collector using HTTP. # optional: # host_name="localhost", # port=9411, - # endpoint: str = "/api/v2/spans", - # protocol: str = "http", - # ipv4: Optional[str] = None, - # ipv6: Optional[str] = None, + # endpoint="/api/v2/spans", + # protocol="http", + # ipv4="", + # ipv6="", ) # Create a BatchExportSpanProcessor and add the exporter to it From 0e21ed84820e00446bf07bce58ea25362c718fcd Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 20 Dec 2019 08:44:10 -0800 Subject: [PATCH 09/11] code cleanup Signed-off-by: Alex Boten --- .../src/opentelemetry/ext/zipkin/__init__.py | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index 83a94f63bd8..f4bcaf40393 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -37,7 +37,7 @@ SpanKind.CONSUMER: "CONSUMER", } -SUCCESS_STATUS_CODE = (200, 202) +SUCCESS_STATUS_CODES = (200, 202) logger = logging.getLogger(__name__) @@ -77,13 +77,13 @@ def __init__( self.ipv4 = ipv4 self.ipv6 = ipv6 - def export(self, spans: Sequence[Span]) -> "SpanExportResult": + def export(self, spans: Sequence[Span]) -> SpanExportResult: zipkin_spans = self._translate_to_zipkin(spans) result = requests.post( url=self.url, data=json.dumps(zipkin_spans), headers=ZIPKIN_HEADERS ) - if result.status_code not in SUCCESS_STATUS_CODE: + if result.status_code not in SUCCESS_STATUS_CODES: logger.error( "Traces cannot be uploaded; status code: %s, message %s", result.status_code, @@ -117,8 +117,8 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): duration_mus = _nsec_to_usec_round(span.end_time - span.start_time) zipkin_span = { - "traceId": _format(trace_id), - "id": _format(span_id), + "traceId": format(trace_id, "x"), + "id": format(span_id, "x"), "name": span.name, "timestamp": start_timestamp_mus, "duration": duration_mus, @@ -129,11 +129,11 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): } if isinstance(span.parent, Span): - zipkin_span["parentId"] = _format( - span.parent.get_context().span_id + zipkin_span["parentId"] = format( + span.parent.get_context().span_id, "x" ) elif isinstance(span.parent, SpanContext): - zipkin_span["parentId"] = _format(span.parent.span_id) + zipkin_span["parentId"] = format(span.parent.span_id, "x") zipkin_spans.append(zipkin_span) return zipkin_spans @@ -142,10 +142,6 @@ def shutdown(self) -> None: pass -def _format(unformatted_id): - return format(unformatted_id, "x") - - def _extract_tags_from_span(attr): if not attr: return None @@ -154,8 +150,7 @@ def _extract_tags_from_span(attr): if isinstance(attribute_value, (int, bool, float)): value = str(attribute_value) elif isinstance(attribute_value, str): - res = attribute_value[:128] - value = res + value = attribute_value[:128] else: logger.warning("Could not serialize tag %s", attribute_key) continue @@ -164,17 +159,14 @@ def _extract_tags_from_span(attr): def _extract_annotations_from_events(events): - if not events: - return None - annotations = [] - for event in events: - annotations.append( - { - "timestamp": _nsec_to_usec_round(event.timestamp), - "value": event.name, - } - ) - return annotations + return ( + [ + {"timestamp": _nsec_to_usec_round(e.timestamp), "value": e.name} + for e in events + ] + if events + else None + ) def _nsec_to_usec_round(nsec): From 47360044c898de3789fd218c902fb9d0426e8ce8 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 20 Dec 2019 08:49:49 -0800 Subject: [PATCH 10/11] code cleanup - adding retry configuration option - adding link to zipkin documentation for microseconds Signed-off-by: Alex Boten --- ext/opentelemetry-ext-zipkin/README.rst | 1 + .../src/opentelemetry/ext/zipkin/__init__.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst index bf46112a2ee..f91d0c2c6a7 100644 --- a/ext/opentelemetry-ext-zipkin/README.rst +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -46,6 +46,7 @@ This exporter always send traces to the configured Zipkin collector using HTTP. # protocol="http", # ipv4="", # ipv6="", + # retry=False, ) # Create a BatchExportSpanProcessor and add the exporter to it diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index f4bcaf40393..e09aa24242b 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -27,6 +27,7 @@ DEFAULT_HOST_NAME = "localhost" DEFAULT_PORT = 9411 DEFAULT_PROTOCOL = "http" +DEFAULT_RETRY = False ZIPKIN_HEADERS = {"Content-Type": "application/json"} SPAN_KIND_MAP = { @@ -54,6 +55,7 @@ class ZipkinSpanExporter(SpanExporter): protocol: The protocol used for the request. ipv4: Primary IPv4 address associated with this connection. ipv6: Primary IPv6 address associated with this connection. + retry: Set to True to configure the exporter to retry on failure. """ def __init__( @@ -65,6 +67,7 @@ def __init__( protocol: str = DEFAULT_PROTOCOL, ipv4: Optional[str] = None, ipv6: Optional[str] = None, + retry: Optional[str] = DEFAULT_RETRY, ): self.service_name = service_name self.host_name = host_name @@ -76,6 +79,7 @@ def __init__( ) self.ipv4 = ipv4 self.ipv6 = ipv6 + self.retry = retry def export(self, spans: Sequence[Span]) -> SpanExportResult: zipkin_spans = self._translate_to_zipkin(spans) @@ -89,7 +93,9 @@ def export(self, spans: Sequence[Span]) -> SpanExportResult: result.status_code, result.text, ) - # TODO: should we retry here? + + if self.retry: + return SpanExportResult.FAILED_RETRYABLE return SpanExportResult.FAILED_NOT_RETRYABLE return SpanExportResult.SUCCESS @@ -108,11 +114,12 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): zipkin_spans = [] for span in spans: - ctx = span.get_context() - trace_id = ctx.trace_id - span_id = ctx.span_id + context = span.get_context() + trace_id = context.trace_id + span_id = context.span_id # Timestamp in zipkin spans is int of microseconds. + # see: https://zipkin.io/pages/instrumenting.html start_timestamp_mus = _nsec_to_usec_round(span.start_time) duration_mus = _nsec_to_usec_round(span.end_time - span.start_time) From b20310c5f79be255ac739a1650693735bfd2299b Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Fri, 20 Dec 2019 14:52:52 -0800 Subject: [PATCH 11/11] add debug field Signed-off-by: Alex Boten --- .../src/opentelemetry/ext/zipkin/__init__.py | 3 +++ .../tests/test_zipkin_exporter.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index e09aa24242b..e0b5791d1e1 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -135,6 +135,9 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): "annotations": _extract_annotations_from_events(span.events), } + if context.trace_options.sampled: + zipkin_span["debug"] = 1 + if isinstance(span.parent, Span): zipkin_span["parentId"] = format( span.parent.get_context().span_id, "x" diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index 97fd3ec04dd..745c662f53b 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -20,6 +20,7 @@ from opentelemetry.ext.zipkin import ZipkinSpanExporter from opentelemetry.sdk import trace from opentelemetry.sdk.trace.export import SpanExportResult +from opentelemetry.trace import TraceOptions class MockResponse: @@ -112,7 +113,11 @@ def test_export(self): start_times[2] + durations[2], ) - span_context = trace_api.SpanContext(trace_id, span_id) + span_context = trace_api.SpanContext( + trace_id, + span_id, + trace_options=TraceOptions(TraceOptions.SAMPLED), + ) parent_context = trace_api.SpanContext(trace_id, parent_id) other_context = trace_api.SpanContext(trace_id, other_id) @@ -189,6 +194,7 @@ def test_export(self): "value": "event0", } ], + "debug": 1, "parentId": format(parent_id, "x"), }, {