Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

OT Collector trace exporter #405

Merged
merged 25 commits into from
Feb 28, 2020
Merged
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d6aa116
Initial commit
hectorhdzg Jan 7, 2020
83267f6
Blocked on proto python compiled files
hectorhdzg Jan 8, 2020
1c79ae6
WIP
hectorhdzg Feb 11, 2020
444334c
Merge remote-tracking branch 'upstream/master' into hectorhdzg/otcoll…
hectorhdzg Feb 11, 2020
2bab1cc
WIP
hectorhdzg Feb 11, 2020
bd7a021
Adding protobuf dependency
hectorhdzg Feb 11, 2020
e36cf3e
Adding more checks in tests
hectorhdzg Feb 11, 2020
809a27c
Removing pypy3 because if is not supported for GRPC
hectorhdzg Feb 11, 2020
3c769e1
Removing pypy3 Collector env from tox
hectorhdzg Feb 11, 2020
f90526d
Adding readme details
hectorhdzg Feb 13, 2020
242704a
Adding sample
hectorhdzg Feb 19, 2020
3c794df
Fix lint
hectorhdzg Feb 19, 2020
3a66e47
Addressing comments
hectorhdzg Feb 20, 2020
744b372
Merge branch 'master' into hectorhdzg/otcollector
hectorhdzg Feb 20, 2020
402e6d6
Fix lint
hectorhdzg Feb 20, 2020
4c48178
Merge branch 'hectorhdzg/otcollector' of https://github.com/hectorhdz…
hectorhdzg Feb 20, 2020
a4d93b9
Addressing comments
hectorhdzg Feb 21, 2020
9cac0c2
Merge branch 'master' into hectorhdzg/otcollector
hectorhdzg Feb 21, 2020
bb43c70
Merge branch 'master' into hectorhdzg/otcollector
c24t Feb 26, 2020
2c9c018
Add missing init file
c24t Feb 26, 2020
dc1274e
Use TracerSource in OTC example
c24t Feb 26, 2020
8f5bbe9
Update tox.ini
hectorhdzg Feb 27, 2020
f7feccb
Addressing comments
hectorhdzg Feb 27, 2020
64250e0
Merge branch 'hectorhdzg/otcollector' of https://github.com/hectorhdz…
hectorhdzg Feb 27, 2020
3df18a4
Adding exporter version
hectorhdzg Feb 28, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/basic_tracer/README.md
Original file line number Diff line number Diff line change
@@ -53,6 +53,24 @@ Click on the trace to view its details.

<p align="center"><img src="./images/jaeger-ui-detail.png?raw=true"/></p>

### Collector

* Start Collector

```sh
$ pip install docker-compose
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
$ cd docker
$ docker-compose up

* Run the sample

$ pip install opentelemetry-ext-otcollector
$ # from this directory
$ EXPORTER=collector python tracer.py




## Useful links
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
- For more information on tracing in Python, visit: <https://github.com/open-telemetry/opentelemetry-python>
18 changes: 18 additions & 0 deletions examples/basic_tracer/docker/collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
receivers:
opencensus:
endpoint: "0.0.0.0:55678"

exporters:
logging:
loglevel: debug

processors:
batch:
queued_retry:

service:
pipelines:
traces:
receivers: [opencensus]
exporters: [logging]
processors: [batch, queued_retry]
11 changes: 11 additions & 0 deletions examples/basic_tracer/docker/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: "2"
services:

# Collector
collector:
image: omnition/opentelemetry-collector-contrib:latest
command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"]
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
volumes:
- ./collector-config.yaml:/conf/collector-config.yaml
ports:
- "55678:55678"
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 11 additions & 0 deletions examples/basic_tracer/tracer.py
Original file line number Diff line number Diff line change
@@ -27,12 +27,23 @@
if os.getenv("EXPORTER") == "jaeger":
from opentelemetry.ext.jaeger import JaegerSpanExporter

print("Using JaegerSpanExporter")
exporter = JaegerSpanExporter(
service_name="basic-service",
agent_host_name="localhost",
agent_port=6831,
)
elif os.getenv("EXPORTER") == "collector":
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
from opentelemetry.ext.otcollector.trace_exporter import (
CollectorSpanExporter,
)

print("Using CollectorSpanExporter")
exporter = CollectorSpanExporter(
service_name="basic-service", endpoint="localhost:55678"
)
else:
print("Using ConsoleSpanExporter")
exporter = ConsoleSpanExporter()

# The preferred tracer implementation must be set, as the opentelemetry-api
4 changes: 4 additions & 0 deletions ext/opentelemetry-ext-otcollector/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## Unreleased

55 changes: 55 additions & 0 deletions ext/opentelemetry-ext-otcollector/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
OpenTelemetry Collector Exporter
=============================
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved

|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.
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.
Show resolved Hide resolved

# create a CollectorSpanExporter
collector_exporter = CollectorSpanExporter(
# optional:
# endpoint="http://myCollector:55678",
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
# service_name="test_service",
# host_name="http://localhost",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this have a protocol prefixing the host name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

)

# 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/>`_
49 changes: 49 additions & 0 deletions ext/opentelemetry-ext-otcollector/setup.cfg
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 = cncf-opentelemetry-contributors@lists.cncf.io
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an OpenCensus exporter? You write OT exporter in the title.

