Add backward compatible drivers structure
Change-Id: I3e904d0e456aa6999cd9a02a268f54e6d8b729de
Spec: Multi backend support
Alexey Yelistratov
7 years ago
12 | 12 | For help with syntax, see http://sphinx-doc.org/rest.html |
13 | 13 | To test out your formatting, see http://www.tele3.cz/jbar/rest/rest.html |
14 | 14 | |
15 | ====================== | |
16 | Multi backend support | |
17 | ====================== | |
15 | ===================== | |
16 | Multi backend support | |
17 | ===================== | |
18 | 18 | |
19 | 19 | Make OSProfiler more flexible and production ready. |
20 | 20 | |
21 | 21 | Problem description |
22 | 22 | =================== |
23 | 23 | |
24 | Currently OSprofiler works only with one backend Celiometer which actually | |
24 | Currently OSprofiler works only with one backend Ceilometer which actually | |
25 | 25 | doesn't work well and adds huge overhead. More over often Ceilometer is not |
26 | 26 | installed/used at all. To resolve this we should add support for different |
27 | 27 | backends like: MongoDB, InfluxDB, ElasticSearch, ... |
31 | 31 | =============== |
32 | 32 | |
33 | 33 | And new osprofiler.drivers mechanism, each driver will do 2 things: |
34 | send notifications and parse all notification in unififed tree strcture | |
34 | send notifications and parse all notification in unified tree structure | |
35 | 35 | that can be processed by the REST lib. |
36 | 36 | |
37 | 37 | Deprecate osprofiler.notifiers and osprofiler.parsers |
49 | 49 | Assignee(s) |
50 | 50 | ----------- |
51 | 51 | |
52 | Primary assignee: | |
53 | <launchpad-id or None> | |
52 | Primary assignees: | |
53 | dbelova | |
54 | ayelistratov | |
54 | 55 | |
55 | 56 | |
56 | 57 | Work Items |
72 | 73 | in the same place. |
73 | 74 | |
74 | 75 | This change should be done with keeping backward compatiblity, in other words |
75 | we should create separated direcotory osprofier.drivers and put first | |
76 | Ceilometer and then start working on other backends. | |
76 | we should create separated directory osprofier.drivers and put first | |
77 | Ceilometer and then start working on other backends. | |
77 | 78 | |
78 | 79 | These drivers will be chosen based on connection string |
79 | 80 | |
80 | 81 | - Deprecate osprofiler.notifiers and osprofier.parsers |
81 | ||
82 | - Cut new release 0.4.2 | |
83 | 82 | |
84 | 83 | - Switch all projects to new model with connection string |
85 | 84 | |
87 | 86 | Dependencies |
88 | 87 | ============ |
89 | 88 | |
90 | - Cinder, Glance, Trove should be changed | |
89 | - Cinder, Glance, Trove, Heat should be changed |
16 | 16 | import os |
17 | 17 | |
18 | 18 | from osprofiler.cmd import cliutils |
19 | from osprofiler.cmd import exc | |
19 | from osprofiler import exc | |
20 | 20 | from osprofiler.parsers import ceilometer as ceiloparser |
21 | 21 | |
22 | 22 | |
89 | 89 | with open(args.file_name, "w+") as output_file: |
90 | 90 | output_file.write(output) |
91 | 91 | else: |
92 | print (output) | |
92 | print(output) |
0 | # Copyright 2014 Mirantis Inc. | |
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 | ||
16 | class CommandError(Exception): | |
17 | """Invalid usage of CLI.""" | |
18 | ||
19 | def __init__(self, message=None): | |
20 | self.message = message | |
21 | ||
22 | def __str__(self): | |
23 | return self.message or self.__class__.__doc__ |
25 | 25 | import osprofiler |
26 | 26 | from osprofiler.cmd import cliutils |
27 | 27 | from osprofiler.cmd import commands |
28 | from osprofiler.cmd import exc | |
28 | from osprofiler import exc | |
29 | 29 | |
30 | 30 | |
31 | 31 | class OSProfilerShell(object): |
234 | 234 | try: |
235 | 235 | OSProfilerShell(args) |
236 | 236 | except exc.CommandError as e: |
237 | print (e.message) | |
237 | print(e.message) | |
238 | 238 | return 1 |
239 | 239 | |
240 | 240 |
0 | # Copyright 2016 Mirantis Inc. | |
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 datetime | |
16 | import logging as log | |
17 | ||
18 | import six.moves.urllib.parse as urlparse | |
19 | ||
20 | from osprofiler import _utils | |
21 | ||
22 | LOG = log.getLogger(__name__) | |
23 | ||
24 | ||
25 | def get_driver(connection_string, *args, **kwargs): | |
26 | """Create driver's instance according to specified connection string""" | |
27 | # NOTE(ayelistratov) Backward compatibility with old Messaging notation | |
28 | # Remove after patching all OS services | |
29 | # NOTE(ishakhat) Raise exception when ParsedResult.scheme is empty | |
30 | if "://" not in connection_string: | |
31 | connection_string += "://" | |
32 | ||
33 | parsed_connection = urlparse.urlparse(connection_string) | |
34 | LOG.debug("String %s looks like a connection string, trying it.", | |
35 | connection_string) | |
36 | ||
37 | backend = parsed_connection.scheme | |
38 | for driver in _utils.itersubclasses(Driver): | |
39 | if backend == driver.get_name(): | |
40 | return driver(connection_string, *args, **kwargs) | |
41 | ||
42 | raise ValueError("Driver not found for connection string: " | |
43 | "%s" % connection_string) | |
44 | ||
45 | ||
46 | class Driver(object): | |
47 | """Base Driver class. | |
48 | ||
49 | This class provides protected common methods that | |
50 | do not rely on a specific storage backend. Public methods notify() and/or | |
51 | get_report(), which require using storage backend API, must be overridden | |
52 | and implemented by any class derived from this class. | |
53 | """ | |
54 | ||
55 | def __init__(self, connection_str, project=None, service=None, host=None): | |
56 | self.connection_str = connection_str | |
57 | self.project = project | |
58 | self.service = service | |
59 | self.host = host | |
60 | self.result = {} | |
61 | self.started_at = None | |
62 | self.finished_at = None | |
63 | ||
64 | def notify(self, info, **kwargs): | |
65 | """This method will be called on each notifier.notify() call. | |
66 | ||
67 | To add new drivers you should, create new subclass of this class and | |
68 | implement notify method. | |
69 | ||
70 | :param info: Contains information about trace element. | |
71 | In payload dict there are always 3 ids: | |
72 | "base_id" - uuid that is common for all notifications | |
73 | related to one trace. Used to simplify | |
74 | retrieving of all trace elements from | |
75 | the backend. | |
76 | "parent_id" - uuid of parent element in trace | |
77 | "trace_id" - uuid of current element in trace | |
78 | ||
79 | With parent_id and trace_id it's quite simple to build | |
80 | tree of trace elements, which simplify analyze of trace. | |
81 | ||
82 | """ | |
83 | raise NotImplementedError("{0}: This method is either not supported " | |
84 | "or has to be overridden".format( | |
85 | self.get_name())) | |
86 | ||
87 | def get_report(self, base_id): | |
88 | """Forms and returns report composed from the stored notifications. | |
89 | ||
90 | :param base_id: Base id of trace elements. | |
91 | """ | |
92 | raise NotImplementedError("{0}: This method is either not supported " | |
93 | "or has to be overridden".format( | |
94 | self.get_name())) | |
95 | ||
96 | @classmethod | |
97 | def get_name(cls): | |
98 | """Returns backend specific name for the driver.""" | |
99 | return cls.__name__ | |
100 | ||
101 | def list_traces(self, query, fields): | |
102 | """Returns array of all base_id fields that match the given criteria | |
103 | ||
104 | :param query: dict that specifies the query criteria | |
105 | :param fields: iterable of strings that specifies the output fields | |
106 | """ | |
107 | raise NotImplementedError("{0}: This method is either not supported " | |
108 | "or has to be overridden".format( | |
109 | self.get_name())) | |
110 | ||
111 | @staticmethod | |
112 | def _build_tree(nodes): | |
113 | """Builds the tree (forest) data structure based on the list of nodes. | |
114 | ||
115 | Tree building works in O(n*log(n)). | |
116 | ||
117 | :param nodes: dict of nodes, where each node is a dictionary with fields | |
118 | "parent_id", "trace_id", "info" | |
119 | :returns: list of top level ("root") nodes in form of dictionaries, | |
120 | each containing the "info" and "children" fields, where | |
121 | "children" is the list of child nodes ("children" will be | |
122 | empty for leafs) | |
123 | """ | |
124 | ||
125 | tree = [] | |
126 | ||
127 | for trace_id in nodes: | |
128 | node = nodes[trace_id] | |
129 | node.setdefault("children", []) | |
130 | parent_id = node["parent_id"] | |
131 | if parent_id in nodes: | |
132 | nodes[parent_id].setdefault("children", []) | |
133 | nodes[parent_id]["children"].append(node) | |
134 | else: | |
135 | tree.append(node) # no parent => top-level node | |
136 | ||
137 | for trace_id in nodes: | |
138 | nodes[trace_id]["children"].sort( | |
139 | key=lambda x: x["info"]["started"]) | |
140 | ||
141 | return sorted(tree, key=lambda x: x["info"]["started"]) | |
142 | ||
143 | def _append_results(self, trace_id, parent_id, name, project, service, | |
144 | host, timestamp, raw_payload=None): | |
145 | """Appends the notification to the dictionary of notifications. | |
146 | ||
147 | :param trace_id: UUID of current trace point | |
148 | :param parent_id: UUID of parent trace point | |
149 | :param name: name of operation | |
150 | :param project: project name | |
151 | :param service: service name | |
152 | :param host: host name or FQDN | |
153 | :param timestamp: Unicode-style timestamp matching the pattern | |
154 | "%Y-%m-%dT%H:%M:%S.%f" , e.g. 2016-04-18T17:42:10.77 | |
155 | :param raw_payload: raw notification without any filtering, with all | |
156 | fields included | |
157 | """ | |
158 | timestamp = datetime.datetime.strptime(timestamp, | |
159 | "%Y-%m-%dT%H:%M:%S.%f") | |
160 | if trace_id not in self.result: | |
161 | self.result[trace_id] = { | |
162 | "info": { | |
163 | "name": name.split("-")[0], | |
164 | "project": project, | |
165 | "service": service, | |
166 | "host": host, | |
167 | }, | |
168 | "trace_id": trace_id, | |
169 | "parent_id": parent_id, | |
170 | } | |
171 | ||
172 | self.result[trace_id]["info"]["meta.raw_payload.%s" | |
173 | % name] = raw_payload | |
174 | ||
175 | if name.endswith("stop"): | |
176 | self.result[trace_id]["info"]["finished"] = timestamp | |
177 | else: | |
178 | self.result[trace_id]["info"]["started"] = timestamp | |
179 | ||
180 | if not self.started_at or self.started_at > timestamp: | |
181 | self.started_at = timestamp | |
182 | ||
183 | if not self.finished_at or self.finished_at < timestamp: | |
184 | self.finished_at = timestamp | |
185 | ||
186 | def _parse_results(self): | |
187 | """Parses Driver's notifications placed by _append_results() . | |
188 | ||
189 | :returns: full profiling report | |
190 | """ | |
191 | ||
192 | def msec(dt): | |
193 | # NOTE(boris-42): Unfortunately this is the simplest way that works | |
194 | # in py26 and py27 | |
195 | microsec = (dt.microseconds + (dt.seconds + dt.days * 24 * 3600) * | |
196 | 1e6) | |
197 | return int(microsec / 1000.0) | |
198 | ||
199 | for r in self.result.values(): | |
200 | # NOTE(boris-42): We are not able to guarantee that the backend | |
201 | # consumed all messages => so we should at make duration 0ms. | |
202 | ||
203 | if "started" not in r["info"]: | |
204 | r["info"]["started"] = r["info"]["finished"] | |
205 | if "finished" not in r["info"]: | |
206 | r["info"]["finished"] = r["info"]["started"] | |
207 | ||
208 | r["info"]["started"] = msec(r["info"]["started"] - self.started_at) | |
209 | r["info"]["finished"] = msec(r["info"]["finished"] - | |
210 | self.started_at) | |
211 | ||
212 | return { | |
213 | "info": { | |
214 | "name": "total", | |
215 | "started": 0, | |
216 | "finished": msec(self.finished_at - | |
217 | self.started_at) if self.started_at else None | |
218 | }, | |
219 | "children": self._build_tree(self.result) | |
220 | } |
0 | # Copyright 2016 Mirantis Inc. | |
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 | from osprofiler.drivers import base | |
16 | ||
17 | ||
18 | class Messaging(base.Driver): | |
19 | def __init__(self, connection_str, messaging=None, context=None, | |
20 | transport=None, project=None, service=None, | |
21 | host=None, **kwargs): | |
22 | """Driver sending notifications via message queues.""" | |
23 | ||
24 | super(Messaging, self).__init__(connection_str, project=project, | |
25 | service=service, host=host) | |
26 | ||
27 | self.messaging = messaging | |
28 | self.context = context | |
29 | ||
30 | self.client = messaging.Notifier( | |
31 | transport, publisher_id=self.host, driver="messaging", | |
32 | topic="profiler", retry=0) | |
33 | ||
34 | @classmethod | |
35 | def get_name(cls): | |
36 | return "messaging" | |
37 | ||
38 | def notify(self, info, context=None): | |
39 | """Send notifications to backend via oslo.messaging notifier API. | |
40 | ||
41 | :param info: Contains information about trace element. | |
42 | In payload dict there are always 3 ids: | |
43 | "base_id" - uuid that is common for all notifications | |
44 | related to one trace. Used to simplify | |
45 | retrieving of all trace elements from | |
46 | Ceilometer. | |
47 | "parent_id" - uuid of parent element in trace | |
48 | "trace_id" - uuid of current element in trace | |
49 | ||
50 | With parent_id and trace_id it's quite simple to build | |
51 | tree of trace elements, which simplify analyze of trace. | |
52 | ||
53 | :param context: request context that is mostly used to specify | |
54 | current active user and tenant. | |
55 | """ | |
56 | ||
57 | info["project"] = self.project | |
58 | info["service"] = self.service | |
59 | self.client.info(context or self.context, | |
60 | "profiler.%s" % info["service"], | |
61 | info) |
0 | # Copyright 2014 Mirantis Inc. | |
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 | ||
16 | class CommandError(Exception): | |
17 | """Invalid usage of CLI.""" | |
18 | ||
19 | def __init__(self, message=None): | |
20 | self.message = message | |
21 | ||
22 | def __str__(self): | |
23 | return self.message or self.__class__.__doc__ |
12 | 12 | # License for the specific language governing permissions and limitations |
13 | 13 | # under the License. |
14 | 14 | |
15 | from osprofiler._notifiers import base | |
15 | from osprofiler.drivers import base | |
16 | 16 | |
17 | 17 | |
18 | 18 | def _noop_notifier(info, context=None): |
21 | 21 | |
22 | 22 | # NOTE(boris-42): By default we are using noop notifier. |
23 | 23 | __notifier = _noop_notifier |
24 | __driver_cache = {} | |
24 | 25 | |
25 | 26 | |
26 | 27 | def notify(info): |
47 | 48 | __notifier = notifier |
48 | 49 | |
49 | 50 | |
50 | def create(plugin_name, *args, **kwargs): | |
51 | def create(connection_string, *args, **kwargs): | |
51 | 52 | """Create notifier based on specified plugin_name |
52 | 53 | |
53 | :param plugin_name: Name of plugin that creates notifier | |
54 | :param *args: args that will be passed to plugin init method | |
55 | :param **kwargs: kwargs that will be passed to plugin init method | |
54 | :param connection_string: connection string which specifies the storage | |
55 | driver for notifier | |
56 | :param *args: args that will be passed to the driver's __init__ method | |
57 | :param **kwargs: kwargs that will be passed to the driver's __init__ method | |
56 | 58 | :returns: Callable notifier method |
57 | 59 | :raises TypeError: In case of invalid name of plugin raises TypeError |
58 | 60 | """ |
59 | return base.Notifier.factory(plugin_name, *args, **kwargs) | |
61 | global __driver_cache | |
62 | if connection_string not in __driver_cache: | |
63 | __driver_cache[connection_string] = base.get_driver(connection_string, | |
64 | *args, | |
65 | **kwargs).notify | |
66 | return __driver_cache[connection_string] |
77 | 77 | ensures it can be used from client side to generate the trace, containing |
78 | 78 | information from all possible resources.""") |
79 | 79 | |
80 | _connection_string_opt = cfg.StrOpt( | |
81 | "connection_string", | |
82 | default="messaging://", | |
83 | help=""" | |
84 | Connection string for a notifier backend. Default value is messaging:// which | |
85 | sets the notifier to oslo_messaging. | |
86 | ||
87 | Examples of possible values: | |
88 | ||
89 | * messaging://: use oslo_messaging driver for sending notifications. | |
90 | """) | |
91 | ||
92 | ||
80 | 93 | _PROFILER_OPTS = [ |
81 | 94 | _enabled_opt, |
82 | 95 | _trace_sqlalchemy_opt, |
83 | 96 | _hmac_keys_opt, |
97 | _connection_string_opt, | |
84 | 98 | ] |
85 | 99 | |
86 | 100 | |
87 | def set_defaults(conf, enabled=None, trace_sqlalchemy=None, hmac_keys=None): | |
101 | def set_defaults(conf, enabled=None, trace_sqlalchemy=None, hmac_keys=None, | |
102 | connection_string=None): | |
88 | 103 | conf.register_opts(_PROFILER_OPTS, group=_profiler_opt_group) |
89 | 104 | |
90 | 105 | if enabled is not None: |
95 | 110 | group=_profiler_opt_group.name) |
96 | 111 | if hmac_keys is not None: |
97 | 112 | conf.set_default("hmac_keys", hmac_keys, |
113 | group=_profiler_opt_group.name) | |
114 | ||
115 | if connection_string is not None: | |
116 | conf.set_default("connection_string", connection_string, | |
98 | 117 | group=_profiler_opt_group.name) |
99 | 118 | |
100 | 119 |
44 | 44 | % (attr_name, traced_times)) |
45 | 45 | |
46 | 46 | |
47 | def init(hmac_key, base_id=None, parent_id=None): | |
47 | def init(hmac_key, base_id=None, parent_id=None, connection_str=None, | |
48 | project=None, service=None): | |
48 | 49 | """Init profiler instance for current thread. |
49 | 50 | |
50 | 51 | You should call profiler.init() before using osprofiler. |
53 | 54 | :param hmac_key: secret key to sign trace information. |
54 | 55 | :param base_id: Used to bind all related traces. |
55 | 56 | :param parent_id: Used to build tree of traces. |
57 | :param connection_str: Connection string to the backend to use for | |
58 | notifications. | |
59 | :param project: Project name that is under profiling | |
60 | :param service: Service name that is under profiling | |
56 | 61 | :returns: Profiler instance |
57 | 62 | """ |
58 | 63 | __local_ctx.profiler = _Profiler(hmac_key, base_id=base_id, |
59 | parent_id=parent_id) | |
64 | parent_id=parent_id, | |
65 | connection_str=connection_str, | |
66 | project=project, service=service) | |
60 | 67 | return __local_ctx.profiler |
61 | 68 | |
62 | 69 | |
318 | 325 | |
319 | 326 | class _Profiler(object): |
320 | 327 | |
321 | def __init__(self, hmac_key, base_id=None, parent_id=None): | |
328 | def __init__(self, hmac_key, base_id=None, parent_id=None, | |
329 | connection_str=None, project=None, service=None): | |
322 | 330 | self.hmac_key = hmac_key |
323 | 331 | if not base_id: |
324 | 332 | base_id = str(uuid.uuid4()) |
325 | 333 | self._trace_stack = collections.deque([base_id, parent_id or base_id]) |
326 | 334 | self._name = collections.deque() |
327 | 335 | self._host = socket.gethostname() |
336 | self._connection_str = connection_str | |
337 | self._project = project | |
338 | self._service = service | |
328 | 339 | |
329 | 340 | def get_base_id(self): |
330 | 341 | """Return base id of a trace. |
351 | 362 | parent_id - to build tree of events (not just a list) |
352 | 363 | trace_id - current event id. |
353 | 364 | |
354 | As we are writing this code special for OpenStack, and there will be | |
355 | only one implementation of notifier based on ceilometer notifier api. | |
356 | That already contains timestamps, so we don't measure time by hand. | |
357 | ||
358 | 365 | :param name: name of trace element (db, wsgi, rpc, etc..) |
359 | 366 | :param info: Dictionary with any useful information related to this |
360 | 367 | trace element. (sql request, rpc message or url...) |
362 | 369 | |
363 | 370 | info = info or {} |
364 | 371 | info["host"] = self._host |
372 | info["project"] = self._project | |
373 | info["service"] = self._service | |
365 | 374 | self._name.append(name) |
366 | 375 | self._trace_stack.append(str(uuid.uuid4())) |
367 | 376 | self._notify("%s-start" % name, info) |
368 | 377 | |
369 | 378 | def stop(self, info=None): |
370 | """Finish latests event. | |
379 | """Finish latest event. | |
371 | 380 | |
372 | 381 | Same as a start, but instead of pushing trace_id to stack it pops it. |
373 | 382 | |
375 | 384 | """ |
376 | 385 | info = info or {} |
377 | 386 | info["host"] = self._host |
387 | info["project"] = self._project | |
388 | info["service"] = self._service | |
378 | 389 | self._notify("%s-stop" % self._name.pop(), info) |
379 | 390 | self._trace_stack.pop() |
380 | 391 |
19 | 19 | import mock |
20 | 20 | import six |
21 | 21 | |
22 | from osprofiler.cmd import exc | |
23 | 22 | from osprofiler.cmd import shell |
23 | from osprofiler import exc | |
24 | 24 | from osprofiler.tests import test |
25 | 25 | |
26 | 26 |
0 | # Copyright 2016 Mirantis Inc. | |
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 base | |
18 | from osprofiler.tests import test | |
19 | ||
20 | ||
21 | class NotifierBaseTestCase(test.TestCase): | |
22 | ||
23 | def test_factory(self): | |
24 | ||
25 | class A(base.Driver): | |
26 | @classmethod | |
27 | def get_name(cls): | |
28 | return "a" | |
29 | ||
30 | def notify(self, a): | |
31 | return a | |
32 | ||
33 | self.assertEqual(10, base.get_driver("a://").notify(10)) | |
34 | ||
35 | def test_factory_with_args(self): | |
36 | ||
37 | class B(base.Driver): | |
38 | ||
39 | def __init__(self, c_str, a, b=10): | |
40 | self.a = a | |
41 | self.b = b | |
42 | ||
43 | @classmethod | |
44 | def get_name(cls): | |
45 | return "b" | |
46 | ||
47 | def notify(self, c): | |
48 | return self.a + self.b + c | |
49 | ||
50 | self.assertEqual(22, base.get_driver("b://", 5, b=7).notify(10)) | |
51 | ||
52 | def test_driver_not_found(self): | |
53 | self.assertRaises(ValueError, base.get_driver, | |
54 | "Driver not found for connection string: " | |
55 | "nonexisting://") | |
56 | ||
57 | def test_plugins_are_imported(self): | |
58 | base.get_driver("messaging://", mock.MagicMock(), "context", | |
59 | "transport", "host") | |
60 | ||
61 | def test_build_empty_tree(self): | |
62 | class C(base.Driver): | |
63 | @classmethod | |
64 | def get_name(cls): | |
65 | return "c" | |
66 | ||
67 | self.assertEqual([], base.get_driver("c://")._build_tree({})) | |
68 | ||
69 | def test_build_complex_tree(self): | |
70 | class D(base.Driver): | |
71 | @classmethod | |
72 | def get_name(cls): | |
73 | return "d" | |
74 | ||
75 | test_input = { | |
76 | "2": {"parent_id": "0", "trace_id": "2", "info": {"started": 1}}, | |
77 | "1": {"parent_id": "0", "trace_id": "1", "info": {"started": 0}}, | |
78 | "21": {"parent_id": "2", "trace_id": "21", "info": {"started": 6}}, | |
79 | "22": {"parent_id": "2", "trace_id": "22", "info": {"started": 7}}, | |
80 | "11": {"parent_id": "1", "trace_id": "11", "info": {"started": 1}}, | |
81 | "113": {"parent_id": "11", "trace_id": "113", | |
82 | "info": {"started": 3}}, | |
83 | "112": {"parent_id": "11", "trace_id": "112", | |
84 | "info": {"started": 2}}, | |
85 | "114": {"parent_id": "11", "trace_id": "114", | |
86 | "info": {"started": 5}} | |
87 | } | |
88 | ||
89 | expected_output = [ | |
90 | { | |
91 | "parent_id": "0", | |
92 | "trace_id": "1", | |
93 | "info": {"started": 0}, | |
94 | "children": [ | |
95 | { | |
96 | "parent_id": "1", | |
97 | "trace_id": "11", | |
98 | "info": {"started": 1}, | |
99 | "children": [ | |
100 | {"parent_id": "11", "trace_id": "112", | |
101 | "info": {"started": 2}, "children": []}, | |
102 | {"parent_id": "11", "trace_id": "113", | |
103 | "info": {"started": 3}, "children": []}, | |
104 | {"parent_id": "11", "trace_id": "114", | |
105 | "info": {"started": 5}, "children": []} | |
106 | ] | |
107 | } | |
108 | ] | |
109 | }, | |
110 | { | |
111 | "parent_id": "0", | |
112 | "trace_id": "2", | |
113 | "info": {"started": 1}, | |
114 | "children": [ | |
115 | {"parent_id": "2", "trace_id": "21", | |
116 | "info": {"started": 6}, "children": []}, | |
117 | {"parent_id": "2", "trace_id": "22", | |
118 | "info": {"started": 7}, "children": []} | |
119 | ] | |
120 | } | |
121 | ] | |
122 | ||
123 | self.assertEqual( | |
124 | expected_output, base.get_driver("d://")._build_tree(test_input)) |
0 | # Copyright 2016 Mirantis Inc. | |
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 base | |
18 | from osprofiler.tests import test | |
19 | ||
20 | ||
21 | class MessagingTestCase(test.TestCase): | |
22 | ||
23 | def test_init_and_notify(self): | |
24 | ||
25 | messaging = mock.MagicMock() | |
26 | context = "context" | |
27 | transport = "transport" | |
28 | project = "project" | |
29 | service = "service" | |
30 | host = "host" | |
31 | ||
32 | notify_func = base.get_driver( | |
33 | "messaging://", messaging, context, transport, | |
34 | project, service, host).notify | |
35 | ||
36 | messaging.Notifier.assert_called_once_with( | |
37 | transport, publisher_id=host, driver="messaging", | |
38 | topic="profiler", retry=0) | |
39 | ||
40 | info = { | |
41 | "a": 10, | |
42 | "project": project, | |
43 | "service": service, | |
44 | "host": host | |
45 | } | |
46 | notify_func(info) | |
47 | ||
48 | messaging.Notifier().info.assert_called_once_with( | |
49 | context, "profiler.service", info) | |
50 | ||
51 | messaging.reset_mock() | |
52 | notify_func(info, context="my_context") | |
53 | messaging.Notifier().info.assert_called_once_with( | |
54 | "my_context", "profiler.service", info) |
42 | 42 | |
43 | 43 | m.assert_called_once_with(10) |
44 | 44 | |
45 | @mock.patch("osprofiler.notifier.base.Notifier.factory") | |
45 | @mock.patch("osprofiler.notifier.base.get_driver") | |
46 | 46 | def test_create(self, mock_factory): |
47 | 47 | |
48 | 48 | result = notifier.create("test", 10, b=20) |
49 | 49 | mock_factory.assert_called_once_with("test", 10, b=20) |
50 | self.assertEqual(mock_factory.return_value, result) | |
50 | self.assertEqual(mock_factory.return_value.notify, result) |