Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1174 zipkin exporter v1 thrift support #2

Open
wants to merge 27 commits into
base: 1173_zipkin_exporter_v1_json_support_fix
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4124acc
Thrift v1 api support!
robwknox Nov 13, 2020
30c1cc8
black, flake
robwknox Nov 13, 2020
ddddf60
Merge branch '1173_zipkin_exporter_v1_json_support_refactor' into 117…
robwknox Nov 13, 2020
4642c9a
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 14, 2020
06f335c
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 15, 2020
bc28f15
unit tests
robwknox Nov 15, 2020
bc9bd85
unit tests for httpsender
robwknox Nov 15, 2020
c4a291d
unit test logic for python <= 3.5 to handle unordered lists attrs
robwknox Nov 15, 2020
e1149b6
Reducing random id generated bits for span and trace ids to avoid tru…
robwknox Nov 16, 2020
818381e
thrift-specific id generator for overriding default to avoid truncation
robwknox Nov 16, 2020
a6144b6
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 16, 2020
e2b3837
changelog
robwknox Nov 16, 2020
88fd6f3
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 17, 2020
cb0762a
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 18, 2020
8a69a6d
test fix
robwknox Nov 18, 2020
449d6f5
stricter type handling for thrift encoder
robwknox Nov 18, 2020
174143c
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 18, 2020
9b3f7cc
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 18, 2020
1304703
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 19, 2020
963c5f3
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 19, 2020
da47c61
Merge branch '1173_zipkin_exporter_v1_json_support' into 1174_zipkin_…
robwknox Nov 19, 2020
da0af86
Merge branch '1173_zipkin_exporter_v1_json_support_fix' into 1174_zip…
robwknox Nov 23, 2020
a0c9ce5
Merge branch '1173_zipkin_exporter_v1_json_support_fix' into 1174_zip…
robwknox Dec 3, 2020
55a40a0
lint
robwknox Dec 3, 2020
3bfdbbe
Merge branch '1173_zipkin_exporter_v1_json_support_fix' into 1174_zip…
robwknox Dec 3, 2020
9ff2490
Merge branch '1173_zipkin_exporter_v1_json_support_fix' into 1174_zip…
robwknox Dec 3, 2020
34f3931
lint
robwknox Dec 3, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ exclude =
__pycache__
exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/
exporter/opentelemetry-exporter-jaeger/build/*
exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/gen/
exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v2/protobuf/gen/
docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/
docs/examples/opentelemetry-example-app/build/*
Expand Down
1 change: 1 addition & 0 deletions exporter/opentelemetry-exporter-zipkin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Released 2020-11-25

- Support for v2 api protobuf format ([#1318](https://github.com/open-telemetry/opentelemetry-python/pull/1318))
- Support for v1 api json format ([#1411](https://github.com/open-telemetry/opentelemetry-python/pull/1411))
- Support for v1 api thrift format (TBD)

## Version 0.14b0

Expand Down
1 change: 1 addition & 0 deletions exporter/opentelemetry-exporter-zipkin/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ install_requires =
requests ~= 2.7
opentelemetry-api == 0.17.dev0
opentelemetry-sdk == 0.17.dev0
thrift >= 0.13.0

[options.packages.find]
where = src
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
The **OpenTelemetry Zipkin Exporter** allows exporting of `OpenTelemetry`_
traces to `Zipkin`_. This exporter sends traces to the configured Zipkin
collector endpoint using HTTP and supports multiple encodings (v1 json,
v2 json, v2 protobuf).
v1 thrift, v2 json, v2 protobuf).

.. _Zipkin: https://zipkin.io/
.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/
Expand Down Expand Up @@ -90,6 +90,7 @@
from opentelemetry.configuration import Configuration
from opentelemetry.exporter.zipkin.encoder import Encoder, Encoding
from opentelemetry.exporter.zipkin.encoder.v1.json import JsonV1Encoder
from opentelemetry.exporter.zipkin.encoder.v1.thrift import ThriftEncoder
from opentelemetry.exporter.zipkin.encoder.v2.json import JsonV2Encoder
from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder
from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint
Expand Down Expand Up @@ -139,6 +140,8 @@ def __init__(

if encoding == Encoding.V1_JSON:
self.encoder = JsonV1Encoder(max_tag_value_length)
elif encoding == Encoding.V1_THRIFT:
self.encoder = ThriftEncoder(max_tag_value_length)
elif encoding == Encoding.V2_JSON:
self.encoder = JsonV2Encoder(max_tag_value_length)
elif encoding == Encoding.V2_PROTOBUF:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Encoding(Enum):
"""

V1_JSON = "v1_json"
V1_THRIFT = "v1_thrift"
V2_JSON = "v2_json"
V2_PROTOBUF = "v2_protobuf"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,10 @@

# pylint: disable=W0223
class V1Encoder(Encoder, abc.ABC):
def _extract_binary_annotations(
self, span: Span, encoded_local_endpoint: Dict
) -> List[Dict]:
def _extract_binary_annotations(self, span: Span) -> List[Dict]:
binary_annotations = []
for tag_key, tag_value in self._extract_tags_from_span(span).items():
if isinstance(tag_value, str) and self.max_tag_value_length > 0:
tag_value = tag_value[: self.max_tag_value_length]
binary_annotations.append(
{
"key": tag_key,
"value": tag_value,
"endpoint": encoded_local_endpoint,
}
)
binary_annotations.append({"key": tag_key, "value": tag_value})
return binary_annotations
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ def _encode_span(self, span: Span, encoded_local_endpoint: Dict) -> Dict:
annotation["endpoint"] = encoded_local_endpoint
encoded_span["annotations"] = encoded_annotations

binary_annotations = self._extract_binary_annotations(
span, encoded_local_endpoint
)
binary_annotations = self._extract_binary_annotations(span)
if binary_annotations:
for binary_annotation in binary_annotations:
binary_annotation["endpoint"] = encoded_local_endpoint
encoded_span["binaryAnnotations"] = binary_annotations

debug = self._encode_debug(context)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Zipkin Export Encoder for Thrift

API spec: https://github.com/openzipkin/zipkin-api/tree/master/thrift

Thrift documentation is severely lacking and largely outdated, so the cppkin
implementation proved invaluable as a reference and is the basis for the
majority of the encoding logic. Link:

https://github.com/Dudi119/cppKin/blob/master/src/ThriftEncoder.h

WARNING: Thrift only supports signed integers (max 64 bits). This results in
the Zipkin Thrift API having definitions of:

i64 id (span id)
i64 trace_id
i64 trace_id_high

Which in turn results in our encoding having truncation logic built-in to avoid
overflow when converting from unsigned -> signed ints, as well as to chunk the
trace_id into two components if necessary for transport. Refer to the
encode_trace_id() and encode_span_id() methods for details.

If you wish to avoid truncation, you can use ThriftRandomIdsGenerator class
provided in this module to override the default API ID generator in a given
TracerProvider().
"""

import ipaddress
import logging
import random
from typing import List, Optional, Sequence

from opentelemetry.exporter.zipkin.encoder.v1 import V1Encoder
from opentelemetry.exporter.zipkin.encoder.v1.thrift.gen.zipkinCore import (
ttypes,
)
from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint
from opentelemetry.trace import Span, SpanContext
from opentelemetry.trace.ids_generator import RandomIdsGenerator
from thrift.Thrift import TType
from thrift.transport.TTransport import TMemoryBuffer
from thrift.protocol import TBinaryProtocol

logger = logging.getLogger(__name__)


class ThriftEncoder(V1Encoder):
"""Zipkin Export Encoder for Thrift

API spec: https://github.com/openzipkin/zipkin-api/tree/master/thrift
"""

@staticmethod
def content_type() -> str:
return "application/x-thrift"

def serialize(
self, spans: Sequence[Span], local_endpoint: NodeEndpoint
) -> str:
encoded_local_endpoint = self._encode_local_endpoint(local_endpoint)
buffer = TMemoryBuffer()
protocol = TBinaryProtocol.TBinaryProtocolFactory().getProtocol(buffer)
protocol.writeListBegin(TType.STRUCT, len(spans))
for span in spans:
self._encode_span(span, encoded_local_endpoint).write(protocol)
protocol.writeListEnd()
return buffer.getvalue()

def _encode_span(
self, span: Span, encoded_local_endpoint: ttypes.Endpoint
) -> ttypes.Span:
context = span.get_span_context()
thrift_trace_id, thrift_trace_id_high = self._encode_trace_id(
context.trace_id
)
thrift_span = ttypes.Span(
trace_id=thrift_trace_id,
trace_id_high=thrift_trace_id_high,
id=self._encode_span_id(context.span_id),
name=span.name,
timestamp=self._nsec_to_usec_round(span.start_time),
duration=self._nsec_to_usec_round(span.end_time - span.start_time),
)

annotations = self._encode_annotations(span, encoded_local_endpoint)
if annotations:
thrift_span.annotations = annotations

binary_annotations = self._encode_binary_annotations(
span, encoded_local_endpoint
)
if binary_annotations:
thrift_span.binary_annotations = binary_annotations

debug = self._encode_debug(context)
if debug:
thrift_span.debug = debug

if isinstance(span.parent, Span):
thrift_span.parent_id = self._encode_span_id(
span.parent.get_span_context().span_id
)
elif isinstance(span.parent, SpanContext):
thrift_span.parent_id = self._encode_span_id(span.parent.span_id)

return thrift_span

def _encode_annotations(
self, span: Span, encoded_local_endpoint: ttypes.Endpoint
) -> Optional[List[ttypes.Annotation]]:
annotations = self._extract_annotations_from_events(span.events)
if annotations is None:
encoded_annotations = None
else:
encoded_annotations = []
for annotation in self._extract_annotations_from_events(
span.events
):
encoded_annotations.append(
ttypes.Annotation(
timestamp=annotation["timestamp"],
value=annotation["value"],
host=encoded_local_endpoint,
)
)
return encoded_annotations

def _encode_binary_annotations(
self, span: Span, encoded_local_endpoint: ttypes.Endpoint
) -> List[ttypes.BinaryAnnotation]:
thrift_binary_annotations = []
for binary_annotation in self._extract_binary_annotations(span):
thrift_binary_annotations.append(
ttypes.BinaryAnnotation(
key=binary_annotation["key"],
value=binary_annotation["value"].encode("utf-8"),
annotation_type=ttypes.AnnotationType.STRING,
host=encoded_local_endpoint,
)
)
return thrift_binary_annotations

@staticmethod
def _encode_local_endpoint(
local_endpoint: NodeEndpoint,
) -> ttypes.Endpoint:
endpoint = ttypes.Endpoint(service_name=local_endpoint.service_name,)
if local_endpoint.ipv4 is not None:
endpoint.ipv4 = ipaddress.ip_address(local_endpoint.ipv4).packed
if local_endpoint.ipv6 is not None:
endpoint.ipv6 = ipaddress.ip_address(local_endpoint.ipv6).packed
if local_endpoint.port is not None:
endpoint.port = local_endpoint.port
return endpoint

@staticmethod
def _encode_span_id(span_id: int) -> int:
"""Since Thrift only supports signed integers (max size 64 bits) the
Zipkin Thrift API defines the span id as an i64 field.

If a provided span id is > 63 bits it will be truncated to 63 bits
to fit into the API field and a warning log will be emitted. We use
63 bits instead of 64 bits because we have to leave 1 bit in the API
field for the positive sign representation.
"""
bits = format(span_id, "b")
if len(bits) < 64:
encoded_span_id = span_id
else:
encoded_span_id = int(bits[-63:], 2)
logger.warning(
"Span id truncated to fit into Thrift "
"protocol signed integer format: [%02x => %02x]",
span_id,
encoded_span_id,
)

return encoded_span_id

@staticmethod
def _encode_trace_id(trace_id: int) -> (int, Optional[int]):
"""Since Thrift only supports signed integers (max size 64 bits) the
Zipkin Thrift API defines two fields to hold a trace id:
- i64 trace_id
- i64 trace_id_high (only used if provided trace id is > 63 bits)

If a provided trace id is > 126 bits it will be truncated to 126 bits
to fit into the API fields and a warning log will be emitted. We use
126 bits instead of 128 bits because we have to leave 1 bit in each
of the two API fields for the positive sign representation.

:param trace_id:
:return: tuple of (encoded_trace_id, encoded_trace_id_high)
"""
bits = format(trace_id, "b")
bits_length = len(bits)
encoded_trace_id = int(bits[-63:], 2)

if bits_length > 63:
encoded_trace_id_high = int(bits[-126:-63], 2)
if bits_length > 126:
logger.warning(
"Trace id truncated to fit into Thrift "
"protocol signed integer format: [%02x => %02x%02x]",
trace_id,
encoded_trace_id_high,
encoded_trace_id,
)
else:
encoded_trace_id_high = None

return encoded_trace_id, encoded_trace_id_high


class ThriftRandomIdsGenerator(RandomIdsGenerator):
"""
ID generator for Thrift encoder to avoid truncation of trace and span ids.

.. code:: python

from opentelemetry.exporter.zipkin.encoder.v1.thrift import(
ThriftRandomIdsGenerator
)

trace.set_tracer_provider(TracerProvider(
ids_generator=ThriftRandomIdsGenerator()
))
tracer = trace.get_tracer(__name__)

# ... follow normal pattern of tracer usage ...

Thrift only supports signed integers (max 64 bits). This results in
the Zipkin Thrift API having definitions of:

i64 id (span id)
i64 trace_id
i64 trace_id_high

Which in turn results in our encoding having truncation logic built-in to avoid
overflow when converting from unsigned -> signed ints, as well as to chunk the
trace_id into two components if necessary for transport. Refer to the
encode_trace_id() and encode_span_id() methods for details.
"""

def generate_span_id(self) -> int:
return random.getrandbits(63)

def generate_trace_id(self) -> int:
return random.getrandbits(126)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = ['ttypes', 'constants']

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading