Merge "OSprofiler with Jaeger Tracing as backend"
Zuul authored 5 years ago
Gerrit Code Review committed 5 years ago
0 | 0 | coverage===4.0 |
1 | 1 | ddt===1.0.1 |
2 | 2 | elasticsearch===2.0.0 |
3 | futures===3.0.0 | |
4 | jaeger-client==3.8.0 | |
3 | 5 | mock===2.0.0 |
4 | 6 | netaddr===0.7.18 |
5 | 7 | openstackdocstheme===1.18.1 |
17 | 17 | import hmac |
18 | 18 | import json |
19 | 19 | import os |
20 | import uuid | |
20 | 21 | |
21 | 22 | from oslo_utils import secretutils |
23 | from oslo_utils import uuidutils | |
22 | 24 | import six |
23 | 25 | |
24 | 26 | |
146 | 148 | new_package = ".".join(root.split(os.sep)).split("....")[1] |
147 | 149 | module_name = "%s.%s" % (new_package, filename[:-3]) |
148 | 150 | __import__(module_name) |
151 | ||
152 | ||
153 | def shorten_id(span_id): | |
154 | """Convert from uuid4 to 64 bit id for OpenTracing""" | |
155 | try: | |
156 | short_id = uuid.UUID(span_id).int & (1 << 64) - 1 | |
157 | except ValueError: | |
158 | # Return a new short id for this | |
159 | short_id = shorten_id(uuidutils.generate_uuid()) | |
160 | return short_id |
0 | 0 | from osprofiler.drivers import base # noqa |
1 | 1 | from osprofiler.drivers import elasticsearch_driver # noqa |
2 | from osprofiler.drivers import jaeger # noqa | |
2 | 3 | from osprofiler.drivers import loginsight # noqa |
3 | 4 | from osprofiler.drivers import messaging # noqa |
4 | 5 | from osprofiler.drivers import mongodb # noqa |
0 | # Copyright 2018 Fujitsu Ltd. | |
1 | # All Rights Reserved. | |
2 | # | |
3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | |
4 | # not use this file except in compliance with the License. You may obtain | |
5 | # a copy of the License at | |
6 | # | |
7 | # http://www.apache.org/licenses/LICENSE-2.0 | |
8 | # | |
9 | # Unless required by applicable law or agreed to in writing, software | |
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | # License for the specific language governing permissions and limitations | |
13 | # under the License. | |
14 | ||
15 | import collections | |
16 | import datetime | |
17 | import time | |
18 | ||
19 | from oslo_config import cfg | |
20 | from oslo_serialization import jsonutils | |
21 | import six.moves.urllib.parse as parser | |
22 | ||
23 | from osprofiler import _utils as utils | |
24 | from osprofiler.drivers import base | |
25 | from osprofiler import exc | |
26 | ||
27 | ||
28 | class Jaeger(base.Driver): | |
29 | def __init__(self, connection_str, project=None, service=None, host=None, | |
30 | conf=cfg.CONF, **kwargs): | |
31 | """Jaeger driver for OSProfiler.""" | |
32 | ||
33 | super(Jaeger, self).__init__(connection_str, project=project, | |
34 | service=service, host=host, | |
35 | conf=conf, **kwargs) | |
36 | try: | |
37 | import jaeger_client | |
38 | self.jaeger_client = jaeger_client | |
39 | except ImportError: | |
40 | raise exc.CommandError( | |
41 | "To use OSProfiler with Uber Jaeger tracer, " | |
42 | "you have to install `jaeger-client` manually. " | |
43 | "Install with pip:\n `pip install jaeger-client`." | |
44 | ) | |
45 | ||
46 | parsed_url = parser.urlparse(connection_str) | |
47 | cfg = { | |
48 | "local_agent": { | |
49 | "reporting_host": parsed_url.hostname, | |
50 | "reporting_port": parsed_url.port, | |
51 | } | |
52 | } | |
53 | ||
54 | # Initialize tracer for each profiler | |
55 | service_name = "{}-{}".format(project, service) | |
56 | config = jaeger_client.Config(cfg, service_name=service_name) | |
57 | self.tracer = config.initialize_tracer() | |
58 | ||
59 | self.spans = collections.deque() | |
60 | ||
61 | @classmethod | |
62 | def get_name(cls): | |
63 | return "jaeger" | |
64 | ||
65 | def notify(self, payload): | |
66 | if payload["name"].endswith("start"): | |
67 | timestamp = datetime.datetime.strptime(payload["timestamp"], | |
68 | "%Y-%m-%dT%H:%M:%S.%f") | |
69 | epoch = datetime.datetime.utcfromtimestamp(0) | |
70 | start_time = (timestamp - epoch).total_seconds() | |
71 | ||
72 | # Create parent span | |
73 | child_of = self.jaeger_client.SpanContext( | |
74 | trace_id=utils.shorten_id(payload["base_id"]), | |
75 | span_id=utils.shorten_id(payload["parent_id"]), | |
76 | parent_id=None, | |
77 | flags=self.jaeger_client.span.SAMPLED_FLAG | |
78 | ) | |
79 | ||
80 | # Create Jaeger Tracing span | |
81 | span = self.tracer.start_span( | |
82 | operation_name=payload["name"].rstrip("-start"), | |
83 | child_of=child_of, | |
84 | tags=self.create_span_tags(payload), | |
85 | start_time=start_time | |
86 | ) | |
87 | ||
88 | # Replace Jaeger Tracing span_id (random id) to OSProfiler span_id | |
89 | span.context.span_id = utils.shorten_id(payload["trace_id"]) | |
90 | self.spans.append(span) | |
91 | else: | |
92 | span = self.spans.pop() | |
93 | ||
94 | # Store result of db call and function call | |
95 | for call in ("db", "function"): | |
96 | if payload.get("info", {}).get(call) is not None: | |
97 | span.set_tag("result", payload["info"][call]["result"]) | |
98 | ||
99 | # Span error tag and log | |
100 | if payload["info"].get("etype") is not None: | |
101 | span.set_tag("error", True) | |
102 | span.log_kv({"error.kind": payload["info"]["etype"]}) | |
103 | span.log_kv({"message": payload["info"]["message"]}) | |
104 | ||
105 | span.finish(finish_time=time.time()) | |
106 | ||
107 | def get_report(self, base_id): | |
108 | """Please use Jaeger Tracing UI for this task.""" | |
109 | return self._parse_results() | |
110 | ||
111 | def list_traces(self, fields=None): | |
112 | """Please use Jaeger Tracing UI for this task.""" | |
113 | return [] | |
114 | ||
115 | def list_error_traces(self): | |
116 | """Please use Jaeger Tracing UI for this task.""" | |
117 | return [] | |
118 | ||
119 | def create_span_tags(self, payload): | |
120 | """Create tags for OpenTracing span. | |
121 | ||
122 | :param info: Information from OSProfiler trace. | |
123 | :returns tags: A dictionary contains standard tags | |
124 | from OpenTracing sematic conventions, | |
125 | and some other custom tags related to http, db calls. | |
126 | """ | |
127 | tags = {} | |
128 | info = payload["info"] | |
129 | ||
130 | if info.get("db"): | |
131 | # DB calls | |
132 | tags["db.statement"] = info["db"]["statement"] | |
133 | tags["db.params"] = jsonutils.dumps(info["db"]["params"]) | |
134 | elif info.get("request"): | |
135 | # WSGI call | |
136 | tags["http.path"] = info["request"]["path"] | |
137 | tags["http.query"] = info["request"]["query"] | |
138 | tags["http.method"] = info["request"]["method"] | |
139 | tags["http.scheme"] = info["request"]["scheme"] | |
140 | elif info.get("function"): | |
141 | # RPC, function calls | |
142 | tags["args"] = info["function"]["args"] | |
143 | tags["kwargs"] = info["function"]["kwargs"] | |
144 | tags["name"] = info["function"]["name"] | |
145 | ||
146 | return tags |
86 | 86 | |
87 | 87 | Examples of possible values: |
88 | 88 | |
89 | * messaging://: use oslo_messaging driver for sending notifications. | |
90 | * mongodb://127.0.0.1:27017 : use mongodb driver for sending notifications. | |
91 | * elasticsearch://127.0.0.1:9200 : use elasticsearch driver for sending | |
92 | notifications. | |
89 | * messaging:// - use oslo_messaging driver for sending spans. | |
90 | * redis://127.0.0.1:6379 - use redis driver for sending spans. | |
91 | * mongodb://127.0.0.1:27017 - use mongodb driver for sending spans. | |
92 | * elasticsearch://127.0.0.1:9200 - use elasticsearch driver for sending spans. | |
93 | * jaeger://127.0.0.1:6831 - use jaeger tracing as driver for sending spans. | |
93 | 94 | """) |
94 | 95 | |
95 | 96 | _es_doc_type_opt = cfg.StrOpt( |
22 | 22 | from oslo_utils import reflection |
23 | 23 | from oslo_utils import uuidutils |
24 | 24 | |
25 | from osprofiler import _utils as utils | |
25 | 26 | from osprofiler import notifier |
26 | 27 | |
27 | 28 | |
342 | 343 | |
343 | 344 | def __exit__(self, etype, value, traceback): |
344 | 345 | if etype: |
345 | info = {"etype": reflection.get_class_name(etype)} | |
346 | info = { | |
347 | "etype": reflection.get_class_name(etype), | |
348 | "message": value.args[0] if value.args else None | |
349 | } | |
346 | 350 | stop(info=info) |
347 | 351 | else: |
348 | 352 | stop() |
357 | 361 | self._trace_stack = collections.deque([base_id, parent_id or base_id]) |
358 | 362 | self._name = collections.deque() |
359 | 363 | self._host = socket.gethostname() |
364 | ||
365 | def get_shorten_id(self, uuid_id): | |
366 | """Return shorten id of a uuid that will be used in OpenTracing drivers | |
367 | ||
368 | :param uuid_id: A string of uuid that was generated by uuidutils | |
369 | :returns: A shorter 64-bit long id | |
370 | """ | |
371 | return format(utils.shorten_id(uuid_id), "x") | |
360 | 372 | |
361 | 373 | def get_base_id(self): |
362 | 374 | """Return base id of a trace. |
0 | # Copyright 2018 Fujitsu Ltd. | |
1 | # All Rights Reserved. | |
2 | # | |
3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | |
4 | # not use this file except in compliance with the License. You may obtain | |
5 | # a copy of the License at | |
6 | # | |
7 | # http://www.apache.org/licenses/LICENSE-2.0 | |
8 | # | |
9 | # Unless required by applicable law or agreed to in writing, software | |
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | # License for the specific language governing permissions and limitations | |
13 | # under the License. | |
14 | ||
15 | import mock | |
16 | ||
17 | from osprofiler.drivers import jaeger | |
18 | from osprofiler.tests import test | |
19 | ||
20 | ||
21 | class JaegerTestCase(test.TestCase): | |
22 | ||
23 | def setUp(self): | |
24 | super(JaegerTestCase, self).setUp() | |
25 | self.payload_start = { | |
26 | "name": "api-start", | |
27 | "base_id": "4e3e0ec6-2938-40b1-8504-09eb1d4b0dee", | |
28 | "trace_id": "1c089ea8-28fe-4f3d-8c00-f6daa2bc32f1", | |
29 | "parent_id": "e2715537-3d1c-4f0c-b3af-87355dc5fc5b", | |
30 | "timestamp": "2018-05-03T04:31:51.781381", | |
31 | "info": { | |
32 | "host": "test" | |
33 | } | |
34 | } | |
35 | ||
36 | self.payload_stop = { | |
37 | "name": "api-stop", | |
38 | "base_id": "4e3e0ec6-2938-40b1-8504-09eb1d4b0dee", | |
39 | "trace_id": "1c089ea8-28fe-4f3d-8c00-f6daa2bc32f1", | |
40 | "parent_id": "e2715537-3d1c-4f0c-b3af-87355dc5fc5b", | |
41 | "timestamp": "2018-05-03T04:31:51.781381", | |
42 | "info": { | |
43 | "host": "test", | |
44 | "function": { | |
45 | "result": 1 | |
46 | } | |
47 | } | |
48 | } | |
49 | ||
50 | self.driver = jaeger.Jaeger("jaeger://127.0.0.1:6831", | |
51 | project="nova", service="api") | |
52 | ||
53 | @mock.patch("osprofiler._utils.shorten_id") | |
54 | def test_notify_start(self, mock_shorten_id): | |
55 | self.driver.notify(self.payload_start) | |
56 | calls = [ | |
57 | mock.call(self.payload_start["base_id"]), | |
58 | mock.call(self.payload_start["parent_id"]), | |
59 | mock.call(self.payload_start["trace_id"]) | |
60 | ] | |
61 | mock_shorten_id.assert_has_calls(calls, any_order=True) | |
62 | ||
63 | @mock.patch("jaeger_client.span.Span") | |
64 | @mock.patch("time.time") | |
65 | def test_notify_stop(self, mock_time, mock_span): | |
66 | fake_time = 1525416065.5958152 | |
67 | mock_time.return_value = fake_time | |
68 | ||
69 | span = mock_span() | |
70 | self.driver.spans.append(mock_span()) | |
71 | ||
72 | self.driver.notify(self.payload_stop) | |
73 | ||
74 | mock_time.assert_called_once() | |
75 | mock_time.reset_mock() | |
76 | ||
77 | span.finish.assert_called_once_with(finish_time=fake_time) |
61 | 61 | |
62 | 62 | class ProfilerTestCase(test.TestCase): |
63 | 63 | |
64 | def test_profiler_get_shorten_id(self): | |
65 | uuid_id = "4e3e0ec6-2938-40b1-8504-09eb1d4b0dee" | |
66 | prof = profiler._Profiler("secret", base_id="1", parent_id="2") | |
67 | result = prof.get_shorten_id(uuid_id) | |
68 | expected = "850409eb1d4b0dee" | |
69 | self.assertEqual(expected, result) | |
70 | ||
64 | 71 | def test_profiler_get_base_id(self): |
65 | 72 | prof = profiler._Profiler("secret", base_id="1", parent_id="2") |
66 | 73 | self.assertEqual(prof.get_base_id(), "1") |
166 | 173 | |
167 | 174 | self.assertRaises(ValueError, foo) |
168 | 175 | mock_start.assert_called_once_with("foo", info=None) |
169 | mock_stop.assert_called_once_with(info={"etype": "ValueError"}) | |
176 | mock_stop.assert_called_once_with(info={ | |
177 | "etype": "ValueError", | |
178 | "message": "bar" | |
179 | }) | |
170 | 180 | |
171 | 181 | |
172 | 182 | @profiler.trace("function", info={"info": "some_info"}) |
15 | 15 | import base64 |
16 | 16 | import hashlib |
17 | 17 | import hmac |
18 | import uuid | |
18 | 19 | |
19 | 20 | import mock |
20 | 21 | |
110 | 111 | |
111 | 112 | self.assertIsNone(utils.signed_unpack(data, hmac_data, hmac)) |
112 | 113 | |
114 | def test_shorten_id_with_valid_uuid(self): | |
115 | valid_id = "4e3e0ec6-2938-40b1-8504-09eb1d4b0dee" | |
116 | ||
117 | uuid_obj = uuid.UUID(valid_id) | |
118 | ||
119 | with mock.patch("uuid.UUID") as mock_uuid: | |
120 | mock_uuid.return_value = uuid_obj | |
121 | ||
122 | result = utils.shorten_id(valid_id) | |
123 | expected = 9584796812364680686 | |
124 | ||
125 | self.assertEqual(expected, result) | |
126 | ||
127 | @mock.patch("oslo_utils.uuidutils.generate_uuid") | |
128 | def test_shorten_id_with_invalid_uuid(self, mock_gen_uuid): | |
129 | invalid_id = "invalid" | |
130 | mock_gen_uuid.return_value = "1c089ea8-28fe-4f3d-8c00-f6daa2bc32f1" | |
131 | ||
132 | result = utils.shorten_id(invalid_id) | |
133 | expected = 10088334584203457265 | |
134 | ||
135 | self.assertEqual(expected, result) | |
136 | ||
113 | 137 | def test_itersubclasses(self): |
114 | 138 | |
115 | 139 | class A(object): |