Skip to content

Commit 810d982

Browse files
authored
mongo db - fix db statement capturing (#1512)
1 parent e1a1bad commit 810d982

File tree

5 files changed

+82
-16
lines changed

5 files changed

+82
-16
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
([#1555](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1555))
2828
- `opentelemetry-instrumentation-asgi` Fix keys() in class ASGIGetter to correctly fetch values from carrier headers.
2929
([#1435](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1435))
30-
30+
- mongo db - fix db statement capturing
31+
([#1512](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1512))
3132

3233
## Version 1.15.0/0.36b0 (2022-12-10)
3334

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

+20-7
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
failed_hook (Callable) -
4747
a function with extra user-defined logic to be performed after the query returns with a failed response
4848
this function signature is: def failed_hook(span: Span, event: CommandFailedEvent) -> None
49+
capture_statement (bool) - an optional value to enable capturing the database statement that is being executed
4950
5051
for example:
5152
@@ -81,6 +82,9 @@ def failed_hook(span, event):
8182
from opentelemetry import context
8283
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
8384
from opentelemetry.instrumentation.pymongo.package import _instruments
85+
from opentelemetry.instrumentation.pymongo.utils import (
86+
COMMAND_TO_ATTRIBUTE_MAPPING,
87+
)
8488
from opentelemetry.instrumentation.pymongo.version import __version__
8589
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
8690
from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes
@@ -106,30 +110,29 @@ def __init__(
106110
request_hook: RequestHookT = dummy_callback,
107111
response_hook: ResponseHookT = dummy_callback,
108112
failed_hook: FailedHookT = dummy_callback,
113+
capture_statement: bool = False,
109114
):
110115
self._tracer = tracer
111116
self._span_dict = {}
112117
self.is_enabled = True
113118
self.start_hook = request_hook
114119
self.success_hook = response_hook
115120
self.failed_hook = failed_hook
121+
self.capture_statement = capture_statement
116122

117123
def started(self, event: monitoring.CommandStartedEvent):
118124
"""Method to handle a pymongo CommandStartedEvent"""
119125
if not self.is_enabled or context.get_value(
120126
_SUPPRESS_INSTRUMENTATION_KEY
121127
):
122128
return
123-
command = event.command.get(event.command_name, "")
124-
name = event.database_name
125-
name += "." + event.command_name
126-
statement = event.command_name
127-
if command:
128-
statement += " " + str(command)
129+
command_name = event.command_name
130+
span_name = f"{event.database_name}.{command_name}"
131+
statement = self._get_statement_by_command_name(command_name, event)
129132
collection = event.command.get(event.command_name)
130133

131134
try:
132-
span = self._tracer.start_span(name, kind=SpanKind.CLIENT)
135+
span = self._tracer.start_span(span_name, kind=SpanKind.CLIENT)
133136
if span.is_recording():
134137
span.set_attribute(
135138
SpanAttributes.DB_SYSTEM, DbSystemValues.MONGODB.value
@@ -196,6 +199,14 @@ def failed(self, event: monitoring.CommandFailedEvent):
196199
def _pop_span(self, event):
197200
return self._span_dict.pop(_get_span_dict_key(event), None)
198201

202+
def _get_statement_by_command_name(self, command_name, event):
203+
statement = command_name
204+
command_attribute = COMMAND_TO_ATTRIBUTE_MAPPING.get(command_name)
205+
command = event.command.get(command_attribute)
206+
if command and self.capture_statement:
207+
statement += " " + str(command)
208+
return statement
209+
199210

200211
def _get_span_dict_key(event):
201212
if event.connection_id is not None:
@@ -228,6 +239,7 @@ def _instrument(self, **kwargs):
228239
request_hook = kwargs.get("request_hook", dummy_callback)
229240
response_hook = kwargs.get("response_hook", dummy_callback)
230241
failed_hook = kwargs.get("failed_hook", dummy_callback)
242+
capture_statement = kwargs.get("capture_statement")
231243
# Create and register a CommandTracer only the first time
232244
if self._commandtracer_instance is None:
233245
tracer = get_tracer(__name__, __version__, tracer_provider)
@@ -237,6 +249,7 @@ def _instrument(self, **kwargs):
237249
request_hook=request_hook,
238250
response_hook=response_hook,
239251
failed_hook=failed_hook,
252+
capture_statement=capture_statement,
240253
)
241254
monitoring.register(self._commandtracer_instance)
242255
# If already created, just enable it
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain 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,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
COMMAND_TO_ATTRIBUTE_MAPPING = {
16+
"insert": "documents",
17+
"delete": "deletes",
18+
"update": "updates",
19+
"find": "filter",
20+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def test_started(self):
6464
span.attributes[SpanAttributes.DB_NAME], "database_name"
6565
)
6666
self.assertEqual(
67-
span.attributes[SpanAttributes.DB_STATEMENT], "command_name find"
67+
span.attributes[SpanAttributes.DB_STATEMENT], "command_name"
6868
)
6969
self.assertEqual(
7070
span.attributes[SpanAttributes.NET_PEER_NAME], "test.com"

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

+39-7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def setUp(self):
3434
self.instrumentor = PymongoInstrumentor()
3535
self.instrumentor.instrument()
3636
self.instrumentor._commandtracer_instance._tracer = self._tracer
37+
self.instrumentor._commandtracer_instance.capture_statement = True
3738
client = MongoClient(
3839
MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000
3940
)
@@ -44,7 +45,7 @@ def tearDown(self):
4445
self.instrumentor.uninstrument()
4546
super().tearDown()
4647

47-
def validate_spans(self):
48+
def validate_spans(self, expected_db_statement):
4849
spans = self.memory_exporter.get_finished_spans()
4950
self.assertEqual(len(spans), 2)
5051
for span in spans:
@@ -72,34 +73,65 @@ def validate_spans(self):
7273
pymongo_span.attributes[SpanAttributes.DB_MONGODB_COLLECTION],
7374
MONGODB_COLLECTION_NAME,
7475
)
76+
self.assertEqual(
77+
pymongo_span.attributes[SpanAttributes.DB_STATEMENT],
78+
expected_db_statement,
79+
)
7580

7681
def test_insert(self):
7782
"""Should create a child span for insert"""
7883
with self._tracer.start_as_current_span("rootSpan"):
79-
self._collection.insert_one(
84+
insert_result = self._collection.insert_one(
8085
{"name": "testName", "value": "testValue"}
8186
)
82-
self.validate_spans()
87+
insert_result_id = insert_result.inserted_id
88+
89+
expected_db_statement = (
90+
f"insert [{{'name': 'testName', 'value': 'testValue', '_id': "
91+
f"ObjectId('{insert_result_id}')}}]"
92+
)
93+
self.validate_spans(expected_db_statement)
8394

8495
def test_update(self):
8596
"""Should create a child span for update"""
8697
with self._tracer.start_as_current_span("rootSpan"):
8798
self._collection.update_one(
8899
{"name": "testName"}, {"$set": {"value": "someOtherValue"}}
89100
)
90-
self.validate_spans()
101+
102+
expected_db_statement = (
103+
"update [SON([('q', {'name': 'testName'}), ('u', "
104+
"{'$set': {'value': 'someOtherValue'}}), ('multi', False), ('upsert', False)])]"
105+
)
106+
self.validate_spans(expected_db_statement)
91107

92108
def test_find(self):
93109
"""Should create a child span for find"""
94110
with self._tracer.start_as_current_span("rootSpan"):
95-
self._collection.find_one()
96-
self.validate_spans()
111+
self._collection.find_one({"name": "testName"})
112+
113+
expected_db_statement = "find {'name': 'testName'}"
114+
self.validate_spans(expected_db_statement)
97115

98116
def test_delete(self):
99117
"""Should create a child span for delete"""
100118
with self._tracer.start_as_current_span("rootSpan"):
101119
self._collection.delete_one({"name": "testName"})
102-
self.validate_spans()
120+
121+
expected_db_statement = (
122+
"delete [SON([('q', {'name': 'testName'}), ('limit', 1)])]"
123+
)
124+
self.validate_spans(expected_db_statement)
125+
126+
def test_find_without_capture_statement(self):
127+
"""Should create a child span for find"""
128+
self.instrumentor._commandtracer_instance.capture_statement = False
129+
130+
with self._tracer.start_as_current_span("rootSpan"):
131+
self._collection.find_one({"name": "testName"})
132+
133+
expected_db_statement = "find"
134+
self.validate_spans(expected_db_statement)
103135

104136
def test_uninstrument(self):
105137
# check that integration is working

0 commit comments

Comments
 (0)