Skip to content

Commit

Permalink
add support of otlp exporter
Browse files Browse the repository at this point in the history
Depends-On: Idcff5d79b4884dd1cc7ed1ab42c9e4ce89d4a6d2
Signed-off-by: Sahid Orentino Ferdjaoui <sahid.ferdjaoui@industrialdiscipline.com>
Change-Id: I74cdcb2aa99b0162ba1c14059111f09d0bb534e3
  • Loading branch information
sahid committed May 10, 2023
1 parent 4f3eaa4 commit 908e740
Show file tree
Hide file tree
Showing 11 changed files with 345 additions and 4 deletions.
17 changes: 15 additions & 2 deletions devstack/lib/osprofiler
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,29 @@ function install_redis() {
pip_install_gr redis
}

function install_jaeger() {
function install_jaeger_backend() {
if is_ubuntu; then
install_package docker.io
start_service docker
add_user_to_group $STACK_USER docker
sg docker -c "docker run -d --name jaeger -p 6831:6831/udp -p 16686:16686 jaegertracing/all-in-one:1.7"
sg docker -c "docker run -d --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 6831:6831/udp -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:1.42"
else
exit_distro_not_supported "docker.io installation"
fi
}

function install_jaeger() {
install_jaeger_backend
pip_install jaeger-client
}

function install_otlp() {
# For OTLP we use Jaeger backend but any OTLP compatible backend
# can be used.
install_jaeger_backend
pip_install opentelemetry-sdk opentelemetry-exporter-otlp
}

function drop_jaeger() {
sg docker -c 'docker rm jaeger --force'
}
Expand Down Expand Up @@ -112,6 +122,9 @@ function install_osprofiler_collector() {
elif [ "$OSPROFILER_COLLECTOR" == "jaeger" ]; then
install_jaeger
OSPROFILER_CONNECTION_STRING=${OSPROFILER_CONNECTION_STRING:-"jaeger://localhost:6831"}
elif [ "$OSPROFILER_COLLECTOR" == "otlp" ]; then
install_otlp
OSPROFILER_CONNECTION_STRING=${OSPROFILER_CONNECTION_STRING:-"otlp://localhost:4318"}
elif [ "$OSPROFILER_COLLECTOR" == "elasticsearch" ]; then
install_elasticsearch
OSPROFILER_CONNECTION_STRING=${OSPROFILER_CONNECTION_STRING:-"elasticsearch://elastic:changeme@localhost:9200"}
Expand Down
3 changes: 2 additions & 1 deletion devstack/plugin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then
configure_osprofiler_in_tempest

elif [[ "$1" == "unstack" ]]; then
if [[ "$OSPROFILER_COLLECTOR" == "jaeger" ]]; then
if [[ "$OSPROFILER_COLLECTOR" == "jaeger" || \
"$OSPROFILER_COLLECTOR" == "otlp" ]]; then
echo_summary "Deleting jaeger docker container"
drop_jaeger
fi
Expand Down
22 changes: 22 additions & 0 deletions doc/source/user/collectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,25 @@ to create tables and select and insert rows.
- MySQL 5.7.8

.. _SQLAlchemy understands: https://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls


OTLP
----

Use OTLP exporter. Can be used with any comptable backend that support
OTLP.

Usage
=====
To use the driver, the `connection_string` in the `[osprofiler]` config section
needs to be set::

[osprofiler]
connection_string = otlp://192.168.192.81:4318

Example: By default, jaeger is listening OTLP on 4318.

.. note::

Curently the exporter is only supporting HTTP. In future some work
may happen to support gRPC.
2 changes: 2 additions & 0 deletions lower-constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ dulwich===0.15.0
elasticsearch===2.0.0
importlib_metadata==1.7.0
jaeger-client==3.8.0
opentelemetry-exporter-otlp==1.16.0
opentelemetry-sdk==1.16.0
netaddr===0.7.18
openstackdocstheme==2.2.1
oslo.concurrency===3.26.0
Expand Down
12 changes: 12 additions & 0 deletions osprofiler/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,15 @@ def shorten_id(span_id):
# Return a new short id for this
short_id = shorten_id(uuidutils.generate_uuid())
return short_id


def uuid_to_int128(span_uuid):
"""Convert from uuid4 to 128 bit id for OpenTracing"""
if isinstance(span_uuid, int):
return span_uuid
try:
span_int = uuid.UUID(span_uuid).int
except ValueError:
# Return a new short id for this
span_int = uuid_to_int128(uuidutils.generate_uuid())
return span_int
1 change: 1 addition & 0 deletions osprofiler/drivers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from osprofiler.drivers import base # noqa
from osprofiler.drivers import elasticsearch_driver # noqa
from osprofiler.drivers import jaeger # noqa
from osprofiler.drivers import otlp # noqa
from osprofiler.drivers import loginsight # noqa
from osprofiler.drivers import messaging # noqa
from osprofiler.drivers import mongodb # noqa
Expand Down
179 changes: 179 additions & 0 deletions osprofiler/drivers/otlp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# All Rights Reserved.
#
# 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 collections
from urllib import parse as parser

from oslo_config import cfg
from oslo_serialization import jsonutils

from osprofiler import _utils as utils
from osprofiler.drivers import base
from osprofiler import exc


class OTLP(base.Driver):
def __init__(self, connection_str, project=None, service=None, host=None,
conf=cfg.CONF, **kwargs):
"""OTLP driver using OTLP exporters."""

super(OTLP, self).__init__(connection_str, project=project,
service=service, host=host,
conf=conf, **kwargs)
try:
from opentelemetry import trace as trace_api

from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter # noqa
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace import TracerProvider

self.trace_api = trace_api
except ImportError:
raise exc.CommandError(
"To use OSProfiler with OTLP exporters, "
"please install `opentelemetry-sdk` and "
"opentelemetry-exporter-otlp libraries. "
"To install with pip:\n `pip install opentelemetry-sdk "
"opentelemetry-exporter-otlp`.")

service_name = self._get_service_name(conf, project, service)
resource = Resource(attributes={
"service.name": service_name
})

parsed_url = parser.urlparse(connection_str)
# TODO("sahid"): We also want to handle https scheme?
parsed_url = parsed_url._replace(scheme="http")

self.trace_api.set_tracer_provider(
TracerProvider(resource=resource))
self.tracer = self.trace_api.get_tracer(__name__)

exporter = OTLPSpanExporter("{}/v1/traces".format(
parsed_url.geturl()))
self.trace_api.get_tracer_provider().add_span_processor(
BatchSpanProcessor(exporter))

self.spans = collections.deque()

def _get_service_name(self, conf, project, service):
prefix = conf.profiler_otlp.service_name_prefix
if prefix:
return "{}-{}-{}".format(prefix, project, service)
return "{}-{}".format(project, service)

@classmethod
def get_name(cls):
return "otlp"

def _kind(self, name):
if "wsgi" in name:
return self.trace_api.SpanKind.SERVER
elif ("db" in name or "http_client" in name or "api" in name):
return self.trace_api.SpanKind.CLIENT
return self.trace_api.SpanKind.INTERNAL

def _name(self, payload):
info = payload["info"]
if info.get("request"):
return "{}_{}".format(
info["request"]["method"], info["request"]["path"])
elif info.get("db"):
return "SQL_{}".format(
info["db"]["statement"].split(' ', 1)[0].upper())
return payload["name"].rstrip("-start")

def notify(self, payload):
if payload["name"].endswith("start"):
parent = self.trace_api.SpanContext(
trace_id=utils.uuid_to_int128(payload["base_id"]),
span_id=utils.shorten_id(payload["parent_id"]),
is_remote=False,
trace_flags=self.trace_api.TraceFlags(
self.trace_api.TraceFlags.SAMPLED))

ctx = self.trace_api.set_span_in_context(
self.trace_api.NonRecordingSpan(parent))

# OTLP Tracing span
span = self.tracer.start_span(
name=self._name(payload),
kind=self._kind(payload['name']),
attributes=self.create_span_tags(payload),
context=ctx)

span._context = self.trace_api.SpanContext(
trace_id=span.context.trace_id,
span_id=utils.shorten_id(payload["trace_id"]),
is_remote=span.context.is_remote,
trace_flags=span.context.trace_flags,
trace_state=span.context.trace_state)

self.spans.append(span)
else:
span = self.spans.pop()

# Store result of db call and function call
for call in ("db", "function"):
if payload.get("info", {}).get(call):
span.set_attribute(
"result", payload["info"][call]["result"])
# Span error tag and log
if payload["info"].get("etype"):
span.set_attribute("error", True)
span.add_event("log", {
"error.kind": payload["info"]["etype"],
"message": payload["info"]["message"]})
span.end()

def get_report(self, base_id):
return self._parse_results()

def list_traces(self, fields=None):
return []

def list_error_traces(self):
return []

def create_span_tags(self, payload):
"""Create tags an OpenTracing compatible span.
:param info: Information from OSProfiler trace.
:returns tags: A dictionary contains standard tags
from OpenTracing sematic conventions,
and some other custom tags related to http, db calls.
"""
tags = {}
info = payload["info"]

if info.get("db"):
# DB calls
tags["db.statement"] = info["db"]["statement"]
tags["db.params"] = jsonutils.dumps(info["db"]["params"])
elif info.get("request"):
# WSGI call
tags["http.path"] = info["request"]["path"]
tags["http.query"] = info["request"]["query"]
tags["http.method"] = info["request"]["method"]
tags["http.scheme"] = info["request"]["scheme"]
elif info.get("function"):
# RPC, function calls
if "args" in info["function"]:
tags["args"] = info["function"]["args"]
if "kwargs" in info["function"]:
tags["kwargs"] = info["function"]["kwargs"]
tags["name"] = info["function"]["name"]

return tags
19 changes: 18 additions & 1 deletion osprofiler/opts.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,22 @@

cfg.CONF.register_opts(_JAEGER_OPTS, group=_jaegerprofiler_opt_group)

_otlp_profiler_opt_group = cfg.OptGroup(
"profiler_otlp",
title="OTLP's profiler driver related options")

_otlp_service_name_prefix = cfg.StrOpt(
"service_name_prefix",
help="""
Set service name prefix to OTLP exporters.
""")

_OTLP_OPTS = [
_otlp_service_name_prefix,
]

cfg.CONF.register_opts(_OTLP_OPTS, group=_otlp_profiler_opt_group)


def set_defaults(conf, enabled=None, trace_sqlalchemy=None, hmac_keys=None,
connection_string=None, es_doc_type=None,
Expand Down Expand Up @@ -265,4 +281,5 @@ def disable_web_trace(conf=None):

def list_opts():
return [(_profiler_opt_group.name, _PROFILER_OPTS),
(_jaegerprofiler_opt_group, _JAEGER_OPTS)]
(_jaegerprofiler_opt_group, _JAEGER_OPTS),
(_otlp_profiler_opt_group, _OTLP_OPTS)]

0 comments on commit 908e740

Please sign in to comment.