-
Notifications
You must be signed in to change notification settings - Fork 767
OT Collector trace exporter #405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
d6aa116
83267f6
1c79ae6
444334c
2bab1cc
bd7a021
e36cf3e
809a27c
3c769e1
f90526d
242704a
3c794df
3a66e47
744b372
402e6d6
4c48178
a4d93b9
9cac0c2
bb43c70
2c9c018
dc1274e
8f5bbe9
f7feccb
64250e0
3df18a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Changelog | ||
|
|
||
| ## Unreleased | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| OpenTelemetry Collector Exporter | ||
| ============================= | ||
|
|
||
| |pypi| | ||
|
|
||
| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-otcollector.svg | ||
| :target: https://pypi.org/project/opentelemetry-ext-otcollector/ | ||
|
|
||
| This library allows to export data to `OpenTelemetry Collector <https://github.com/open-telemetry/opentelemetry-collector/>`_. | ||
|
|
||
| Installation | ||
| ------------ | ||
|
|
||
| :: | ||
|
|
||
| pip install opentelemetry-ext-otcollector | ||
|
|
||
|
|
||
| Usage | ||
| ----- | ||
|
|
||
| The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ traces to `OpenTelemetry Collector`_. | ||
|
|
||
| .. code:: python | ||
|
|
||
| from opentelemetry import trace | ||
| from opentelemetry.ext.otcollector import CollectorSpanExporter | ||
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| from opentelemetry.sdk.trace import TracerSource | ||
| from opentelemetry.sdk.trace.export import BatchExportSpanProcessor | ||
|
|
||
| trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) | ||
| tracer = trace.tracer_source().get_tracer(__name__) | ||
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # create a CollectorSpanExporter | ||
| collector_exporter = CollectorSpanExporter( | ||
| # optional: | ||
| # endpoint="http://myCollector:55678", | ||
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # service_name="test_service", | ||
| # host_name="http://localhost", | ||
|
||
| ) | ||
|
|
||
| # Create a BatchExportSpanProcessor and add the exporter to it | ||
| span_processor = BatchExportSpanProcessor(collector_exporter) | ||
|
|
||
| # add to the tracer | ||
| trace.tracer_source().add_span_processor(span_processor) | ||
|
|
||
| with tracer.start_as_current_span("foo"): | ||
| print("Hello world!") | ||
|
|
||
| References | ||
| ---------- | ||
|
|
||
| * `OpenTelemetry Collector <https://github.com/open-telemetry/opentelemetry-collector/>`_ | ||
| * `OpenTelemetry Project <https://opentelemetry.io/>`_ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # Copyright 2020, OpenTelemetry Authors | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # | ||
| [metadata] | ||
| name = opentelemetry-ext-otcollector | ||
| description = OpenTelemetry Collector Exporter | ||
| long_description = file: README.rst | ||
| long_description_content_type = text/x-rst | ||
| author = OpenTelemetry Authors | ||
| author_email = [email protected] | ||
| url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-otcollector | ||
| platforms = any | ||
| license = Apache-2.0 | ||
| classifiers = | ||
| Development Status :: 3 - Alpha | ||
| Intended Audience :: Developers | ||
| License :: OSI Approved :: Apache Software License | ||
| Programming Language :: Python | ||
| Programming Language :: Python :: 3 | ||
| Programming Language :: Python :: 3.4 | ||
| Programming Language :: Python :: 3.5 | ||
| Programming Language :: Python :: 3.6 | ||
| Programming Language :: Python :: 3.7 | ||
|
|
||
| [options] | ||
| python_requires = >=3.4 | ||
| package_dir= | ||
| =src | ||
| packages=find_namespace: | ||
| install_requires = | ||
| grpcio >= 1.0.0, < 2.0.0 | ||
| opencensus-proto >= 0.1.0, < 1.0.0 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an OpenCensus exporter? You write OT exporter in the title.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well is in fact exporting to OT Collector through OpenCensus receiver, OT receiver will be ready in several weeks I added more details in first comment in the PR, we will need to revisit this one and add code to handle OT receiver using OT proto
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI I spent some time trying to make it work with OT proto building the protobuf files myself then realizing the receiver is not there yet, people are interested in having this ready so decided to take same approach as JS SDK and support it through OpenCensus receiver, once OT receiver is ready hopefully changes only affect the span transformation and some other small pieces of code |
||
| opentelemetry-api | ||
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| opentelemetry-sdk | ||
| protobuf | ||
|
|
||
| [options.packages.find] | ||
| where = src | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Copyright 2020, OpenTelemetry Authors | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| import os | ||
|
|
||
| import setuptools | ||
|
|
||
| BASE_DIR = os.path.dirname(__file__) | ||
| VERSION_FILENAME = os.path.join( | ||
| BASE_DIR, "src", "opentelemetry", "ext", "otcollector", "version.py" | ||
| ) | ||
| PACKAGE_INFO = {} | ||
| with open(VERSION_FILENAME) as f: | ||
| exec(f.read(), PACKAGE_INFO) | ||
|
|
||
| setuptools.setup(version=PACKAGE_INFO["__version__"]) |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,158 @@ | ||||
| # Copyright 2020, OpenTelemetry Authors | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
|
|
||||
| """OpenTelemetry Collector Exporter.""" | ||||
|
|
||||
| import logging | ||||
| from typing import Optional, Sequence | ||||
|
|
||||
| import grpc | ||||
| from opencensus.proto.agent.trace.v1 import ( | ||||
| trace_service_pb2, | ||||
| trace_service_pb2_grpc, | ||||
| ) | ||||
| from opencensus.proto.trace.v1 import trace_pb2 | ||||
|
|
||||
| import opentelemetry.ext.otcollector.util as utils | ||||
| from opentelemetry.sdk.trace import Span, SpanContext | ||||
| from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult | ||||
| from opentelemetry.trace import SpanKind, TraceState | ||||
|
|
||||
| DEFAULT_ENDPOINT = "http://localhost:55678" | ||||
|
|
||||
| logger = logging.getLogger(__name__) | ||||
|
|
||||
|
|
||||
| # pylint: disable=no-member | ||||
| class CollectorSpanExporter(SpanExporter): | ||||
| """OpenTelemetry Collector span exporter. | ||||
|
|
||||
| Args: | ||||
| endpoint: OpenTelemetry Collector OpenCensus receiver endpoint. | ||||
| service_name: Name of Collector service. | ||||
| host_name: Host name. | ||||
| client: TraceService client stub. | ||||
| """ | ||||
|
|
||||
| def __init__( | ||||
| self, endpoint=None, service_name=None, host_name=None, client=None | ||||
| ): | ||||
| self.endpoint = DEFAULT_ENDPOINT if endpoint is None else endpoint | ||||
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
|
|
||||
| if client is None: | ||||
| self.channel = grpc.insecure_channel(self.endpoint) | ||||
| self.client = trace_service_pb2_grpc.TraceServiceStub( | ||||
| channel=self.channel | ||||
| ) | ||||
| else: | ||||
| self.client = client | ||||
|
|
||||
| self.node = utils.get_node(service_name, host_name) | ||||
|
|
||||
| def export(self, spans: Sequence[Span]) -> SpanExportResult: | ||||
| collector_spans = translate_to_collector(spans) | ||||
| export_request = trace_service_pb2.ExportTraceServiceRequest( | ||||
| node=self.node, spans=collector_spans | ||||
| ) | ||||
| try: | ||||
| self.client.Export(export_request) | ||||
| except grpc.RpcError: | ||||
| return SpanExportResult.FAILED_NOT_RETRYABLE | ||||
|
|
||||
| return SpanExportResult.SUCCESS | ||||
|
|
||||
| def shutdown(self) -> None: | ||||
| pass | ||||
|
|
||||
|
|
||||
| # pylint: disable=too-many-branches | ||||
| def translate_to_collector(spans: Sequence[Span]): | ||||
| collector_spans = [] | ||||
| for span in spans: | ||||
| collector_span = trace_pb2.Span( | ||||
| name=trace_pb2.TruncatableString(value=span.name), | ||||
| kind=utils.get_collector_span_kind(span.kind), | ||||
| trace_id=span.context.trace_id.to_bytes(16, "big"), | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity: Why "big"?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no particular reason, is there any issue with it?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to know if the collector expects the traces in a particular format, it is possible that a collection system is not able to assemble a full trace if we use the wrong trace id here.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://github.com/census-instrumentation/opencensus-proto/blob/master/src/opencensus/proto/trace/v1/trace.proto#L41 there are no details about that, @owais s this something you know?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It shouldn't matter as long as the trace ID is consistent in all spans for a trace but I still suggest to test it out end to end. Generate a trace in python, export using this lib and check how the collector interprets it. You can use the file exporter to write the received spans to a file exporters:
file:
path: ./filename.json
service:
pipelines:
traces:
receivers: [opencensusreceiver]
exporters: [file]
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This absolutely matters. Traces can span multiple systems, multiple OpenTelemetry implementations/languages, even systems not using OpenTelemetry at all but e.g. OpenTracing+jaeger to report to the same back end. The "problem" here is that we use integers in Python to represent the trace ID, which is semantically a byte array. If we get an incoming trace ID like "4bf92f3577b34da6a3ce929d0e0e4736" then I think it is clear that
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I configured an http server example to use the collector exporter and a client to use Jaeger, when "big" is used the trace is correctly assembled, so "big" should be the right choice.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However I'm not sure this is true in all cases, so probably we want to keep an eye on this int <-> bytes <-> string conversions.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we read the trace ID from the wire we assume big-endianness, so "big" is right here unless we want to reverse it on the way out. |
||||
| span_id=span.context.span_id.to_bytes(8, "big"), | ||||
| start_time=utils.proto_timestamp_from_time_ns(span.start_time), | ||||
| end_time=utils.proto_timestamp_from_time_ns(span.end_time), | ||||
| status=trace_pb2.Status( | ||||
| code=span.status.canonical_code.value, | ||||
| message=span.status.description, | ||||
| ) | ||||
| if span.status is not None | ||||
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
| else None, | ||||
| ) | ||||
|
|
||||
| if span.parent is not None: | ||||
| collector_span.parent_span_id = span.parent.span_id.to_bytes( | ||||
| 8, "big" | ||||
| ) | ||||
|
|
||||
| if span.context.trace_state is not None: | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is not possible.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believed I triggered this one with a manually created Span in a unit test, same as others checks in this method I added if x instead of if x is not None. |
||||
| for (key, value) in span.context.trace_state.items(): | ||||
| collector_span.tracestate.entries.add(key=key, value=value) | ||||
|
|
||||
| if span.attributes is not None: | ||||
|
||||
| self.attributes = Span.empty_attributes |
Maybe you wan to use if span.attributes instead, but I guess the check can simply be omitted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have multiple "if" checks without the "is not None" for all collections now, I guess is safer to check even if we expect this value to be there.
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| # Copyright 2020, OpenTelemetry Authors | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| import os | ||
| import socket | ||
| import time | ||
|
|
||
| from google.protobuf.timestamp_pb2 import Timestamp | ||
| from opencensus.proto.agent.common.v1 import common_pb2 | ||
| from opencensus.proto.trace.v1 import trace_pb2 | ||
|
|
||
| from opentelemetry.trace import SpanKind | ||
| from opentelemetry.util.version import __version__ as opentelemetry_version | ||
|
|
||
| # OT Collector exporter version | ||
| EXPORTER_VERSION = "0.0.1" | ||
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def proto_timestamp_from_time_ns(time_ns): | ||
| """Converts datetime to protobuf timestamp. | ||
|
|
||
| :type time_ns: int | ||
hectorhdzg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| :param time_ns: Time in nanoseconds | ||
|
|
||
| :rtype: :class:`~google.protobuf.timestamp_pb2.Timestamp` | ||
| :returns: protobuf timestamp | ||
| """ | ||
| ts = Timestamp() | ||
| if time_ns is not None: | ||
| # pylint: disable=no-member | ||
| ts.FromNanoseconds(time_ns) | ||
| return ts | ||
|
|
||
|
|
||
| # pylint: disable=no-member | ||
| def get_collector_span_kind(kind: SpanKind): | ||
| if kind is SpanKind.SERVER: | ||
| return trace_pb2.Span.SpanKind.SERVER | ||
| if kind is SpanKind.CLIENT: | ||
| return trace_pb2.Span.SpanKind.CLIENT | ||
| return trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED | ||
|
|
||
|
|
||
| def add_proto_attribute_value(pb_attributes, key, value): | ||
| """Sets string, int, boolean or float value on protobuf | ||
| span, link or annotation attributes. | ||
|
|
||
| :type pb_attributes: | ||
| :class: `~opencensus.proto.trace.Span.Attributes` | ||
| :param pb_attributes: protobuf Span's attributes property | ||
|
|
||
| :type key: str | ||
| :param key: attribute key to set | ||
|
|
||
| :type value: str or int or bool or float | ||
| :param value: attribute value | ||
| """ | ||
|
|
||
| if isinstance(value, bool): | ||
| pb_attributes.attribute_map[key].bool_value = value | ||
| elif isinstance(value, int): | ||
| pb_attributes.attribute_map[key].int_value = value | ||
| elif isinstance(value, str): | ||
| pb_attributes.attribute_map[key].string_value.value = value | ||
| elif isinstance(value, float): | ||
| pb_attributes.attribute_map[key].double_value = value | ||
| else: | ||
| pb_attributes.attribute_map[key].string_value.value = str(value) | ||
|
|
||
|
|
||
| # pylint: disable=no-member | ||
| def get_node(service_name, host_name): | ||
| """Generates Node message from params and system information. | ||
| """ | ||
| return common_pb2.Node( | ||
| identifier=common_pb2.ProcessIdentifier( | ||
| host_name=socket.gethostname() if host_name is None else host_name, | ||
| pid=os.getpid(), | ||
| start_timestamp=proto_timestamp_from_time_ns( | ||
| int(time.time() * 1e9) | ||
| ), | ||
| ), | ||
| library_info=common_pb2.LibraryInfo( | ||
| language=common_pb2.LibraryInfo.Language.Value("PYTHON"), | ||
| exporter_version=EXPORTER_VERSION, | ||
| core_library_version=opentelemetry_version, | ||
| ), | ||
| service_info=common_pb2.ServiceInfo(name=service_name), | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.