Skip to content

Commit 8c360a1

Browse files
mauriciovasquezbernalAlex Boten
and
Alex Boten
authored
ext/pymongo: Add instrumentor (#612)
The current Pymongo integration uses the monitoring.register() [1] to hook the different internal calls of Pymongo. This integration doesn't allow to unregister a monitor. This commit workaround that limitation by adding an enable flag to the CommandTracer class and adds a logic to disable the integration. This solution is not perfect becasue there will be some overhead even when the instrumentation is disabled, but that's what we can do with the current approach. [1] https://api.mongodb.com/python/current/api/pymongo/monitoring.html#pymongo.monitoring.register Co-authored-by: Alex Boten <[email protected]>
1 parent 8e3ed35 commit 8c360a1

File tree

6 files changed

+98
-46
lines changed

6 files changed

+98
-46
lines changed

ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from pymongo import MongoClient
1818

1919
from opentelemetry import trace as trace_api
20-
from opentelemetry.ext.pymongo import trace_integration
20+
from opentelemetry.ext.pymongo import PymongoInstrumentor
2121
from opentelemetry.test.test_base import TestBase
2222

2323
MONGODB_HOST = os.getenv("MONGODB_HOST ", "localhost")
@@ -31,7 +31,7 @@ class TestFunctionalPymongo(TestBase):
3131
def setUpClass(cls):
3232
super().setUpClass()
3333
cls._tracer = cls.tracer_provider.get_tracer(__name__)
34-
trace_integration(cls.tracer_provider)
34+
PymongoInstrumentor().instrument()
3535
client = MongoClient(
3636
MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000
3737
)
@@ -94,3 +94,23 @@ def test_delete(self):
9494
with self._tracer.start_as_current_span("rootSpan"):
9595
self._collection.delete_one({"name": "testName"})
9696
self.validate_spans()
97+
98+
def test_uninstrument(self):
99+
# check that integration is working
100+
self._collection.find_one()
101+
spans = self.memory_exporter.get_finished_spans()
102+
self.memory_exporter.clear()
103+
self.assertEqual(len(spans), 1)
104+
105+
# uninstrument and check not new spans are created
106+
PymongoInstrumentor().uninstrument()
107+
self._collection.find_one()
108+
spans = self.memory_exporter.get_finished_spans()
109+
self.memory_exporter.clear()
110+
self.assertEqual(len(spans), 0)
111+
112+
# re-enable and check that it works again
113+
PymongoInstrumentor().instrument()
114+
self._collection.find_one()
115+
spans = self.memory_exporter.get_finished_spans()
116+
self.assertEqual(len(spans), 1)

ext/opentelemetry-ext-pymongo/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Implement instrumentor interface ([#612](https://github.com/open-telemetry/opentelemetry-python/pull/612))
6+
7+
58
## 0.4a0
69

710
Released 2020-02-21

ext/opentelemetry-ext-pymongo/setup.cfg

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ package_dir=
4141
packages=find_namespace:
4242
install_requires =
4343
opentelemetry-api == 0.7.dev0
44+
opentelemetry-auto-instrumentation == 0.7.dev0
4445
pymongo ~= 3.1
4546

4647
[options.extras_require]
@@ -49,3 +50,7 @@ test =
4950

5051
[options.packages.find]
5152
where = src
53+
54+
[options.entry_points]
55+
opentelemetry_instrumentor =
56+
pymongo = opentelemetry.ext.pymongo:PymongoInstrumentor

ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
# limitations under the License.
1414

1515
"""
16-
The integration with MongoDB supports the `pymongo`_ library and is specified
17-
to ``trace_integration`` using ``'pymongo'``.
16+
The integration with MongoDB supports the `pymongo`_ library, it can be
17+
enabled using the ``PymongoInstrumentor``.
1818
1919
.. _pymongo: https://pypi.org/project/pymongo
2020
@@ -26,11 +26,11 @@
2626
from pymongo import MongoClient
2727
from opentelemetry import trace
2828
from opentelemetry.trace import TracerProvider
29-
from opentelemetry.trace.ext.pymongo import trace_integration
29+
from opentelemetry.trace.ext.pymongo import PymongoInstrumentor
3030
3131
trace.set_tracer_provider(TracerProvider())
3232
33-
trace_integration()
33+
PymongoInstrumentor().instrument()
3434
client = MongoClient()
3535
db = client["MongoDB_Database"]
3636
collection = db["MongoDB_Collection"]
@@ -42,6 +42,8 @@
4242

4343
from pymongo import monitoring
4444

45+
from opentelemetry import trace
46+
from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
4547
from opentelemetry.ext.pymongo.version import __version__
4648
from opentelemetry.trace import SpanKind, get_tracer
4749
from opentelemetry.trace.status import Status, StatusCanonicalCode
@@ -50,27 +52,16 @@
5052
COMMAND_ATTRIBUTES = ["filter", "sort", "skip", "limit", "pipeline"]
5153

5254

53-
def trace_integration(tracer_provider=None):
54-
"""Integrate with pymongo to trace it using event listener.
55-
https://api.mongodb.com/python/current/api/pymongo/monitoring.html
56-
57-
Args:
58-
tracer_provider: The `TracerProvider` to use. If none is passed the
59-
current configured one is used.
60-
"""
61-
62-
tracer = get_tracer(__name__, __version__, tracer_provider)
63-
64-
monitoring.register(CommandTracer(tracer))
65-
66-
6755
class CommandTracer(monitoring.CommandListener):
6856
def __init__(self, tracer):
6957
self._tracer = tracer
7058
self._span_dict = {}
59+
self.is_enabled = True
7160

7261
def started(self, event: monitoring.CommandStartedEvent):
7362
""" Method to handle a pymongo CommandStartedEvent """
63+
if not self.is_enabled:
64+
return
7465
command = event.command.get(event.command_name, "")
7566
name = DATABASE_TYPE + "." + event.command_name
7667
statement = event.command_name
@@ -103,38 +94,70 @@ def started(self, event: monitoring.CommandStartedEvent):
10394
if span is not None:
10495
span.set_status(Status(StatusCanonicalCode.INTERNAL, str(ex)))
10596
span.end()
106-
self._remove_span(event)
97+
self._pop_span(event)
10798

10899
def succeeded(self, event: monitoring.CommandSucceededEvent):
109100
""" Method to handle a pymongo CommandSucceededEvent """
110-
span = self._get_span(event)
111-
if span is not None:
112-
span.set_attribute(
113-
"db.mongo.duration_micros", event.duration_micros
114-
)
115-
span.set_status(Status(StatusCanonicalCode.OK, event.reply))
116-
span.end()
117-
self._remove_span(event)
101+
if not self.is_enabled:
102+
return
103+
span = self._pop_span(event)
104+
if span is None:
105+
return
106+
span.set_attribute("db.mongo.duration_micros", event.duration_micros)
107+
span.set_status(Status(StatusCanonicalCode.OK, event.reply))
108+
span.end()
118109

119110
def failed(self, event: monitoring.CommandFailedEvent):
120111
""" Method to handle a pymongo CommandFailedEvent """
121-
span = self._get_span(event)
122-
if span is not None:
123-
span.set_attribute(
124-
"db.mongo.duration_micros", event.duration_micros
125-
)
126-
span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure))
127-
span.end()
128-
self._remove_span(event)
129-
130-
def _get_span(self, event):
131-
return self._span_dict.get(_get_span_dict_key(event))
112+
if not self.is_enabled:
113+
return
114+
span = self._pop_span(event)
115+
if span is None:
116+
return
117+
span.set_attribute("db.mongo.duration_micros", event.duration_micros)
118+
span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure))
119+
span.end()
132120

133-
def _remove_span(self, event):
134-
self._span_dict.pop(_get_span_dict_key(event))
121+
def _pop_span(self, event):
122+
return self._span_dict.pop(_get_span_dict_key(event), None)
135123

136124

137125
def _get_span_dict_key(event):
138126
if event.connection_id is not None:
139127
return (event.request_id, event.connection_id)
140128
return event.request_id
129+
130+
131+
class PymongoInstrumentor(BaseInstrumentor):
132+
_commandtracer_instance = None # type CommandTracer
133+
# The instrumentation for PyMongo is based on the event listener interface
134+
# https://api.mongodb.com/python/current/api/pymongo/monitoring.html.
135+
# This interface only allows to register listeners and does not provide
136+
# an unregister API. In order to provide a mechanishm to disable
137+
# instrumentation an enabled flag is implemented in CommandTracer,
138+
# it's checked in the different listeners.
139+
140+
def _instrument(self, **kwargs):
141+
"""Integrate with pymongo to trace it using event listener.
142+
https://api.mongodb.com/python/current/api/pymongo/monitoring.html
143+
144+
Args:
145+
tracer_provider: The `TracerProvider` to use. If none is passed the
146+
current configured one is used.
147+
"""
148+
149+
tracer_provider = kwargs.get("tracer_provider")
150+
151+
# Create and register a CommandTracer only the first time
152+
if self._commandtracer_instance is None:
153+
tracer = get_tracer(__name__, __version__, tracer_provider)
154+
155+
self._commandtracer_instance = CommandTracer(tracer)
156+
monitoring.register(self._commandtracer_instance)
157+
158+
# If already created, just enable it
159+
self._commandtracer_instance.is_enabled = True
160+
161+
def _uninstrument(self, **kwargs):
162+
if self._commandtracer_instance is not None:
163+
self._commandtracer_instance.is_enabled = False

ext/opentelemetry-ext-pymongo/tests/test_pymongo.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from unittest import mock
1616

1717
from opentelemetry import trace as trace_api
18-
from opentelemetry.ext.pymongo import CommandTracer, trace_integration
18+
from opentelemetry.ext.pymongo import CommandTracer, PymongoInstrumentor
1919
from opentelemetry.test.test_base import TestBase
2020

2121

@@ -24,13 +24,13 @@ def setUp(self):
2424
super().setUp()
2525
self.tracer = self.tracer_provider.get_tracer(__name__)
2626

27-
def test_trace_integration(self):
27+
def test_pymongo_instrumentor(self):
2828
mock_register = mock.Mock()
2929
patch = mock.patch(
3030
"pymongo.monitoring.register", side_effect=mock_register
3131
)
3232
with patch:
33-
trace_integration(self.tracer_provider)
33+
PymongoInstrumentor().instrument()
3434

3535
self.assertTrue(mock_register.called)
3636

@@ -50,7 +50,7 @@ def test_started(self):
5050
# the memory exporter can't be used here because the span isn't ended
5151
# yet
5252
# pylint: disable=protected-access
53-
span = command_tracer._get_span(mock_event)
53+
span = command_tracer._pop_span(mock_event)
5454
self.assertIs(span.kind, trace_api.SpanKind.CLIENT)
5555
self.assertEqual(span.name, "mongodb.command_name.find")
5656
self.assertEqual(span.attributes["component"], "mongodb")

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ commands_pre =
177177

178178
prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus
179179

180+
pymongo: pip install {toxinidir}/opentelemetry-auto-instrumentation
180181
pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo[test]
181182

182183
psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi

0 commit comments

Comments
 (0)