Codebase list python-osprofiler / 676a239
Add backward compatible drivers structure Change-Id: I3e904d0e456aa6999cd9a02a268f54e6d8b729de Spec: Multi backend support Alexey Yelistratov 7 years ago
16 changed file(s) with 559 addition(s) and 58 deletion(s). Raw diff Collapse all Expand all
1212 For help with syntax, see http://sphinx-doc.org/rest.html
1313 To test out your formatting, see http://www.tele3.cz/jbar/rest/rest.html
1414
15 ======================
16 Multi backend support
17 ======================
15 =====================
16 Multi backend support
17 =====================
1818
1919 Make OSProfiler more flexible and production ready.
2020
2121 Problem description
2222 ===================
2323
24 Currently OSprofiler works only with one backend Celiometer which actually
24 Currently OSprofiler works only with one backend Ceilometer which actually
2525 doesn't work well and adds huge overhead. More over often Ceilometer is not
2626 installed/used at all. To resolve this we should add support for different
2727 backends like: MongoDB, InfluxDB, ElasticSearch, ...
3131 ===============
3232
3333 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
3535 that can be processed by the REST lib.
3636
3737 Deprecate osprofiler.notifiers and osprofiler.parsers
4949 Assignee(s)
5050 -----------
5151
52 Primary assignee:
53 <launchpad-id or None>
52 Primary assignees:
53 dbelova
54 ayelistratov
5455
5556
5657 Work Items
7273 in the same place.
7374
7475 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.
7778
7879 These drivers will be chosen based on connection string
7980
8081 - Deprecate osprofiler.notifiers and osprofier.parsers
81
82 - Cut new release 0.4.2
8382
8483 - Switch all projects to new model with connection string
8584
8786 Dependencies
8887 ============
8988
90 - Cinder, Glance, Trove should be changed
89 - Cinder, Glance, Trove, Heat should be changed
1616 import os
1717
1818 from osprofiler.cmd import cliutils
19 from osprofiler.cmd import exc
19 from osprofiler import exc
2020 from osprofiler.parsers import ceilometer as ceiloparser
2121
2222
8989 with open(args.file_name, "w+") as output_file:
9090 output_file.write(output)
9191 else:
92 print (output)
92 print(output)
+0
-24
osprofiler/cmd/exc.py less more
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__
2525 import osprofiler
2626 from osprofiler.cmd import cliutils
2727 from osprofiler.cmd import commands
28 from osprofiler.cmd import exc
28 from osprofiler import exc
2929
3030
3131 class OSProfilerShell(object):
234234 try:
235235 OSProfilerShell(args)
236236 except exc.CommandError as e:
237 print (e.message)
237 print(e.message)
238238 return 1
239239
240240
0 from osprofiler.drivers import base # noqa
1 from osprofiler.drivers import messaging # noqa
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__
1212 # License for the specific language governing permissions and limitations
1313 # under the License.
1414
15 from osprofiler._notifiers import base
15 from osprofiler.drivers import base
1616
1717
1818 def _noop_notifier(info, context=None):
2121
2222 # NOTE(boris-42): By default we are using noop notifier.
2323 __notifier = _noop_notifier
24 __driver_cache = {}
2425
2526
2627 def notify(info):
4748 __notifier = notifier
4849
4950
50 def create(plugin_name, *args, **kwargs):
51 def create(connection_string, *args, **kwargs):
5152 """Create notifier based on specified plugin_name
5253
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
5658 :returns: Callable notifier method
5759 :raises TypeError: In case of invalid name of plugin raises TypeError
5860 """
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]
7777 ensures it can be used from client side to generate the trace, containing
7878 information from all possible resources.""")
7979
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
8093 _PROFILER_OPTS = [
8194 _enabled_opt,
8295 _trace_sqlalchemy_opt,
8396 _hmac_keys_opt,
97 _connection_string_opt,
8498 ]
8599
86100
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):
88103 conf.register_opts(_PROFILER_OPTS, group=_profiler_opt_group)
89104
90105 if enabled is not None:
95110 group=_profiler_opt_group.name)
96111 if hmac_keys is not None:
97112 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,
98117 group=_profiler_opt_group.name)
99118
100119
4444 % (attr_name, traced_times))
4545
4646
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):
4849 """Init profiler instance for current thread.
4950
5051 You should call profiler.init() before using osprofiler.
5354 :param hmac_key: secret key to sign trace information.
5455 :param base_id: Used to bind all related traces.
5556 :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
5661 :returns: Profiler instance
5762 """
5863 __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)
6067 return __local_ctx.profiler
6168
6269
318325
319326 class _Profiler(object):
320327
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):
322330 self.hmac_key = hmac_key
323331 if not base_id:
324332 base_id = str(uuid.uuid4())
325333 self._trace_stack = collections.deque([base_id, parent_id or base_id])
326334 self._name = collections.deque()
327335 self._host = socket.gethostname()
336 self._connection_str = connection_str
337 self._project = project
338 self._service = service
328339
329340 def get_base_id(self):
330341 """Return base id of a trace.
351362 parent_id - to build tree of events (not just a list)
352363 trace_id - current event id.
353364
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
358365 :param name: name of trace element (db, wsgi, rpc, etc..)
359366 :param info: Dictionary with any useful information related to this
360367 trace element. (sql request, rpc message or url...)
362369
363370 info = info or {}
364371 info["host"] = self._host
372 info["project"] = self._project
373 info["service"] = self._service
365374 self._name.append(name)
366375 self._trace_stack.append(str(uuid.uuid4()))
367376 self._notify("%s-start" % name, info)
368377
369378 def stop(self, info=None):
370 """Finish latests event.
379 """Finish latest event.
371380
372381 Same as a start, but instead of pushing trace_id to stack it pops it.
373382
375384 """
376385 info = info or {}
377386 info["host"] = self._host
387 info["project"] = self._project
388 info["service"] = self._service
378389 self._notify("%s-stop" % self._name.pop(), info)
379390 self._trace_stack.pop()
380391
1919 import mock
2020 import six
2121
22 from osprofiler.cmd import exc
2322 from osprofiler.cmd import shell
23 from osprofiler import exc
2424 from osprofiler.tests import test
2525
2626
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)
4242
4343 m.assert_called_once_with(10)
4444
45 @mock.patch("osprofiler.notifier.base.Notifier.factory")
45 @mock.patch("osprofiler.notifier.base.get_driver")
4646 def test_create(self, mock_factory):
4747
4848 result = notifier.create("test", 10, b=20)
4949 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)