summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--devstack/lib/osprofiler17
-rw-r--r--devstack/plugin.sh3
-rw-r--r--doc/source/user/collectors.rst22
-rw-r--r--lower-constraints.txt2
-rw-r--r--osprofiler/_utils.py12
-rw-r--r--osprofiler/drivers/__init__.py1
-rw-r--r--osprofiler/drivers/otlp.py179
-rw-r--r--osprofiler/opts.py19
-rw-r--r--osprofiler/tests/unit/drivers/test_otlp.py84
-rw-r--r--releasenotes/notes/otlp-driver-cb932038ad580ac2.yaml8
-rw-r--r--test-requirements.txt2
11 files changed, 345 insertions, 4 deletions
diff --git a/devstack/lib/osprofiler b/devstack/lib/osprofiler
index 6dc7e0e..d5b4755 100644
--- a/devstack/lib/osprofiler
+++ b/devstack/lib/osprofiler
@@ -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'
}
@@ -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"}
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index 705d6ce..49cd670 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -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
diff --git a/doc/source/user/collectors.rst b/doc/source/user/collectors.rst
index a2c11e3..dd26326 100644
--- a/doc/source/user/collectors.rst
+++ b/doc/source/user/collectors.rst
@@ -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.
diff --git a/lower-constraints.txt b/lower-constraints.txt
index aafaad0..91c4dcd 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -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
diff --git a/osprofiler/_utils.py b/osprofiler/_utils.py
index 7563fb5..d903b9b 100644
--- a/osprofiler/_utils.py
+++ b/osprofiler/_utils.py
@@ -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
diff --git a/osprofiler/drivers/__init__.py b/osprofiler/drivers/__init__.py
index 022b094..6e3b93d 100644
--- a/osprofiler/drivers/__init__.py
+++ b/osprofiler/drivers/__init__.py
@@ -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
diff --git a/osprofiler/drivers/otlp.py b/osprofiler/drivers/otlp.py
new file mode 100644
index 0000000..c1d210b
--- /dev/null
+++ b/osprofiler/drivers/otlp.py
@@ -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
diff --git a/osprofiler/opts.py b/osprofiler/opts.py
index af8d4c4..b12e2b5 100644
--- a/osprofiler/opts.py
+++ b/osprofiler/opts.py
@@ -195,6 +195,22 @@ _JAEGER_OPTS = [
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,
@@ -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)]
diff --git a/osprofiler/tests/unit/drivers/test_otlp.py b/osprofiler/tests/unit/drivers/test_otlp.py
new file mode 100644
index 0000000..4162822
--- /dev/null
+++ b/osprofiler/tests/unit/drivers/test_otlp.py
@@ -0,0 +1,84 @@
+# 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.
+
+from unittest import mock
+
+from oslo_config import cfg
+
+from osprofiler.drivers import otlp
+from osprofiler import opts
+from osprofiler.tests import test
+
+
+class OTLPTestCase(test.TestCase):
+
+ def setUp(self):
+ super(OTLPTestCase, self).setUp()
+
+ opts.set_defaults(cfg.CONF)
+
+ self.payload_start = {
+ "name": "api-start",
+ "base_id": "4e3e0ec6-2938-40b1-8504-09eb1d4b0dee",
+ "trace_id": "1c089ea8-28fe-4f3d-8c00-f6daa2bc32f1",
+ "parent_id": "e2715537-3d1c-4f0c-b3af-87355dc5fc5b",
+ "timestamp": "2018-05-03T04:31:51.781381",
+ "info": {
+ "host": "test"
+ }
+ }
+
+ self.payload_stop = {
+ "name": "api-stop",
+ "base_id": "4e3e0ec6-2938-40b1-8504-09eb1d4b0dee",
+ "trace_id": "1c089ea8-28fe-4f3d-8c00-f6daa2bc32f1",
+ "parent_id": "e2715537-3d1c-4f0c-b3af-87355dc5fc5b",
+ "timestamp": "2018-05-03T04:31:51.781381",
+ "info": {
+ "host": "test",
+ "function": {
+ "result": 1
+ }
+ }
+ }
+
+ self.driver = otlp.OTLP(
+ "otlp://127.0.0.1:6831",
+ project="nova", service="api",
+ conf=cfg.CONF)
+
+ def test_notify_start(self):
+ self.driver.notify(self.payload_start)
+ self.assertEqual(1, len(self.driver.spans))
+
+ def test_notify_stop(self):
+ mock_end = mock.MagicMock()
+ self.driver.notify(self.payload_start)
+ self.driver.spans[0].end = mock_end
+ self.driver.notify(self.payload_stop)
+ mock_end.assert_called_once()
+
+ def test_service_name_default(self):
+ self.assertEqual("pr1-svc1", self.driver._get_service_name(
+ cfg.CONF, "pr1", "svc1"))
+
+ def test_service_name_prefix(self):
+ cfg.CONF.set_default(
+ "service_name_prefix", "prx1", "profiler_otlp")
+ self.assertEqual("prx1-pr1-svc1", self.driver._get_service_name(
+ cfg.CONF, "pr1", "svc1"))
+
+ def test_process_tags(self):
+ # Need to be implemented.
+ pass
diff --git a/releasenotes/notes/otlp-driver-cb932038ad580ac2.yaml b/releasenotes/notes/otlp-driver-cb932038ad580ac2.yaml
new file mode 100644
index 0000000..e1a0cfc
--- /dev/null
+++ b/releasenotes/notes/otlp-driver-cb932038ad580ac2.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ An OTLP (OpenTelemetry) exporter is now supported. The current
+ support is experimental but the aim is to deprecate and remove
+ legacy Jaeger driver which is using the already deprecated python
+ library jaeger client. Operators who want to use it should enable
+ `otlp`. OTLP is comptatible with Jaeger backend.
diff --git a/test-requirements.txt b/test-requirements.txt
index 3e1bbd8..096429d 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -20,5 +20,7 @@ redis>=2.10.0 # MIT
# For Jaeger Tracing
jaeger-client>=3.8.0 # Apache-2.0
+opentelemetry-exporter-otlp>=1.16.0
+opentelemetry-sdk>=1.16.0
pre-commit>=2.6.0 # MIT