From 4124acc723c6181d033c55d12df05b851ef0edf1 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Fri, 13 Nov 2020 15:18:32 -0800 Subject: [PATCH 01/12] Thrift v1 api support! --- .../opentelemetry/exporter/zipkin/__init__.py | 17 +- .../exporter/zipkin/encoder/__init__.py | 3 +- .../zipkin/encoder/thrift/__init__.py | 239 +++++++ .../encoder/thrift/gen/zipkinCore/__init__.py | 1 + .../thrift/gen/zipkinCore/constants.py | 35 + .../encoder/thrift/gen/zipkinCore/ttypes.py | 632 ++++++++++++++++++ .../thrift/gen/zipkinDependencies/__init__.py | 1 + .../gen/zipkinDependencies/constants.py | 10 + .../thrift/gen/zipkinDependencies/ttypes.py | 211 ++++++ .../exporter/zipkin/sender/http.py | 9 +- .../opentelemetry-exporter-zipkin/test.py | 0 11 files changed, 1150 insertions(+), 8 deletions(-) create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/constants.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/ttypes.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/__init__.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/constants.py create mode 100644 exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/ttypes.py create mode 100644 exporter/opentelemetry-exporter-zipkin/test.py diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index dc33ebf9d3..9f90cc3f4c 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -18,9 +18,9 @@ Usage ----- -The **OpenTelemetry Zipkin Exporter** allows to export `OpenTelemetry`_ traces to `Zipkin`_. -This exporter always send traces to the configured Zipkin collector using HTTP. - +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/ @@ -110,6 +110,7 @@ JsonV2Encoder, ) from opentelemetry.exporter.zipkin.encoder.protobuf import ProtobufEncoder +from opentelemetry.exporter.zipkin.encoder.thrift import ThriftEncoder from opentelemetry.exporter.zipkin.endpoint import Endpoint from opentelemetry.exporter.zipkin.sender.http import Sender, HttpSender from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -178,10 +179,14 @@ def __init__( self.encoder = JsonV2Encoder(local_endpoint) elif encoding == Encoding.PROTOBUF: self.encoder = ProtobufEncoder(local_endpoint) + elif encoding == Encoding.THRIFT: + self.encoder = ThriftEncoder(local_endpoint) - endpoint = Configuration().EXPORTER_ZIPKIN_ENDPOINT or endpoint - - self.sender = sender or HttpSender(endpoint, encoding) + if sender is not None: + self.sender = sender + else: + endpoint = Configuration().EXPORTER_ZIPKIN_ENDPOINT or endpoint + self.sender = HttpSender(endpoint, encoding) def export(self, spans: Sequence[Span]) -> SpanExportResult: return self.sender.send(self.encoder.encode(spans)) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py index 53a23153bf..0733a94426 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -18,9 +18,9 @@ """ import abc -from enum import Enum import json import logging +from enum import Enum from typing import Sequence from opentelemetry.exporter.zipkin.endpoint import Endpoint @@ -41,6 +41,7 @@ class Encoding(Enum): JSON_V1 = "json_v1" JSON_V2 = "json_v2" PROTOBUF = "protobuf" + THRIFT = "thrift" class Encoder(abc.ABC): diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/__init__.py new file mode 100644 index 0000000000..0ada641ad2 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/__init__.py @@ -0,0 +1,239 @@ +# 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. +""" + +import logging +from typing import Sequence + +from opentelemetry.exporter.zipkin.encoder import Encoder +from opentelemetry.exporter.zipkin.encoder.thrift.gen.zipkinCore import ttypes +from opentelemetry.trace import Span, SpanContext +from thrift.Thrift import TType +from thrift.transport.TTransport import TMemoryBuffer +from thrift.protocol import TBinaryProtocol + +logger = logging.getLogger(__name__) + + +class ThriftEncoder(Encoder): + """Zipkin Export Encoder for Thrift + + API spec: https://github.com/openzipkin/zipkin-api/tree/master/thrift + """ + + def _encode(self, spans: Sequence[Span]): + encoded_local_endpoint = self._encode_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_local_endpoint(self): + endpoint = ttypes.Endpoint( + service_name=self.local_endpoint.service_name, + ipv4=self.local_endpoint.ipv4, + port=self.local_endpoint.port + ) + + if self.local_endpoint.ipv6 is not None: + endpoint.ipv6 = bytes(self.local_endpoint.ipv6) + + return endpoint + + def _encode_span(self, span: Span, encoded_local_endpoint): + 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), + binary_annotations=self._encode_binary_annotations( + span, encoded_local_endpoint + ), + ) + + if context.trace_flags.sampled: + thrift_span.debug = True + + 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): + thrift_annotations = [] + + for annotation in self._extract_annotations_from_events(span.events): + thrift_annotations.append( + ttypes.Annotation( + timestamp=annotation["timestamp"], + value=annotation["value"], + host=encoded_local_endpoint, + ) + ) + + return thrift_annotations + + def _encode_binary_annotations(self, span: Span, encoded_local_endpoint): + 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 + + def _extract_binary_annotations(self, span: Span): + 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}) + + if span.instrumentation_info is not None: + binary_annotations.extend( + [ + { + "key": "otel.instrumentation_library.name", + "value": span.instrumentation_info.name + }, + { + "key": "otel.instrumentation_library.version", + "value": span.instrumentation_info.version + }, + ] + ) + + if span.status is not None: + binary_annotations.append( + { + "key": "otel.status_code", + "value": str(span.status.status_code.value) + } + ) + + if span.status.description is not None: + binary_annotations.append( + { + "key": "otel.status_description", + "value": span.status.description, + } + ) + + return binary_annotations + + @staticmethod + def encode_span_id(span_id: 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): + """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 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/__init__.py new file mode 100644 index 0000000000..adefd8e51f --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/__init__.py @@ -0,0 +1 @@ +__all__ = ['ttypes', 'constants'] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/constants.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/constants.py new file mode 100644 index 0000000000..568f93bd2a --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/constants.py @@ -0,0 +1,35 @@ +# +# Autogenerated by Thrift Compiler (0.13.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.TRecursive import fix_spec + +CLIENT_SEND = "cs" +CLIENT_RECV = "cr" +SERVER_SEND = "ss" +SERVER_RECV = "sr" +MESSAGE_SEND = "ms" +MESSAGE_RECV = "mr" +WIRE_SEND = "ws" +WIRE_RECV = "wr" +CLIENT_SEND_FRAGMENT = "csf" +CLIENT_RECV_FRAGMENT = "crf" +SERVER_SEND_FRAGMENT = "ssf" +SERVER_RECV_FRAGMENT = "srf" +HTTP_HOST = "http.host" +HTTP_METHOD = "http.method" +HTTP_PATH = "http.path" +HTTP_ROUTE = "http.route" +HTTP_URL = "http.url" +HTTP_STATUS_CODE = "http.status_code" +HTTP_REQUEST_SIZE = "http.request.size" +HTTP_RESPONSE_SIZE = "http.response.size" +LOCAL_COMPONENT = "lc" +ERROR = "error" +CLIENT_ADDR = "ca" +SERVER_ADDR = "sa" +MESSAGE_ADDR = "ma" diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/ttypes.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/ttypes.py new file mode 100644 index 0000000000..4f3ba32766 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinCore/ttypes.py @@ -0,0 +1,632 @@ +# +# Autogenerated by Thrift Compiler (0.13.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +import sys + +from thrift.TRecursive import fix_spec +from thrift.Thrift import TType +from thrift.transport import TTransport + +all_structs = [] + + +class AnnotationType(object): + """ + A subset of thrift base types, except BYTES. + + """ + BOOL = 0 + BYTES = 1 + I16 = 2 + I32 = 3 + I64 = 4 + DOUBLE = 5 + STRING = 6 + + _VALUES_TO_NAMES = { + 0: "BOOL", + 1: "BYTES", + 2: "I16", + 3: "I32", + 4: "I64", + 5: "DOUBLE", + 6: "STRING", + } + + _NAMES_TO_VALUES = { + "BOOL": 0, + "BYTES": 1, + "I16": 2, + "I32": 3, + "I64": 4, + "DOUBLE": 5, + "STRING": 6, + } + + +class Endpoint(object): + """ + Indicates the network context of a service recording an annotation with two + exceptions. + + When a BinaryAnnotation, and key is CLIENT_ADDR or SERVER_ADDR, + the endpoint indicates the source or destination of an RPC. This exception + allows zipkin to display network context of uninstrumented services, or + clients such as web browsers. + + Attributes: + - ipv4: IPv4 host address packed into 4 bytes. + + Ex for the ip 1.2.3.4, it would be (1 << 24) | (2 << 16) | (3 << 8) | 4 + - port: IPv4 port or 0, if unknown. + + Note: this is to be treated as an unsigned integer, so watch for negatives. + - service_name: Classifier of a source or destination in lowercase, such as "zipkin-web". + + This is the primary parameter for trace lookup, so should be intuitive as + possible, for example, matching names in service discovery. + + Conventionally, when the service name isn't known, service_name = "unknown". + However, it is also permissible to set service_name = "" (empty string). + The difference in the latter usage is that the span will not be queryable + by service name unless more information is added to the span with non-empty + service name, e.g. an additional annotation from the server. + + Particularly clients may not have a reliable service name at ingest. One + approach is to set service_name to "" at ingest, and later assign a + better label based on binary annotations, such as user agent. + - ipv6: IPv6 host address packed into 16 bytes. Ex Inet6Address.getBytes() + + """ + + + def __init__(self, ipv4=None, port=None, service_name=None, ipv6=None,): + self.ipv4 = ipv4 + self.port = port + self.service_name = service_name + self.ipv6 = ipv6 + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, [self.__class__, self.thrift_spec]) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I32: + self.ipv4 = iprot.readI32() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.I16: + self.port = iprot.readI16() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRING: + self.service_name = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.STRING: + self.ipv6 = iprot.readBinary() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, [self.__class__, self.thrift_spec])) + return + oprot.writeStructBegin('Endpoint') + if self.ipv4 is not None: + oprot.writeFieldBegin('ipv4', TType.I32, 1) + oprot.writeI32(self.ipv4) + oprot.writeFieldEnd() + if self.port is not None: + oprot.writeFieldBegin('port', TType.I16, 2) + oprot.writeI16(self.port) + oprot.writeFieldEnd() + if self.service_name is not None: + oprot.writeFieldBegin('service_name', TType.STRING, 3) + oprot.writeString(self.service_name.encode('utf-8') if sys.version_info[0] == 2 else self.service_name) + oprot.writeFieldEnd() + if self.ipv6 is not None: + oprot.writeFieldBegin('ipv6', TType.STRING, 4) + oprot.writeBinary(self.ipv6) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Annotation(object): + """ + Associates an event that explains latency with a timestamp. + + Unlike log statements, annotations are often codes: for example "sr". + + Attributes: + - timestamp: Microseconds from epoch. + + This value should use the most precise value possible. For example, + gettimeofday or multiplying currentTimeMillis by 1000. + - value: Usually a short tag indicating an event, like "sr" or "finagle.retry". + - host: The host that recorded the value, primarily for query by service name. + + """ + + + def __init__(self, timestamp=None, value=None, host=None,): + self.timestamp = timestamp + self.value = value + self.host = host + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, [self.__class__, self.thrift_spec]) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I64: + self.timestamp = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.STRING: + self.value = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRUCT: + self.host = Endpoint() + self.host.read(iprot) + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, [self.__class__, self.thrift_spec])) + return + oprot.writeStructBegin('Annotation') + if self.timestamp is not None: + oprot.writeFieldBegin('timestamp', TType.I64, 1) + oprot.writeI64(self.timestamp) + oprot.writeFieldEnd() + if self.value is not None: + oprot.writeFieldBegin('value', TType.STRING, 2) + oprot.writeString(self.value.encode('utf-8') if sys.version_info[0] == 2 else self.value) + oprot.writeFieldEnd() + if self.host is not None: + oprot.writeFieldBegin('host', TType.STRUCT, 3) + self.host.write(oprot) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class BinaryAnnotation(object): + """ + Binary annotations are tags applied to a Span to give it context. For + example, a binary annotation of HTTP_PATH ("http.path") could the path + to a resource in a RPC call. + + Binary annotations of type STRING are always queryable, though more a + historical implementation detail than a structural concern. + + Binary annotations can repeat, and vary on the host. Similar to Annotation, + the host indicates who logged the event. This allows you to tell the + difference between the client and server side of the same key. For example, + the key "http.path" might be different on the client and server side due to + rewriting, like "/api/v1/myresource" vs "/myresource. Via the host field, + you can see the different points of view, which often help in debugging. + + Attributes: + - key: Name used to lookup spans, such as "http.path" or "finagle.version". + - value: Serialized thrift bytes, in TBinaryProtocol format. + + For legacy reasons, byte order is big-endian. See THRIFT-3217. + - annotation_type: The thrift type of value, most often STRING. + + annotation_type shouldn't vary for the same key. + - host: The host that recorded value, allowing query by service name or address. + + There are two exceptions: when key is "ca" or "sa", this is the source or + destination of an RPC. This exception allows zipkin to display network + context of uninstrumented services, such as browsers or databases. + + """ + + + def __init__(self, key=None, value=None, annotation_type=None, host=None,): + self.key = key + self.value = value + self.annotation_type = annotation_type + self.host = host + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, [self.__class__, self.thrift_spec]) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.STRING: + self.key = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.STRING: + self.value = iprot.readBinary() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.I32: + self.annotation_type = iprot.readI32() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.STRUCT: + self.host = Endpoint() + self.host.read(iprot) + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, [self.__class__, self.thrift_spec])) + return + oprot.writeStructBegin('BinaryAnnotation') + if self.key is not None: + oprot.writeFieldBegin('key', TType.STRING, 1) + oprot.writeString(self.key.encode('utf-8') if sys.version_info[0] == 2 else self.key) + oprot.writeFieldEnd() + if self.value is not None: + oprot.writeFieldBegin('value', TType.STRING, 2) + oprot.writeBinary(self.value) + oprot.writeFieldEnd() + if self.annotation_type is not None: + oprot.writeFieldBegin('annotation_type', TType.I32, 3) + oprot.writeI32(self.annotation_type) + oprot.writeFieldEnd() + if self.host is not None: + oprot.writeFieldBegin('host', TType.STRUCT, 4) + self.host.write(oprot) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Span(object): + """ + A trace is a series of spans (often RPC calls) which form a latency tree. + + Spans are usually created by instrumentation in RPC clients or servers, but + can also represent in-process activity. Annotations in spans are similar to + log statements, and are sometimes created directly by application developers + to indicate events of interest, such as a cache miss. + + The root span is where parent_id = Nil; it usually has the longest duration + in the trace. + + Span identifiers are packed into i64s, but should be treated opaquely. + String encoding is fixed-width lower-hex, to avoid signed interpretation. + + Attributes: + - trace_id: Unique 8-byte identifier for a trace, set on all spans within it. + - name: Span name in lowercase, rpc method for example. Conventionally, when the + span name isn't known, name = "unknown". + - id: Unique 8-byte identifier of this span within a trace. A span is uniquely + identified in storage by (trace_id, id). + - parent_id: The parent's Span.id; absent if this the root span in a trace. + - annotations: Associates events that explain latency with a timestamp. Unlike log + statements, annotations are often codes: for example SERVER_RECV("sr"). + Annotations are sorted ascending by timestamp. + - binary_annotations: Tags a span with context, usually to support query or aggregation. For + example, a binary annotation key could be "http.path". + - debug: True is a request to store this span even if it overrides sampling policy. + - timestamp: Epoch microseconds of the start of this span, absent if this an incomplete + span. + + This value should be set directly by instrumentation, using the most + precise value possible. For example, gettimeofday or syncing nanoTime + against a tick of currentTimeMillis. + + For compatibility with instrumentation that precede this field, collectors + or span stores can derive this via Annotation.timestamp. + For example, SERVER_RECV.timestamp or CLIENT_SEND.timestamp. + + Timestamp is nullable for input only. Spans without a timestamp cannot be + presented in a timeline: Span stores should not output spans missing a + timestamp. + + There are two known edge-cases where this could be absent: both cases + exist when a collector receives a span in parts and a binary annotation + precedes a timestamp. This is possible when.. + - The span is in-flight (ex not yet received a timestamp) + - The span's start event was lost + - duration: Measurement in microseconds of the critical path, if known. Durations of + less than one microsecond must be rounded up to 1 microsecond. + + This value should be set directly, as opposed to implicitly via annotation + timestamps. Doing so encourages precision decoupled from problems of + clocks, such as skew or NTP updates causing time to move backwards. + + For compatibility with instrumentation that precede this field, collectors + or span stores can derive this by subtracting Annotation.timestamp. + For example, SERVER_SEND.timestamp - SERVER_RECV.timestamp. + + If this field is persisted as unset, zipkin will continue to work, except + duration query support will be implementation-specific. Similarly, setting + this field non-atomically is implementation-specific. + + This field is i64 vs i32 to support spans longer than 35 minutes. + - trace_id_high: Optional unique 8-byte additional identifier for a trace. If non zero, this + means the trace uses 128 bit traceIds instead of 64 bit. + + """ + + + def __init__(self, trace_id=None, name=None, id=None, parent_id=None, annotations=None, binary_annotations=None, debug=False, timestamp=None, duration=None, trace_id_high=None,): + self.trace_id = trace_id + self.name = name + self.id = id + self.parent_id = parent_id + self.annotations = annotations + self.binary_annotations = binary_annotations + self.debug = debug + self.timestamp = timestamp + self.duration = duration + self.trace_id_high = trace_id_high + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, [self.__class__, self.thrift_spec]) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I64: + self.trace_id = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRING: + self.name = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.I64: + self.id = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 5: + if ftype == TType.I64: + self.parent_id = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 6: + if ftype == TType.LIST: + self.annotations = [] + (_etype3, _size0) = iprot.readListBegin() + for _i4 in range(_size0): + _elem5 = Annotation() + _elem5.read(iprot) + self.annotations.append(_elem5) + iprot.readListEnd() + else: + iprot.skip(ftype) + elif fid == 8: + if ftype == TType.LIST: + self.binary_annotations = [] + (_etype9, _size6) = iprot.readListBegin() + for _i10 in range(_size6): + _elem11 = BinaryAnnotation() + _elem11.read(iprot) + self.binary_annotations.append(_elem11) + iprot.readListEnd() + else: + iprot.skip(ftype) + elif fid == 9: + if ftype == TType.BOOL: + self.debug = iprot.readBool() + else: + iprot.skip(ftype) + elif fid == 10: + if ftype == TType.I64: + self.timestamp = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 11: + if ftype == TType.I64: + self.duration = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 12: + if ftype == TType.I64: + self.trace_id_high = iprot.readI64() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, [self.__class__, self.thrift_spec])) + return + oprot.writeStructBegin('Span') + if self.trace_id is not None: + oprot.writeFieldBegin('trace_id', TType.I64, 1) + oprot.writeI64(self.trace_id) + oprot.writeFieldEnd() + if self.name is not None: + oprot.writeFieldBegin('name', TType.STRING, 3) + oprot.writeString(self.name.encode('utf-8') if sys.version_info[0] == 2 else self.name) + oprot.writeFieldEnd() + if self.id is not None: + oprot.writeFieldBegin('id', TType.I64, 4) + oprot.writeI64(self.id) + oprot.writeFieldEnd() + if self.parent_id is not None: + oprot.writeFieldBegin('parent_id', TType.I64, 5) + oprot.writeI64(self.parent_id) + oprot.writeFieldEnd() + if self.annotations is not None: + oprot.writeFieldBegin('annotations', TType.LIST, 6) + oprot.writeListBegin(TType.STRUCT, len(self.annotations)) + for iter12 in self.annotations: + iter12.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + if self.binary_annotations is not None: + oprot.writeFieldBegin('binary_annotations', TType.LIST, 8) + oprot.writeListBegin(TType.STRUCT, len(self.binary_annotations)) + for iter13 in self.binary_annotations: + iter13.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + if self.debug is not None: + oprot.writeFieldBegin('debug', TType.BOOL, 9) + oprot.writeBool(self.debug) + oprot.writeFieldEnd() + if self.timestamp is not None: + oprot.writeFieldBegin('timestamp', TType.I64, 10) + oprot.writeI64(self.timestamp) + oprot.writeFieldEnd() + if self.duration is not None: + oprot.writeFieldBegin('duration', TType.I64, 11) + oprot.writeI64(self.duration) + oprot.writeFieldEnd() + if self.trace_id_high is not None: + oprot.writeFieldBegin('trace_id_high', TType.I64, 12) + oprot.writeI64(self.trace_id_high) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) +all_structs.append(Endpoint) +Endpoint.thrift_spec = ( + None, # 0 + (1, TType.I32, 'ipv4', None, None, ), # 1 + (2, TType.I16, 'port', None, None, ), # 2 + (3, TType.STRING, 'service_name', 'UTF8', None, ), # 3 + (4, TType.STRING, 'ipv6', 'BINARY', None, ), # 4 +) +all_structs.append(Annotation) +Annotation.thrift_spec = ( + None, # 0 + (1, TType.I64, 'timestamp', None, None, ), # 1 + (2, TType.STRING, 'value', 'UTF8', None, ), # 2 + (3, TType.STRUCT, 'host', [Endpoint, None], None, ), # 3 +) +all_structs.append(BinaryAnnotation) +BinaryAnnotation.thrift_spec = ( + None, # 0 + (1, TType.STRING, 'key', 'UTF8', None, ), # 1 + (2, TType.STRING, 'value', 'BINARY', None, ), # 2 + (3, TType.I32, 'annotation_type', None, None, ), # 3 + (4, TType.STRUCT, 'host', [Endpoint, None], None, ), # 4 +) +all_structs.append(Span) +Span.thrift_spec = ( + None, # 0 + (1, TType.I64, 'trace_id', None, None, ), # 1 + None, # 2 + (3, TType.STRING, 'name', 'UTF8', None, ), # 3 + (4, TType.I64, 'id', None, None, ), # 4 + (5, TType.I64, 'parent_id', None, None, ), # 5 + (6, TType.LIST, 'annotations', (TType.STRUCT, [Annotation, None], False), None, ), # 6 + None, # 7 + (8, TType.LIST, 'binary_annotations', (TType.STRUCT, [BinaryAnnotation, None], False), None, ), # 8 + (9, TType.BOOL, 'debug', None, False, ), # 9 + (10, TType.I64, 'timestamp', None, None, ), # 10 + (11, TType.I64, 'duration', None, None, ), # 11 + (12, TType.I64, 'trace_id_high', None, None, ), # 12 +) +fix_spec(all_structs) +del all_structs diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/__init__.py new file mode 100644 index 0000000000..adefd8e51f --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/__init__.py @@ -0,0 +1 @@ +__all__ = ['ttypes', 'constants'] diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/constants.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/constants.py new file mode 100644 index 0000000000..b3b8f21740 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/constants.py @@ -0,0 +1,10 @@ +# +# Autogenerated by Thrift Compiler (0.13.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +from thrift.protocol.TProtocol import TProtocolException + diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/ttypes.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/ttypes.py new file mode 100644 index 0000000000..09af881330 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/zipkinDependencies/ttypes.py @@ -0,0 +1,211 @@ +# +# Autogenerated by Thrift Compiler (0.13.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# options string: py +# + +import sys + +from thrift.TRecursive import fix_spec +from thrift.Thrift import TType +from thrift.transport import TTransport + +all_structs = [] + + +class DependencyLink(object): + """ + Attributes: + - parent: parent service name (caller) + - child: child service name (callee) + - callCount: total traced calls made from parent to child + - errorCount: how many calls are known to be errors + + """ + + + def __init__(self, parent=None, child=None, callCount=None, errorCount=None,): + self.parent = parent + self.child = child + self.callCount = callCount + self.errorCount = errorCount + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, [self.__class__, self.thrift_spec]) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.STRING: + self.parent = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.STRING: + self.child = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.I64: + self.callCount = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 5: + if ftype == TType.I64: + self.errorCount = iprot.readI64() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, [self.__class__, self.thrift_spec])) + return + oprot.writeStructBegin('DependencyLink') + if self.parent is not None: + oprot.writeFieldBegin('parent', TType.STRING, 1) + oprot.writeString(self.parent.encode('utf-8') if sys.version_info[0] == 2 else self.parent) + oprot.writeFieldEnd() + if self.child is not None: + oprot.writeFieldBegin('child', TType.STRING, 2) + oprot.writeString(self.child.encode('utf-8') if sys.version_info[0] == 2 else self.child) + oprot.writeFieldEnd() + if self.callCount is not None: + oprot.writeFieldBegin('callCount', TType.I64, 4) + oprot.writeI64(self.callCount) + oprot.writeFieldEnd() + if self.errorCount is not None: + oprot.writeFieldBegin('errorCount', TType.I64, 5) + oprot.writeI64(self.errorCount) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + + +class Dependencies(object): + """ + Attributes: + - start_ts: milliseconds from epoch + - end_ts: milliseconds from epoch + - links + + """ + + + def __init__(self, start_ts=None, end_ts=None, links=None,): + self.start_ts = start_ts + self.end_ts = end_ts + self.links = links + + def read(self, iprot): + if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: + iprot._fast_decode(self, iprot, [self.__class__, self.thrift_spec]) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I64: + self.start_ts = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.I64: + self.end_ts = iprot.readI64() + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.LIST: + self.links = [] + (_etype3, _size0) = iprot.readListBegin() + for _i4 in range(_size0): + _elem5 = DependencyLink() + _elem5.read(iprot) + self.links.append(_elem5) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot._fast_encode is not None and self.thrift_spec is not None: + oprot.trans.write(oprot._fast_encode(self, [self.__class__, self.thrift_spec])) + return + oprot.writeStructBegin('Dependencies') + if self.start_ts is not None: + oprot.writeFieldBegin('start_ts', TType.I64, 1) + oprot.writeI64(self.start_ts) + oprot.writeFieldEnd() + if self.end_ts is not None: + oprot.writeFieldBegin('end_ts', TType.I64, 2) + oprot.writeI64(self.end_ts) + oprot.writeFieldEnd() + if self.links is not None: + oprot.writeFieldBegin('links', TType.LIST, 3) + oprot.writeListBegin(TType.STRUCT, len(self.links)) + for iter6 in self.links: + iter6.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.items()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) +all_structs.append(DependencyLink) +DependencyLink.thrift_spec = ( + None, # 0 + (1, TType.STRING, 'parent', 'UTF8', None, ), # 1 + (2, TType.STRING, 'child', 'UTF8', None, ), # 2 + None, # 3 + (4, TType.I64, 'callCount', None, None, ), # 4 + (5, TType.I64, 'errorCount', None, None, ), # 5 +) +all_structs.append(Dependencies) +Dependencies.thrift_spec = ( + None, # 0 + (1, TType.I64, 'start_ts', None, None, ), # 1 + (2, TType.I64, 'end_ts', None, None, ), # 2 + (3, TType.LIST, 'links', (TType.STRUCT, [DependencyLink, None], False), None, ), # 3 +) +fix_spec(all_structs) +del all_structs diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/sender/http.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/sender/http.py index cbaa578166..bafca21585 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/sender/http.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/sender/http.py @@ -57,11 +57,18 @@ def send(self, encoded_spans: Sequence[Span]) -> SpanExportResult: @staticmethod def supported_encodings() -> List[Encoding]: - return [Encoding.JSON_V1, Encoding.JSON_V2, Encoding.PROTOBUF] + return [ + Encoding.JSON_V1, + Encoding.JSON_V2, + Encoding.PROTOBUF, + Encoding.THRIFT + ] def content_type(self): if self.encoding == Encoding.PROTOBUF: content_type = "application/x-protobuf" + elif self.encoding == Encoding.THRIFT: + content_type = "application/x-thrift" else: content_type = "application/json" return content_type diff --git a/exporter/opentelemetry-exporter-zipkin/test.py b/exporter/opentelemetry-exporter-zipkin/test.py new file mode 100644 index 0000000000..e69de29bb2 From 30c1cc8d47422e72b5462eb88d7eb7bf23b65076 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Fri, 13 Nov 2020 15:36:13 -0800 Subject: [PATCH 02/12] black, flake --- .flake8 | 1 + .../opentelemetry-exporter-zipkin/setup.cfg | 1 + .../exporter/zipkin/encoder/__init__.py | 4 +--- .../exporter/zipkin/encoder/json.py | 6 +++++- .../zipkin/encoder/thrift/__init__.py | 20 +++++++++---------- .../exporter/zipkin/sender/http.py | 6 ++---- pyproject.toml | 3 ++- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/.flake8 b/.flake8 index f5a0d24ba8..a9475bab93 100644 --- a/.flake8 +++ b/.flake8 @@ -19,6 +19,7 @@ exclude = exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/ exporter/opentelemetry-exporter-jaeger/build/* exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/protobuf/gen/ + exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen/ docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* opentelemetry-proto/build/* diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index d88717e13b..0c6a0d6d06 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,6 +41,7 @@ packages=find_namespace: install_requires = protobuf >= 3.12 requests ~= 2.7 + thrift >= 0.13.0 opentelemetry-api == 0.16.dev0 opentelemetry-sdk == 0.16.dev0 diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py index 0733a94426..a2d239b84a 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -52,9 +52,7 @@ class Encoder(abc.ABC): """ def __init__( - self, - local_endpoint: Endpoint, - max_tag_value_length: int = None, + self, local_endpoint: Endpoint, max_tag_value_length: int = None, ): self.local_endpoint = local_endpoint self.max_tag_value_length = ( diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/json.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/json.py index 82bcb789c8..e287be6b67 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/json.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/json.py @@ -182,7 +182,11 @@ def _extract_binary_annotations(self, span: Span, encoded_local_endpoint): 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} + { + "key": tag_key, + "value": tag_value, + "endpoint": encoded_local_endpoint, + } ) if span.instrumentation_info is not None: diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/__init__.py index 0ada641ad2..d3378ad866 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/__init__.py @@ -57,9 +57,7 @@ class ThriftEncoder(Encoder): def _encode(self, spans: Sequence[Span]): encoded_local_endpoint = self._encode_local_endpoint() buffer = TMemoryBuffer() - protocol = TBinaryProtocol.TBinaryProtocolFactory().getProtocol( - buffer - ) + protocol = TBinaryProtocol.TBinaryProtocolFactory().getProtocol(buffer) protocol.writeListBegin(TType.STRUCT, len(spans)) for span in spans: @@ -73,7 +71,7 @@ def _encode_local_endpoint(self): endpoint = ttypes.Endpoint( service_name=self.local_endpoint.service_name, ipv4=self.local_endpoint.ipv4, - port=self.local_endpoint.port + port=self.local_endpoint.port, ) if self.local_endpoint.ipv6 is not None: @@ -132,7 +130,7 @@ def _encode_binary_annotations(self, span: Span, encoded_local_endpoint): thrift_binary_annotations.append( ttypes.BinaryAnnotation( key=binary_annotation["key"], - value=binary_annotation["value"].encode('utf-8'), + value=binary_annotation["value"].encode("utf-8"), annotation_type=ttypes.AnnotationType.STRING, host=encoded_local_endpoint, ) @@ -153,11 +151,11 @@ def _extract_binary_annotations(self, span: Span): [ { "key": "otel.instrumentation_library.name", - "value": span.instrumentation_info.name + "value": span.instrumentation_info.name, }, { "key": "otel.instrumentation_library.version", - "value": span.instrumentation_info.version + "value": span.instrumentation_info.version, }, ] ) @@ -166,7 +164,7 @@ def _extract_binary_annotations(self, span: Span): binary_annotations.append( { "key": "otel.status_code", - "value": str(span.status.status_code.value) + "value": str(span.status.status_code.value), } ) @@ -190,7 +188,7 @@ def encode_span_id(span_id: int): 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') + bits = format(span_id, "b") if len(bits) < 64: encoded_span_id = span_id else: @@ -219,7 +217,7 @@ def encode_trace_id(trace_id: int): :param trace_id: :return: tuple of (encoded_trace_id, encoded_trace_id_high) """ - bits = format(trace_id, 'b') + bits = format(trace_id, "b") bits_length = len(bits) encoded_trace_id = int(bits[-63:], 2) @@ -231,7 +229,7 @@ def encode_trace_id(trace_id: int): "protocol signed integer format: [%02x => %02x%02x]", trace_id, encoded_trace_id_high, - encoded_trace_id + encoded_trace_id, ) else: encoded_trace_id_high = None diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/sender/http.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/sender/http.py index bafca21585..bbfe5bf9fb 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/sender/http.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/sender/http.py @@ -31,9 +31,7 @@ class HttpSender(Sender): def __init__( - self, - endpoint: str, - encoding: Optional[Encoding] = DEFAULT_ENCODING, + self, endpoint: str, encoding: Optional[Encoding] = DEFAULT_ENCODING, ): super().__init__(endpoint, encoding) @@ -61,7 +59,7 @@ def supported_encodings() -> List[Encoding]: Encoding.JSON_V1, Encoding.JSON_V2, Encoding.PROTOBUF, - Encoding.THRIFT + Encoding.THRIFT, ] def content_type(self): diff --git a/pyproject.toml b/pyproject.toml index e8c4a14439..1bfd1c655c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,8 @@ exclude = ''' /( # generated files docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen| exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen| - exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/protobuf/gen/| + exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/protobuf/gen| + exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/thrift/gen| opentelemetry-proto/src/opentelemetry/proto/collector| opentelemetry-proto/src/opentelemetry/proto/common| opentelemetry-proto/src/opentelemetry/proto/metrics| From bc28f15b4286fe9cfb1e9362fb27890973d393e7 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Sun, 15 Nov 2020 00:41:32 -0800 Subject: [PATCH 03/12] unit tests --- .../zipkin/encoder/v1/thrift/__init__.py | 36 +- .../tests/encoder/common_tests.py | 2 +- .../tests/encoder/test_v1_thrift.py | 399 ++++++++++++++++++ .../tests/test_zipkin_exporter.py | 15 +- 4 files changed, 433 insertions(+), 19 deletions(-) create mode 100644 exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py index b8981f5699..440a3cf707 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py @@ -35,6 +35,7 @@ encode_trace_id() and encode_span_id() methods for details. """ +import ipaddress import logging from typing import Sequence @@ -69,11 +70,16 @@ def _encode_spans(self, spans: Sequence[Span]): def _encode_local_endpoint(self): endpoint = ttypes.Endpoint( service_name=self.local_endpoint.service_name, - ipv4=self.local_endpoint.ipv4, port=self.local_endpoint.port, ) + if self.local_endpoint.ipv4 is not None: + endpoint.ipv4 = ipaddress.ip_address( + self.local_endpoint.ipv4 + ).packed if self.local_endpoint.ipv6 is not None: - endpoint.ipv6 = bytes(self.local_endpoint.ipv6) + endpoint.ipv6 = ipaddress.ip_address( + self.local_endpoint.ipv6 + ).packed return endpoint def _encode_span(self, span: Span, encoded_local_endpoint): @@ -107,18 +113,22 @@ def _encode_span(self, span: Span, encoded_local_endpoint): return thrift_span def _encode_annotations(self, span: Span, encoded_local_endpoint): - thrift_annotations = [] - - for annotation in self._extract_annotations_from_events(span.events): - thrift_annotations.append( - ttypes.Annotation( - timestamp=annotation["timestamp"], - value=annotation["value"], - host=encoded_local_endpoint, + 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 thrift_annotations + return encoded_annotations def _encode_binary_annotations(self, span: Span, encoded_local_endpoint): thrift_binary_annotations = [] diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py index e38350d06b..71de57daa2 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py @@ -155,7 +155,7 @@ def test_get_parent_id_from_span_context(self): @staticmethod def get_exhaustive_otel_span_list() -> List[trace._Span]: - trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + trace_id = 0x3E0C63257DE34C926F9EFCD03927272E base_time = 683647322 * 10 ** 9 # in ns start_times = ( diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py new file mode 100644 index 0000000000..351152e040 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py @@ -0,0 +1,399 @@ +# 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. +import json +import ipaddress + +from .common_tests import CommonEncoderTestCases +from opentelemetry import trace as trace_api +from opentelemetry.exporter.zipkin.encoder.v1.thrift import ThriftEncoder +from opentelemetry.exporter.zipkin.encoder.v1.thrift.gen.zipkinCore import ( + ttypes, +) +from opentelemetry.exporter.zipkin.endpoint import Endpoint +from opentelemetry.sdk import trace +from opentelemetry.trace import SpanKind, TraceFlags +from thrift.Thrift import TType +from thrift.transport.TTransport import TMemoryBuffer +from thrift.protocol import TBinaryProtocol + + +class TestThriftEncoder(CommonEncoderTestCases.CommonEncoderTest): + + @staticmethod + def get_encoder(*args, **kwargs) -> ThriftEncoder: + return ThriftEncoder(*args, **kwargs) + + def test_encode_trace_id_63_bits(self): + trace_id = 2**63 - 1 + encoded_trace_id, encoded_trace_id_high = ( + ThriftEncoder.encode_trace_id(trace_id) + ) + self.assertEqual(int(format(trace_id, "b")[-63:], 2), encoded_trace_id) + self.assertEqual(None, encoded_trace_id_high) + + def test_encode_trace_id_64_bits(self): + trace_id = 2**63 + encoded_trace_id, encoded_trace_id_high = ( + ThriftEncoder.encode_trace_id(trace_id) + ) + self.assertEqual(int(format(trace_id, "b")[-63:], 2), encoded_trace_id) + self.assertEqual( + int(format(trace_id, "b")[-126:-63], 2), + encoded_trace_id_high + ) + + def test_encode_trace_id_126_bits(self): + trace_id = 2**126 - 1 + encoded_trace_id, encoded_trace_id_high = ( + ThriftEncoder.encode_trace_id(trace_id) + ) + self.assertEqual(int(format(trace_id, "b")[-63:], 2), encoded_trace_id) + self.assertEqual( + int(format(trace_id, "b")[-126:-63], 2), + encoded_trace_id_high + ) + + def test_encode_trace_id_127_bits(self): + trace_id = 2**126 + with self.assertLogs(level='WARNING') as cm: + encoded_trace_id, encoded_trace_id_high = ( + ThriftEncoder.encode_trace_id(trace_id) + ) + self.assertEqual( + 'Trace id truncated to fit into Thrift protocol signed integer ' + 'format: [40000000000000000000000000000000 => 0000]', + cm.records[0].message + ) + self.assertEqual(int(format(trace_id, "b")[-63:], 2), encoded_trace_id) + self.assertEqual( + int(format(trace_id, "b")[-126:-63], 2), + encoded_trace_id_high + ) + + def test_encode_span_id(self): + span_id = 2**63 - 1 + self.assertEqual( + int(format(span_id, "b")[-63:], 2), + ThriftEncoder.encode_span_id(span_id), + ) + + def test_encode_span_id_truncate(self): + span_id = 2**63 + with self.assertLogs(level='WARNING') as cm: + encoded_span_id = ThriftEncoder.encode_span_id(span_id) + self.assertEqual( + 'Span id truncated to fit into Thrift protocol signed integer ' + 'format: [8000000000000000 => 00]', + cm.records[0].message + ) + self.assertEqual(int(format(span_id, "b")[-63:], 2), encoded_span_id) + + def test_encode_local_endpoint_default(self): + service_name = "test-service-name" + self.assertEqual( + ttypes.Endpoint(service_name=service_name), + ThriftEncoder(Endpoint(service_name))._encode_local_endpoint(), + ) + + def test_encode_local_endpoint_explicits(self): + service_name = "test-service-name" + ipv4 = "192.168.0.1" + ipv6 = "2001:db8::c001" + port = 414120 + self.assertEqual( + ttypes.Endpoint( + service_name=service_name, + ipv4=ipaddress.ip_address(ipv4).packed, + ipv6=ipaddress.ip_address(ipv6).packed, + port=port, + ), + ThriftEncoder( + Endpoint(service_name, ipv4, ipv6, port) + )._encode_local_endpoint(), + ) + + def test_encode(self): + service_name = "test-service" + local_endpoint = ttypes.Endpoint(service_name=service_name) + + otel_spans = self.get_exhaustive_otel_span_list() + thrift_trace_id, thrift_trace_id_high = ThriftEncoder.encode_trace_id( + otel_spans[0].context.trace_id + ) + + expected_thrift_spans = [ + ttypes.Span( + trace_id=thrift_trace_id, + trace_id_high=thrift_trace_id_high, + id=ThriftEncoder.encode_span_id( + otel_spans[0].context.span_id + ), + name=otel_spans[0].name, + timestamp=ThriftEncoder.nsec_to_usec_round( + otel_spans[0].start_time + ), + duration=( + ThriftEncoder.nsec_to_usec_round( + otel_spans[0].end_time - otel_spans[0].start_time + ) + ), + annotations=[ + ttypes.Annotation( + timestamp=otel_spans[0].events[0].timestamp + // 10 ** 3, + value=json.dumps({ + "event0": { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + }), + host=local_endpoint, + ) + ], + binary_annotations=[ + ttypes.BinaryAnnotation( + key="key_bool", + value=str(False).encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ttypes.BinaryAnnotation( + key="key_string", + value="hello_world".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ttypes.BinaryAnnotation( + key="key_float", + value="111.22".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ttypes.BinaryAnnotation( + key="otel.status_code", + value="2".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ttypes.BinaryAnnotation( + key="otel.status_description", + value="Example description".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ], + debug=True, + parent_id=ThriftEncoder.encode_span_id( + otel_spans[0].parent.span_id + ), + ), + ttypes.Span( + trace_id=thrift_trace_id, + trace_id_high=thrift_trace_id_high, + id=ThriftEncoder.encode_span_id( + otel_spans[1].context.span_id + ), + name=otel_spans[1].name, + timestamp=ThriftEncoder.nsec_to_usec_round( + otel_spans[1].start_time + ), + duration=( + ThriftEncoder.nsec_to_usec_round( + otel_spans[1].end_time - otel_spans[1].start_time + ) + ), + annotations=None, + binary_annotations=[ + ttypes.BinaryAnnotation( + key="key_resource", + value="some_resource".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ttypes.BinaryAnnotation( + key="otel.status_code", + value="1".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ], + debug=False, + ), + ttypes.Span( + trace_id=thrift_trace_id, + trace_id_high=thrift_trace_id_high, + id=ThriftEncoder.encode_span_id( + otel_spans[2].context.span_id + ), + name=otel_spans[2].name, + timestamp=ThriftEncoder.nsec_to_usec_round( + otel_spans[2].start_time + ), + duration=( + ThriftEncoder.nsec_to_usec_round( + otel_spans[2].end_time - otel_spans[2].start_time + ) + ), + annotations=None, + binary_annotations=[ + ttypes.BinaryAnnotation( + key="key_string", + value="hello_world".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ttypes.BinaryAnnotation( + key="key_resource", + value="some_resource".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ttypes.BinaryAnnotation( + key="otel.status_code", + value="1".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ], + debug=False, + ), + ttypes.Span( + trace_id=thrift_trace_id, + trace_id_high=thrift_trace_id_high, + id=ThriftEncoder.encode_span_id( + otel_spans[3].context.span_id + ), + name=otel_spans[3].name, + timestamp=ThriftEncoder.nsec_to_usec_round( + otel_spans[3].start_time + ), + duration=( + ThriftEncoder.nsec_to_usec_round( + otel_spans[3].end_time - otel_spans[3].start_time + ) + ), + annotations=None, + binary_annotations=[ + ttypes.BinaryAnnotation( + key="otel.instrumentation_library.name", + value="name".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ttypes.BinaryAnnotation( + key="otel.instrumentation_library.version", + value="version".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ttypes.BinaryAnnotation( + key="otel.status_code", + value="1".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=local_endpoint, + ), + ], + debug=False, + ), + ] + + self.assertEqual_encoded_spans( + expected_thrift_spans, + ThriftEncoder(Endpoint(service_name)).encode(otel_spans) + ) + + def _test_encode_max_tag_length(self, max_tag_value_length: int): + service_name = "test-service" + trace_id = 0x000C63257DE34C926F9EFCD03927272E + span_id = 0x04BF92DEEFC58C92 + start_time = 683647322 * 10 ** 9 # in ns + duration = 50 * 10 ** 6 + end_time = start_time + duration + tag1_value = "v" * 500 + tag2_value = "v" * 50 + + otel_span = trace._Span( + name=service_name, + context=trace_api.SpanContext( + trace_id, + span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ), + ) + otel_span.start(start_time=start_time) + otel_span.resource = trace.Resource({}) + otel_span.set_attribute("k1", tag1_value) + otel_span.set_attribute("k2", tag2_value) + otel_span.end(end_time=end_time) + + thrift_trace_id, thrift_trace_id_high = ThriftEncoder.encode_trace_id( + trace_id + ) + thrift_local_endpoint = ttypes.Endpoint(service_name=service_name) + expected_thrift_span = ttypes.Span( + trace_id=thrift_trace_id, + trace_id_high=thrift_trace_id_high, + id=ThriftEncoder.encode_span_id(span_id), + name=service_name, + timestamp=ThriftEncoder.nsec_to_usec_round(start_time), + duration=ThriftEncoder.nsec_to_usec_round(duration), + annotations=None, + binary_annotations=[ + ttypes.BinaryAnnotation( + key="k1", + value=tag1_value[:max_tag_value_length].encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=thrift_local_endpoint, + ), + ttypes.BinaryAnnotation( + key="k2", + value=tag2_value[:max_tag_value_length].encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=thrift_local_endpoint, + ), + ttypes.BinaryAnnotation( + key="otel.status_code", + value="1".encode("utf-8"), + annotation_type=ttypes.AnnotationType.STRING, + host=thrift_local_endpoint, + ), + ], + debug=True + ) + + test = ThriftEncoder(Endpoint(service_name)) + encode = test.encode([otel_span]) + + self.assertEqual_encoded_spans( + [expected_thrift_span], + ThriftEncoder( + Endpoint(service_name), + max_tag_value_length + ).encode([otel_span]) + ) + + def assertEqual_encoded_spans(self, expected_thrift_spans, + actual_serialized_output): + buffer = TMemoryBuffer() + protocol = TBinaryProtocol.TBinaryProtocolFactory().getProtocol(buffer) + protocol.writeListBegin(TType.STRUCT, len(expected_thrift_spans)) + + for expected_thrift_span in expected_thrift_spans: + expected_thrift_span.write(protocol) + protocol.writeListEnd() + expected_serialized_output = buffer.getvalue() + + self.assertEqual(expected_serialized_output, actual_serialized_output) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index 812722d7a1..bdc8da1d18 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -183,8 +183,13 @@ def test_constructor_all_params_and_env_vars(self): @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.FAILURE, status) + with self.assertLogs(level='ERROR') as cm: + mock_post.return_value = MockResponse(404) + self.assertEqual( + SpanExportResult.FAILURE, + ZipkinSpanExporter("test-service").export([]) + ) + self.assertEqual( + "Traces cannot be uploaded; status code: 404, message 404", + cm.records[0].message + ) From bc9bd85563962ff8ee49dce23365f8cfd7a7d409 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Sun, 15 Nov 2020 01:14:14 -0800 Subject: [PATCH 04/12] unit tests for httpsender --- .../tests/__init__.py | 13 --- .../tests/sender/test_http.py | 91 +++++++++++++++++++ .../tests/test_zipkin_exporter.py | 23 +---- 3 files changed, 92 insertions(+), 35 deletions(-) create mode 100644 exporter/opentelemetry-exporter-zipkin/tests/sender/test_http.py diff --git a/exporter/opentelemetry-exporter-zipkin/tests/__init__.py b/exporter/opentelemetry-exporter-zipkin/tests/__init__.py index b0a6f42841..e69de29bb2 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/__init__.py @@ -1,13 +0,0 @@ -# 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. diff --git a/exporter/opentelemetry-exporter-zipkin/tests/sender/test_http.py b/exporter/opentelemetry-exporter-zipkin/tests/sender/test_http.py new file mode 100644 index 0000000000..091411bc60 --- /dev/null +++ b/exporter/opentelemetry-exporter-zipkin/tests/sender/test_http.py @@ -0,0 +1,91 @@ +# 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. + +import unittest +from unittest.mock import patch + +from opentelemetry.exporter.zipkin import ZipkinSpanExporter +from opentelemetry.exporter.zipkin.encoder import Encoding +from opentelemetry.exporter.zipkin.sender.http import HttpSender +from opentelemetry.sdk.trace.export import SpanExportResult + + +class MockResponse: + def __init__(self, status_code): + self.status_code = status_code + self.text = status_code + + +class TestHttpSender(unittest.TestCase): + + def test_constructor_invalid_encoding(self): + with self.assertLogs(level='ERROR') as cm: + HttpSender('https://localhost/api', "Fake_Encoding") + self.assertEqual( + "Encoding type Fake_Encoding is not supported by this sender", + cm.records[0].message + ) + + @patch("requests.post") + def test_send_endpoint(self, mock_post): + endpoint = "http://localhost:9411/api/v2/spans" + mock_post.return_value = MockResponse(200) + result = ZipkinSpanExporter("test-service", endpoint).export([]) + self.assertEqual(SpanExportResult.SUCCESS, result) + # pylint: disable=unsubscriptable-object + kwargs = mock_post.call_args[1] + self.assertEqual(kwargs["url"], endpoint) + + @patch("requests.post") + def _test_send_content_type(self, encoding, content_type, mock_post): + mock_post.return_value = MockResponse(200) + ZipkinSpanExporter("test-service", encoding=encoding).export([]) + # pylint: disable=unsubscriptable-object + kwargs = mock_post.call_args[1] + self.assertEqual(content_type, kwargs["headers"]["Content-Type"]) + + def test_send_content_type_v1_thrift(self): + self._test_send_content_type(Encoding.THRIFT, "application/x-thrift") + + def test_send_content_type_v1_json(self): + self._test_send_content_type(Encoding.JSON_V1, "application/json") + + def test_send_content_type_v2_json(self): + self._test_send_content_type(Encoding.JSON_V2, "application/json") + + def test_send_content_type_v2_protobuf(self): + self._test_send_content_type( + Encoding.PROTOBUF, "application/x-protobuf" + ) + + @patch("requests.post") + def test_response_success(self, mock_post): + mock_post.return_value = MockResponse(200) + self.assertEqual( + SpanExportResult.SUCCESS, + ZipkinSpanExporter("test-service").export([]) + ) + + @patch("requests.post") + def test_response_failure(self, mock_post): + with self.assertLogs(level='ERROR') as cm: + mock_post.return_value = MockResponse(404) + self.assertEqual( + SpanExportResult.FAILURE, + ZipkinSpanExporter("test-service").export([]) + ) + self.assertEqual( + "Traces cannot be uploaded; status code: 404, message 404", + cm.records[0].message + ) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index bdc8da1d18..445995e4ef 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -12,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-lines import os import unittest -from unittest.mock import patch + from opentelemetry.configuration import Configuration from opentelemetry.exporter.zipkin import ( @@ -29,13 +28,6 @@ from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder from opentelemetry.exporter.zipkin.endpoint import Endpoint from opentelemetry.exporter.zipkin.sender.http import HttpSender -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): @@ -180,16 +172,3 @@ def test_constructor_all_params_and_env_vars(self): self.assertIsInstance(exporter.sender, HttpSender) self.assertEqual(exporter.sender.endpoint, sender_param_endpoint) self.assertEqual(exporter.sender.encoding, sender_param_encoding) - - @patch("requests.post") - def test_invalid_response(self, mock_post): - with self.assertLogs(level='ERROR') as cm: - mock_post.return_value = MockResponse(404) - self.assertEqual( - SpanExportResult.FAILURE, - ZipkinSpanExporter("test-service").export([]) - ) - self.assertEqual( - "Traces cannot be uploaded; status code: 404, message 404", - cm.records[0].message - ) From c4a291d5139f85d38630ac6a80a0ce0911c58c4a Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Sun, 15 Nov 2020 15:07:47 -0800 Subject: [PATCH 05/12] unit test logic for python <= 3.5 to handle unordered lists attrs --- .../tests/encoder/common_tests.py | 2 +- .../tests/encoder/test_v1_thrift.py | 73 ++++++++++++++++--- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py index 71de57daa2..5e414f88e6 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py @@ -232,7 +232,7 @@ def get_exhaustive_otel_span_list() -> List[trace._Span]: span3.end(end_time=end_times[2]) span4 = trace._Span( - name="test-span-3", context=other_context, parent=None + name="test-span-4", context=other_context, parent=None ) span4.start(start_time=start_times[3]) span4.resource = trace.Resource({}) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py index 351152e040..dad1538703 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py @@ -11,8 +11,9 @@ # 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 ipaddress +import json +import sys from .common_tests import CommonEncoderTestCases from opentelemetry import trace as trace_api @@ -34,7 +35,7 @@ class TestThriftEncoder(CommonEncoderTestCases.CommonEncoderTest): def get_encoder(*args, **kwargs) -> ThriftEncoder: return ThriftEncoder(*args, **kwargs) - def test_encode_trace_id_63_bits(self): + def test_encode_trace_id(self): trace_id = 2**63 - 1 encoded_trace_id, encoded_trace_id_high = ( ThriftEncoder.encode_trace_id(trace_id) @@ -387,13 +388,65 @@ def _test_encode_max_tag_length(self, max_tag_value_length: int): def assertEqual_encoded_spans(self, expected_thrift_spans, actual_serialized_output): - buffer = TMemoryBuffer() - protocol = TBinaryProtocol.TBinaryProtocolFactory().getProtocol(buffer) - protocol.writeListBegin(TType.STRUCT, len(expected_thrift_spans)) + """Since list ordering is not guaranteed in py3.5 or lower we can't + compare the serialized output. Instead we deserialize the actual + output and compare the thrift objects while explicitly handling the + annotations and binary annotations lists.""" + if sys.version_info.major == 3 and sys.version_info.minor <= 5: + actual_thrift_spans = [] + protocol = TBinaryProtocol.TBinaryProtocolFactory().getProtocol( + TMemoryBuffer(actual_serialized_output) + ) + etype, size = protocol.readListBegin() + for _ in range(size): + span = ttypes.Span() + span.read(protocol) + actual_thrift_spans.append(span) + protocol.readListEnd() + + for expected_span, actual_span in zip( + expected_thrift_spans, actual_thrift_spans + ): + actual_annotations = actual_span.annotations + if actual_annotations is not None: + actual_annotations = sorted( + actual_annotations, key=lambda x: x.timestamp + ) + expected_annotations = expected_span.annotations + if expected_annotations is not None: + expected_annotations = sorted( + expected_annotations, key=lambda x: x.timestamp + ) + actual_binary_annotations = actual_span.binary_annotations + if actual_binary_annotations is not None: + actual_binary_annotations = sorted( + actual_binary_annotations, key=lambda x: x.key + ) + expected_binary_annotations = expected_span.binary_annotations + if expected_binary_annotations is not None: + expected_binary_annotations = sorted( + expected_binary_annotations, key=lambda x: x.key + ) - for expected_thrift_span in expected_thrift_spans: - expected_thrift_span.write(protocol) - protocol.writeListEnd() - expected_serialized_output = buffer.getvalue() + actual_span.annotations = [] + actual_span.binary_annotations = [] + expected_span.annotations = [] + expected_span.binary_annotations = [] - self.assertEqual(expected_serialized_output, actual_serialized_output) + self.assertEqual(expected_span, actual_span) + self.assertEqual(expected_annotations, actual_annotations) + self.assertEqual( + expected_binary_annotations, + actual_binary_annotations + ) + else: + buffer = TMemoryBuffer() + protocol = TBinaryProtocol.TBinaryProtocolFactory().getProtocol( + buffer) + protocol.writeListBegin(TType.STRUCT, len(expected_thrift_spans)) + for expected_thrift_span in expected_thrift_spans: + expected_thrift_span.write(protocol) + protocol.writeListEnd() + expected_serialized_output = buffer.getvalue() + self.assertEqual(expected_serialized_output, + actual_serialized_output) From e1149b6ca30547423764ffce98632ec7d15cffe0 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Sun, 15 Nov 2020 16:11:25 -0800 Subject: [PATCH 06/12] Reducing random id generated bits for span and trace ids to avoid truncation with thrift-based exporters --- opentelemetry-api/src/opentelemetry/trace/ids_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/ids_generator.py b/opentelemetry-api/src/opentelemetry/trace/ids_generator.py index 31e1fee078..a3c3d46403 100644 --- a/opentelemetry-api/src/opentelemetry/trace/ids_generator.py +++ b/opentelemetry-api/src/opentelemetry/trace/ids_generator.py @@ -46,7 +46,7 @@ class RandomIdsGenerator(IdsGenerator): """ def generate_span_id(self) -> int: - return random.getrandbits(64) + return random.getrandbits(63) def generate_trace_id(self) -> int: - return random.getrandbits(128) + return random.getrandbits(126) From 818381e120d151d6cf8d7ce180ae0a3adf04a38b Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Sun, 15 Nov 2020 16:46:41 -0800 Subject: [PATCH 07/12] thrift-specific id generator for overriding default to avoid truncation --- .../zipkin/encoder/v1/thrift/__init__.py | 43 +++++++++++++++++++ .../src/opentelemetry/trace/ids_generator.py | 4 +- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py index 440a3cf707..9d2c9a4c5e 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py @@ -33,10 +33,15 @@ 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 Sequence from opentelemetry.exporter.zipkin.encoder.v1 import V1Encoder @@ -44,6 +49,7 @@ ttypes, ) 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 @@ -204,3 +210,40 @@ def encode_trace_id(trace_id: int): 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) diff --git a/opentelemetry-api/src/opentelemetry/trace/ids_generator.py b/opentelemetry-api/src/opentelemetry/trace/ids_generator.py index a3c3d46403..31e1fee078 100644 --- a/opentelemetry-api/src/opentelemetry/trace/ids_generator.py +++ b/opentelemetry-api/src/opentelemetry/trace/ids_generator.py @@ -46,7 +46,7 @@ class RandomIdsGenerator(IdsGenerator): """ def generate_span_id(self) -> int: - return random.getrandbits(63) + return random.getrandbits(64) def generate_trace_id(self) -> int: - return random.getrandbits(126) + return random.getrandbits(128) From e2b38374d12f430afa8db1b07f7eb512e388c6b8 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Sun, 15 Nov 2020 18:10:12 -0800 Subject: [PATCH 08/12] changelog --- exporter/opentelemetry-exporter-zipkin/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md index 8262116c49..a8f657eb02 100644 --- a/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md +++ b/exporter/opentelemetry-exporter-zipkin/CHANGELOG.md @@ -4,6 +4,7 @@ - Support for v2 api protobuf format ([#1318](https://github.com/open-telemetry/opentelemetry-python/pull/1318)) - Support for v1 api json format (TBD) +- Support for v1 api thrift format (TBD) ## Version 0.14b0 From 8a69a6d52c80580c42ea0bb9fdc6640af58ad4bf Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Tue, 17 Nov 2020 17:08:22 -0800 Subject: [PATCH 09/12] test fix --- .../opentelemetry-exporter-zipkin/tests/sender/test_http.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/sender/test_http.py b/exporter/opentelemetry-exporter-zipkin/tests/sender/test_http.py index 091411bc60..24bf3e2aad 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/sender/test_http.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/sender/test_http.py @@ -30,12 +30,8 @@ def __init__(self, status_code): class TestHttpSender(unittest.TestCase): def test_constructor_invalid_encoding(self): - with self.assertLogs(level='ERROR') as cm: + with self.assertRaises(ValueError): HttpSender('https://localhost/api', "Fake_Encoding") - self.assertEqual( - "Encoding type Fake_Encoding is not supported by this sender", - cm.records[0].message - ) @patch("requests.post") def test_send_endpoint(self, mock_post): From 449d6f5c05ca3125196036afead4abc1d3da648f Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Tue, 17 Nov 2020 17:13:38 -0800 Subject: [PATCH 10/12] stricter type handling for thrift encoder --- .../zipkin/encoder/v1/thrift/__init__.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py index 9d2c9a4c5e..6e5ec34dae 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/thrift/__init__.py @@ -42,7 +42,7 @@ import ipaddress import logging import random -from typing import Sequence +from typing import List, Optional, Sequence from opentelemetry.exporter.zipkin.encoder.v1 import V1Encoder from opentelemetry.exporter.zipkin.encoder.v1.thrift.gen.zipkinCore import ( @@ -63,7 +63,7 @@ class ThriftEncoder(V1Encoder): API spec: https://github.com/openzipkin/zipkin-api/tree/master/thrift """ - def _encode_spans(self, spans: Sequence[Span]): + def _encode_spans(self, spans: Sequence[Span]) -> str: encoded_local_endpoint = self._encode_local_endpoint() buffer = TMemoryBuffer() protocol = TBinaryProtocol.TBinaryProtocolFactory().getProtocol(buffer) @@ -73,7 +73,7 @@ def _encode_spans(self, spans: Sequence[Span]): protocol.writeListEnd() return buffer.getvalue() - def _encode_local_endpoint(self): + def _encode_local_endpoint(self) -> ttypes.Endpoint: endpoint = ttypes.Endpoint( service_name=self.local_endpoint.service_name, port=self.local_endpoint.port, @@ -88,7 +88,9 @@ def _encode_local_endpoint(self): ).packed return endpoint - def _encode_span(self, span: Span, encoded_local_endpoint): + 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 @@ -118,7 +120,9 @@ def _encode_span(self, span: Span, encoded_local_endpoint): return thrift_span - def _encode_annotations(self, span: Span, encoded_local_endpoint): + 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 @@ -136,7 +140,9 @@ def _encode_annotations(self, span: Span, encoded_local_endpoint): ) return encoded_annotations - def _encode_binary_annotations(self, span: Span, encoded_local_endpoint): + 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( @@ -154,7 +160,7 @@ def _encode_binary_annotations(self, span: Span, encoded_local_endpoint): return thrift_binary_annotations @staticmethod - def encode_span_id(span_id: int): + 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. @@ -178,7 +184,7 @@ def encode_span_id(span_id: int): return encoded_span_id @staticmethod - def encode_trace_id(trace_id: int): + 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 From 55a40a0acebc80f3f10c2ca238de081504076821 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Thu, 3 Dec 2020 13:34:32 -0800 Subject: [PATCH 11/12] lint --- .../src/opentelemetry/exporter/zipkin/__init__.py | 5 +++-- .../src/opentelemetry/exporter/zipkin/encoder/__init__.py | 3 +-- .../src/opentelemetry/exporter/zipkin/encoder/v1/json.py | 2 +- .../tests/encoder/common_tests.py | 4 ++-- .../tests/encoder/test_v1_json.py | 3 ++- .../tests/encoder/test_v2_json.py | 3 ++- .../tests/encoder/test_v2_protobuf.py | 5 +++-- .../tests/test_zipkin_exporter.py | 2 +- 8 files changed, 15 insertions(+), 12 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py index c2211143d6..cf74bd57ce 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/__init__.py @@ -83,16 +83,17 @@ """ import logging -import requests from typing import Optional, Sequence +import requests + 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 NodeEndpoint, IpInput +from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import Span diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py index 9c4c2b7648..1a0210692b 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/__init__.py @@ -18,9 +18,9 @@ """ import abc -from enum import Enum import json import logging +from enum import Enum from typing import Any, Dict, List, Optional, Sequence, TypeVar from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint @@ -81,7 +81,6 @@ def _encode_span( https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk_exporters/zipkin.md#request-payload """ - pass @staticmethod @abc.abstractmethod diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py index 5e0d4e6f74..d3b612ce88 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/encoder/v1/json.py @@ -14,7 +14,7 @@ """Zipkin Export Encoders for JSON formats """ -from typing import Dict, List +from typing import Dict from opentelemetry.exporter.zipkin.encoder import JsonEncoder from opentelemetry.exporter.zipkin.encoder.v1 import V1Encoder diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py index 83a0026967..5a0bfce75d 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/common_tests.py @@ -13,9 +13,9 @@ # limitations under the License. import abc import json -from typing import List -import unittest import sys +import unittest +from typing import List from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder import ( diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py index dd6ae78e86..12ae2f1bb0 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_json.py @@ -13,13 +13,14 @@ # limitations under the License. import json -from .common_tests import CommonEncoderTestCases from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder.v1.json import JsonV1Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace from opentelemetry.trace import TraceFlags +from .common_tests import CommonEncoderTestCases + class TestV1JsonEncoder(CommonEncoderTestCases.CommonJsonEncoderTest): @staticmethod diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py index ada1534ece..81effd4d39 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_json.py @@ -13,13 +13,14 @@ # limitations under the License. import json -from .common_tests import CommonEncoderTestCases from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder.v2.json import JsonV2Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace from opentelemetry.trace import SpanKind, TraceFlags +from .common_tests import CommonEncoderTestCases + class TestV2JsonEncoder(CommonEncoderTestCases.CommonJsonEncoderTest): @staticmethod diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py index 7b81c68bc2..20f8ab021c 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v2_protobuf.py @@ -11,10 +11,9 @@ # 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 ipaddress +import json -from .common_tests import CommonEncoderTestCases from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder.v2.protobuf import ProtobufEncoder from opentelemetry.exporter.zipkin.encoder.v2.protobuf.gen import zipkin_pb2 @@ -22,6 +21,8 @@ from opentelemetry.sdk import trace from opentelemetry.trace import SpanKind, TraceFlags +from .common_tests import CommonEncoderTestCases + class TestProtobufEncoder(CommonEncoderTestCases.CommonEncoderTest): @staticmethod diff --git a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py index e190c65eb6..d3d6299ec4 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin_exporter.py @@ -20,9 +20,9 @@ from opentelemetry.configuration import Configuration from opentelemetry.exporter.zipkin import ( - ZipkinSpanExporter, DEFAULT_ENDPOINT, DEFAULT_SERVICE_NAME, + ZipkinSpanExporter, ) from opentelemetry.exporter.zipkin.encoder import Encoding from opentelemetry.exporter.zipkin.encoder.v2.json import JsonV2Encoder From 34f3931c091f069ad7b5e47fb1c9ac8b10db6574 Mon Sep 17 00:00:00 2001 From: Rob Knox Date: Thu, 3 Dec 2020 14:01:55 -0800 Subject: [PATCH 12/12] lint --- .../tests/encoder/test_v1_thrift.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py index d38393a295..862631a66d 100644 --- a/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py +++ b/exporter/opentelemetry-exporter-zipkin/tests/encoder/test_v1_thrift.py @@ -23,12 +23,13 @@ ) from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace -from opentelemetry.trace import SpanKind, TraceFlags +from opentelemetry.trace import TraceFlags from thrift.Thrift import TType from thrift.transport.TTransport import TMemoryBuffer from thrift.protocol import TBinaryProtocol +# pylint: disable=protected-access class TestThriftEncoder(CommonEncoderTestCases.CommonEncoderTest): @staticmethod def get_encoder(*args, **kwargs) -> ThriftEncoder: @@ -311,7 +312,7 @@ def test_encode(self): ), ] - self.assertEqual_encoded_spans( + self.assert_equal_encoded_spans( expected_thrift_spans, ThriftEncoder().serialize(otel_spans, NodeEndpoint(service_name)), ) @@ -376,14 +377,15 @@ def _test_encode_max_tag_length(self, max_tag_value_length: int): debug=True, ) - self.assertEqual_encoded_spans( + self.assert_equal_encoded_spans( [expected_thrift_span], ThriftEncoder(max_tag_value_length).serialize( [otel_span], NodeEndpoint(service_name) ), ) - def assertEqual_encoded_spans( + # pylint: disable=too-many-locals + def assert_equal_encoded_spans( self, expected_thrift_spans, actual_serialized_output ): """Since list ordering is not guaranteed in py3.5 or lower we can't @@ -395,7 +397,7 @@ def assertEqual_encoded_spans( protocol = TBinaryProtocol.TBinaryProtocolFactory().getProtocol( TMemoryBuffer(actual_serialized_output) ) - etype, size = protocol.readListBegin() + etype, size = protocol.readListBegin() # pylint: disable=W0612 for _ in range(size): span = ttypes.Span() span.read(protocol)