Skip to content

Commit 760673f

Browse files
pymongo instrumentation hooks (#793)
* pymongo instrumentation hooks * update PR number
1 parent 5993329 commit 760673f

File tree

3 files changed

+106
-8
lines changed

3 files changed

+106
-8
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
([#781](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/781))
1515
- `opentelemetry-instrumentation-aws-lambda` Add instrumentation for AWS Lambda Service - Implementation (Part 2/2)
1616
([#777](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/777))
17+
- `opentelemetry-instrumentation-pymongo` Add `request_hook`, `response_hook` and `failed_hook` callbacks passed as arguments to the instrument method
18+
([#793](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/793))
1719
- `opentelemetry-instrumentation-pymysql` Add support for PyMySQL 1.x series
1820
([#792](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/792))
1921

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

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from pymongo import MongoClient
2727
from opentelemetry.instrumentation.pymongo import PymongoInstrumentor
2828
29-
3029
PymongoInstrumentor().instrument()
3130
client = MongoClient()
3231
db = client["MongoDB_Database"]
@@ -35,9 +34,47 @@
3534
3635
API
3736
---
38-
"""
37+
The `instrument` method accepts the following keyword args:
38+
39+
tracer_provider (TracerProvider) - an optional tracer provider
40+
request_hook (Callable) -
41+
a function with extra user-defined logic to be performed before querying mongodb
42+
this function signature is: def request_hook(span: Span, event: CommandStartedEvent) -> None
43+
response_hook (Callable) -
44+
a function with extra user-defined logic to be performed after the query returns with a successful response
45+
this function signature is: def response_hook(span: Span, event: CommandSucceededEvent) -> None
46+
failed_hook (Callable) -
47+
a function with extra user-defined logic to be performed after the query returns with a failed response
48+
this function signature is: def failed_hook(span: Span, event: CommandFailedEvent) -> None
49+
50+
for example:
51+
52+
.. code: python
53+
54+
from opentelemetry.instrumentation.pymongo import PymongoInstrumentor
55+
from pymongo import MongoClient
56+
57+
def request_hook(span, event):
58+
# request hook logic
3959
40-
from typing import Collection
60+
def response_hook(span, event):
61+
# response hook logic
62+
63+
def failed_hook(span, event):
64+
# failed hook logic
65+
66+
# Instrument pymongo with hooks
67+
PymongoInstrumentor().instrument(request_hook=request_hook, response_hooks=response_hook, failed_hook=failed_hook)
68+
69+
# This will create a span with pymongo specific attributes, including custom attributes added from the hooks
70+
client = MongoClient()
71+
db = client["MongoDB_Database"]
72+
collection = db["MongoDB_Collection"]
73+
collection.find_one()
74+
75+
"""
76+
from logging import getLogger
77+
from typing import Callable, Collection
4178

4279
from pymongo import monitoring
4380

@@ -48,14 +85,34 @@
4885
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
4986
from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes
5087
from opentelemetry.trace import SpanKind, get_tracer
88+
from opentelemetry.trace.span import Span
5189
from opentelemetry.trace.status import Status, StatusCode
5290

91+
_LOG = getLogger(__name__)
92+
93+
RequestHookT = Callable[[Span, monitoring.CommandStartedEvent], None]
94+
ResponseHookT = Callable[[Span, monitoring.CommandSucceededEvent], None]
95+
FailedHookT = Callable[[Span, monitoring.CommandFailedEvent], None]
96+
97+
98+
def dummy_callback(span, event):
99+
...
100+
53101

54102
class CommandTracer(monitoring.CommandListener):
55-
def __init__(self, tracer):
103+
def __init__(
104+
self,
105+
tracer,
106+
request_hook: RequestHookT = dummy_callback,
107+
response_hook: ResponseHookT = dummy_callback,
108+
failed_hook: FailedHookT = dummy_callback,
109+
):
56110
self._tracer = tracer
57111
self._span_dict = {}
58112
self.is_enabled = True
113+
self.start_hook = request_hook
114+
self.success_hook = response_hook
115+
self.failed_hook = failed_hook
59116

60117
def started(self, event: monitoring.CommandStartedEvent):
61118
""" Method to handle a pymongo CommandStartedEvent """
@@ -85,6 +142,10 @@ def started(self, event: monitoring.CommandStartedEvent):
85142
span.set_attribute(
86143
SpanAttributes.NET_PEER_PORT, event.connection_id[1]
87144
)
145+
try:
146+
self.start_hook(span, event)
147+
except Exception as hook_exception: # noqa pylint: disable=broad-except
148+
_LOG.exception(hook_exception)
88149

89150
# Add Span to dictionary
90151
self._span_dict[_get_span_dict_key(event)] = span
@@ -103,6 +164,11 @@ def succeeded(self, event: monitoring.CommandSucceededEvent):
103164
span = self._pop_span(event)
104165
if span is None:
105166
return
167+
if span.is_recording():
168+
try:
169+
self.success_hook(span, event)
170+
except Exception as hook_exception: # noqa pylint: disable=broad-except
171+
_LOG.exception(hook_exception)
106172
span.end()
107173

108174
def failed(self, event: monitoring.CommandFailedEvent):
@@ -116,6 +182,10 @@ def failed(self, event: monitoring.CommandFailedEvent):
116182
return
117183
if span.is_recording():
118184
span.set_status(Status(StatusCode.ERROR, event.failure))
185+
try:
186+
self.failed_hook(span, event)
187+
except Exception as hook_exception: # noqa pylint: disable=broad-except
188+
_LOG.exception(hook_exception)
119189
span.end()
120190

121191
def _pop_span(self, event):
@@ -150,12 +220,20 @@ def _instrument(self, **kwargs):
150220
"""
151221

152222
tracer_provider = kwargs.get("tracer_provider")
223+
request_hook = kwargs.get("request_hook", dummy_callback)
224+
response_hook = kwargs.get("response_hook", dummy_callback)
225+
failed_hook = kwargs.get("failed_hook", dummy_callback)
153226

154227
# Create and register a CommandTracer only the first time
155228
if self._commandtracer_instance is None:
156229
tracer = get_tracer(__name__, __version__, tracer_provider)
157230

158-
self._commandtracer_instance = CommandTracer(tracer)
231+
self._commandtracer_instance = CommandTracer(
232+
tracer,
233+
request_hook=request_hook,
234+
response_hook=response_hook,
235+
failed_hook=failed_hook,
236+
)
159237
monitoring.register(self._commandtracer_instance)
160238

161239
# If already created, just enable it

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class TestPymongo(TestBase):
2929
def setUp(self):
3030
super().setUp()
3131
self.tracer = self.tracer_provider.get_tracer(__name__)
32+
self.start_callback = mock.MagicMock()
33+
self.success_callback = mock.MagicMock()
34+
self.failed_callback = mock.MagicMock()
3235

3336
def test_pymongo_instrumentor(self):
3437
mock_register = mock.Mock()
@@ -44,7 +47,9 @@ def test_started(self):
4447
command_attrs = {
4548
"command_name": "find",
4649
}
47-
command_tracer = CommandTracer(self.tracer)
50+
command_tracer = CommandTracer(
51+
self.tracer, request_hook=self.start_callback
52+
)
4853
mock_event = MockEvent(
4954
command_attrs, ("test.com", "1234"), "test_request_id"
5055
)
@@ -66,17 +71,24 @@ def test_started(self):
6671
span.attributes[SpanAttributes.NET_PEER_NAME], "test.com"
6772
)
6873
self.assertEqual(span.attributes[SpanAttributes.NET_PEER_PORT], "1234")
74+
self.start_callback.assert_called_once_with(span, mock_event)
6975

7076
def test_succeeded(self):
7177
mock_event = MockEvent({})
72-
command_tracer = CommandTracer(self.tracer)
78+
command_tracer = CommandTracer(
79+
self.tracer,
80+
request_hook=self.start_callback,
81+
response_hook=self.success_callback,
82+
)
7383
command_tracer.started(event=mock_event)
7484
command_tracer.succeeded(event=mock_event)
7585
spans_list = self.memory_exporter.get_finished_spans()
7686
self.assertEqual(len(spans_list), 1)
7787
span = spans_list[0]
7888
self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET)
7989
self.assertIsNotNone(span.end_time)
90+
self.start_callback.assert_called_once()
91+
self.success_callback.assert_called_once()
8092

8193
def test_not_recording(self):
8294
mock_tracer = mock.Mock()
@@ -119,7 +131,11 @@ def test_suppression_key(self):
119131

120132
def test_failed(self):
121133
mock_event = MockEvent({})
122-
command_tracer = CommandTracer(self.tracer)
134+
command_tracer = CommandTracer(
135+
self.tracer,
136+
request_hook=self.start_callback,
137+
failed_hook=self.failed_callback,
138+
)
123139
command_tracer.started(event=mock_event)
124140
command_tracer.failed(event=mock_event)
125141

@@ -132,6 +148,8 @@ def test_failed(self):
132148
)
133149
self.assertEqual(span.status.description, "failure")
134150
self.assertIsNotNone(span.end_time)
151+
self.start_callback.assert_called_once()
152+
self.failed_callback.assert_called_once()
135153

136154
def test_multiple_commands(self):
137155
first_mock_event = MockEvent({}, ("firstUrl", "123"), "first")

0 commit comments

Comments
 (0)