Skip to content

Commit 4458698

Browse files
codebotentoumorokoshi
authored andcommitted
Adding Zipkin exporter (#320)
Signed-off-by: Alex Boten <[email protected]>
1 parent 8fa21e6 commit 4458698

File tree

10 files changed

+606
-4
lines changed

10 files changed

+606
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Changelog
2+
3+
## Unreleased
4+
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
OpenTelemetry Zipkin Exporter
2+
=============================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-zipkin.svg
7+
:target: https://pypi.org/project/opentelemetry-ext-zipkin/
8+
9+
This library allows to export tracing data to `Zipkin <https://zipkin.io/>`_.
10+
11+
Installation
12+
------------
13+
14+
::
15+
16+
pip install opentelemetry-ext-zipkin
17+
18+
19+
Usage
20+
-----
21+
22+
The **OpenTelemetry Zipkin Exporter** allows to export `OpenTelemetry`_ traces to `Zipkin`_.
23+
This exporter always send traces to the configured Zipkin collector using HTTP.
24+
25+
26+
.. _Zipkin: https://zipkin.io/
27+
.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/
28+
29+
.. code:: python
30+
31+
from opentelemetry import trace
32+
from opentelemetry.ext import zipkin
33+
from opentelemetry.sdk.trace import TracerSource
34+
from opentelemetry.sdk.trace.export import BatchExportSpanProcessor
35+
36+
trace.set_preferred_tracer_source_implementation(lambda T: TracerSource())
37+
tracer = trace.tracer_source().get_tracer(__name__)
38+
39+
# create a ZipkinSpanExporter
40+
zipkin_exporter = zipkin.ZipkinSpanExporter(
41+
service_name="my-helloworld-service",
42+
# optional:
43+
# host_name="localhost",
44+
# port=9411,
45+
# endpoint="/api/v2/spans",
46+
# protocol="http",
47+
# ipv4="",
48+
# ipv6="",
49+
# retry=False,
50+
)
51+
52+
# Create a BatchExportSpanProcessor and add the exporter to it
53+
span_processor = BatchExportSpanProcessor(zipkin_exporter)
54+
55+
# add to the tracer
56+
trace.tracer_source().add_span_processor(span_processor)
57+
58+
with tracer.start_as_current_span("foo"):
59+
print("Hello world!")
60+
61+
The `examples <./examples>`_ folder contains more elaborated examples.
62+
63+
References
64+
----------
65+
66+
* `Zipkin <https://zipkin.io/>`_
67+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
[metadata]
16+
name = opentelemetry-ext-zipkin
17+
description = Zipkin Span Exporter for OpenTelemetry
18+
long_description = file: README.rst
19+
long_description_content_type = text/x-rst
20+
author = OpenTelemetry Authors
21+
author_email = [email protected]
22+
url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-zipkin
23+
platforms = any
24+
license = Apache-2.0
25+
classifiers =
26+
Development Status :: 3 - Alpha
27+
Intended Audience :: Developers
28+
License :: OSI Approved :: Apache Software License
29+
Programming Language :: Python
30+
Programming Language :: Python :: 3
31+
Programming Language :: Python :: 3.4
32+
Programming Language :: Python :: 3.5
33+
Programming Language :: Python :: 3.6
34+
Programming Language :: Python :: 3.7
35+
36+
[options]
37+
python_requires = >=3.4
38+
package_dir=
39+
=src
40+
packages=find_namespace:
41+
install_requires =
42+
requests~=2.7
43+
opentelemetry-api
44+
opentelemetry-sdk
45+
46+
[options.packages.find]
47+
where = src

ext/opentelemetry-ext-zipkin/setup.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import os
15+
16+
import setuptools
17+
18+
BASE_DIR = os.path.dirname(__file__)
19+
VERSION_FILENAME = os.path.join(
20+
BASE_DIR, "src", "opentelemetry", "ext", "zipkin", "version.py"
21+
)
22+
PACKAGE_INFO = {}
23+
with open(VERSION_FILENAME) as f:
24+
exec(f.read(), PACKAGE_INFO)
25+
26+
setuptools.setup(version=PACKAGE_INFO["__version__"])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Zipkin Span Exporter for OpenTelemetry."""
16+
17+
import json
18+
import logging
19+
from typing import Optional, Sequence
20+
21+
import requests
22+
23+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
24+
from opentelemetry.trace import Span, SpanContext, SpanKind
25+
26+
DEFAULT_ENDPOINT = "/api/v2/spans"
27+
DEFAULT_HOST_NAME = "localhost"
28+
DEFAULT_PORT = 9411
29+
DEFAULT_PROTOCOL = "http"
30+
DEFAULT_RETRY = False
31+
ZIPKIN_HEADERS = {"Content-Type": "application/json"}
32+
33+
SPAN_KIND_MAP = {
34+
SpanKind.INTERNAL: None,
35+
SpanKind.SERVER: "SERVER",
36+
SpanKind.CLIENT: "CLIENT",
37+
SpanKind.PRODUCER: "PRODUCER",
38+
SpanKind.CONSUMER: "CONSUMER",
39+
}
40+
41+
SUCCESS_STATUS_CODES = (200, 202)
42+
43+
logger = logging.getLogger(__name__)
44+
45+
46+
class ZipkinSpanExporter(SpanExporter):
47+
"""Zipkin span exporter for OpenTelemetry.
48+
49+
Args:
50+
service_name: Service that logged an annotation in a trace.Classifier
51+
when query for spans.
52+
host_name: The host name of the Zipkin server
53+
port: The port of the Zipkin server
54+
endpoint: The endpoint of the Zipkin server
55+
protocol: The protocol used for the request.
56+
ipv4: Primary IPv4 address associated with this connection.
57+
ipv6: Primary IPv6 address associated with this connection.
58+
retry: Set to True to configure the exporter to retry on failure.
59+
"""
60+
61+
def __init__(
62+
self,
63+
service_name: str,
64+
host_name: str = DEFAULT_HOST_NAME,
65+
port: int = DEFAULT_PORT,
66+
endpoint: str = DEFAULT_ENDPOINT,
67+
protocol: str = DEFAULT_PROTOCOL,
68+
ipv4: Optional[str] = None,
69+
ipv6: Optional[str] = None,
70+
retry: Optional[str] = DEFAULT_RETRY,
71+
):
72+
self.service_name = service_name
73+
self.host_name = host_name
74+
self.port = port
75+
self.endpoint = endpoint
76+
self.protocol = protocol
77+
self.url = "{}://{}:{}{}".format(
78+
self.protocol, self.host_name, self.port, self.endpoint
79+
)
80+
self.ipv4 = ipv4
81+
self.ipv6 = ipv6
82+
self.retry = retry
83+
84+
def export(self, spans: Sequence[Span]) -> SpanExportResult:
85+
zipkin_spans = self._translate_to_zipkin(spans)
86+
result = requests.post(
87+
url=self.url, data=json.dumps(zipkin_spans), headers=ZIPKIN_HEADERS
88+
)
89+
90+
if result.status_code not in SUCCESS_STATUS_CODES:
91+
logger.error(
92+
"Traces cannot be uploaded; status code: %s, message %s",
93+
result.status_code,
94+
result.text,
95+
)
96+
97+
if self.retry:
98+
return SpanExportResult.FAILED_RETRYABLE
99+
return SpanExportResult.FAILED_NOT_RETRYABLE
100+
return SpanExportResult.SUCCESS
101+
102+
def _translate_to_zipkin(self, spans: Sequence[Span]):
103+
104+
local_endpoint = {
105+
"serviceName": self.service_name,
106+
"port": self.port,
107+
}
108+
109+
if self.ipv4 is not None:
110+
local_endpoint["ipv4"] = self.ipv4
111+
112+
if self.ipv6 is not None:
113+
local_endpoint["ipv6"] = self.ipv6
114+
115+
zipkin_spans = []
116+
for span in spans:
117+
context = span.get_context()
118+
trace_id = context.trace_id
119+
span_id = context.span_id
120+
121+
# Timestamp in zipkin spans is int of microseconds.
122+
# see: https://zipkin.io/pages/instrumenting.html
123+
start_timestamp_mus = _nsec_to_usec_round(span.start_time)
124+
duration_mus = _nsec_to_usec_round(span.end_time - span.start_time)
125+
126+
zipkin_span = {
127+
"traceId": format(trace_id, "x"),
128+
"id": format(span_id, "x"),
129+
"name": span.name,
130+
"timestamp": start_timestamp_mus,
131+
"duration": duration_mus,
132+
"localEndpoint": local_endpoint,
133+
"kind": SPAN_KIND_MAP[span.kind],
134+
"tags": _extract_tags_from_span(span.attributes),
135+
"annotations": _extract_annotations_from_events(span.events),
136+
}
137+
138+
if context.trace_options.sampled:
139+
zipkin_span["debug"] = 1
140+
141+
if isinstance(span.parent, Span):
142+
zipkin_span["parentId"] = format(
143+
span.parent.get_context().span_id, "x"
144+
)
145+
elif isinstance(span.parent, SpanContext):
146+
zipkin_span["parentId"] = format(span.parent.span_id, "x")
147+
148+
zipkin_spans.append(zipkin_span)
149+
return zipkin_spans
150+
151+
def shutdown(self) -> None:
152+
pass
153+
154+
155+
def _extract_tags_from_span(attr):
156+
if not attr:
157+
return None
158+
tags = {}
159+
for attribute_key, attribute_value in attr.items():
160+
if isinstance(attribute_value, (int, bool, float)):
161+
value = str(attribute_value)
162+
elif isinstance(attribute_value, str):
163+
value = attribute_value[:128]
164+
else:
165+
logger.warning("Could not serialize tag %s", attribute_key)
166+
continue
167+
tags[attribute_key] = value
168+
return tags
169+
170+
171+
def _extract_annotations_from_events(events):
172+
return (
173+
[
174+
{"timestamp": _nsec_to_usec_round(e.timestamp), "value": e.name}
175+
for e in events
176+
]
177+
if events
178+
else None
179+
)
180+
181+
182+
def _nsec_to_usec_round(nsec):
183+
"""Round nanoseconds to microseconds"""
184+
return (nsec + 500) // 10 ** 3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
__version__ = "0.3.dev0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.

0 commit comments

Comments
 (0)