Copy link
Member Author

@hectorhdzg hectorhdzg Feb 21, 2020

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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.
Show resolved Hide resolved
opentelemetry-sdk
protobuf

[options.packages.find]
where = src
26 changes: 26 additions & 0 deletions ext/opentelemetry-ext-otcollector/setup.py
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,166 @@
# 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 = "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.
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:
try:
responses = self.client.Export(self.generate_span_requests(spans))

# # read response
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
for _ in responses:
pass

except grpc.RpcError:
return SpanExportResult.FAILED_NOT_RETRYABLE

return SpanExportResult.SUCCESS

def shutdown(self) -> None:
pass

def generate_span_requests(self, spans):
collector_spans = translate_to_collector(spans)
service_request = trace_service_pb2.ExportTraceServiceRequest(
node=self.node, spans=collector_spans
)
yield service_request


# 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"),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity: Why "big"?

Copy link
Member Author

@hectorhdzg hectorhdzg Feb 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no particular reason, is there any issue with it?

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The 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]

Copy link
Member

@Oberon00 Oberon00 Feb 21, 2020

Choose a reason for hiding this comment

The 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 trace_id[0] == 0x4b and trace_id[15] == 0x36 must be the case. Thus, "big" sounds correct.

Choose a reason for hiding this comment

The 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.

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.
Show resolved Hide resolved
else None,
)

if span.parent is not None and getattr(span.parent, "span_id", None):
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
collector_span.parent_span_id = span.parent.span_id.to_bytes(
8, "big"
)

if span.context.trace_state is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not possible.

Copy link
Member Author

Choose a reason for hiding this comment

The 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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cannot happen:

Maybe you wan to use if span.attributes instead, but I guess the check can simply be omitted.

Copy link
Member Author

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.
Show resolved Hide resolved
for (key, value) in span.attributes.items():
utils.add_proto_attribute_value(
collector_span.attributes, key, value
)

if span.events is not None:
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
for event in span.events:

collector_annotation = trace_pb2.Span.TimeEvent.Annotation(
description=trace_pb2.TruncatableString(value=event.name)
)

for (key, value) in event.attributes.items():
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
utils.add_proto_attribute_value(
collector_annotation.attributes, key, value
)

collector_span.time_events.time_event.add(
time=utils.proto_timestamp_from_time_ns(event.timestamp),
annotation=collector_annotation,
)

if span.links is not None:
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
for link in span.links:
collector_span_link = collector_span.links.link.add()
collector_span_link.trace_id = link.context.trace_id.to_bytes(
16, "big"
)
collector_span_link.span_id = link.context.span_id.to_bytes(
8, "big"
)
if (
span.parent is not None
and link.context.span_id == span.parent.span_id
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
and link.context.trace_id == span.parent.trace_id
):
collector_span_link.type = (
trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN
)
else:
collector_span_link.type = (
trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED
)

for (key, value) in link.attributes.items():
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
utils.add_proto_attribute_value(
collector_span_link.attributes, key, value
)

collector_spans.append(collector_span)
return collector_spans
Loading