Skip to content

Commit d149d6a

Browse files
emdnetoxrmxherin049
authored
opentelemetry-instrumentation-pymongo: semantic convention stability migration (#4772)
* feat(pymongo): database semconv stability migration Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * add error.type Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * add changelog Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * fix docker-tests Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * fix lint Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * fix docker-tests Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * Update instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py --------- Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> Co-authored-by: Lukas Hering <40302054+herin049@users.noreply.github.com>
1 parent ed72afb commit d149d6a

6 files changed

Lines changed: 344 additions & 17 deletions

File tree

.changelog/4772.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-instrumentation-pymongo`: add database semantic convention stability support through `OTEL_SEMCONV_STABILITY_OPT_IN`

instrumentation/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
| [opentelemetry-instrumentation-psycopg](./opentelemetry-instrumentation-psycopg) | psycopg >= 3.1.0 | No | migration
3535
| [opentelemetry-instrumentation-psycopg2](./opentelemetry-instrumentation-psycopg2) | psycopg2 >= 2.7.3.1,psycopg2-binary >= 2.7.3.1 | No | migration
3636
| [opentelemetry-instrumentation-pymemcache](./opentelemetry-instrumentation-pymemcache) | pymemcache >= 1.3.5, < 5 | No | migration
37-
| [opentelemetry-instrumentation-pymongo](./opentelemetry-instrumentation-pymongo) | pymongo >= 3.1, < 5.0 | No | development
37+
| [opentelemetry-instrumentation-pymongo](./opentelemetry-instrumentation-pymongo) | pymongo >= 3.1, < 5.0 | No | migration
3838
| [opentelemetry-instrumentation-pymssql](./opentelemetry-instrumentation-pymssql) | pymssql >= 2.1.5, < 3 | No | migration
3939
| [opentelemetry-instrumentation-pymysql](./opentelemetry-instrumentation-pymysql) | PyMySQL < 2 | No | migration
4040
| [opentelemetry-instrumentation-pyramid](./opentelemetry-instrumentation-pyramid) | pyramid >= 1.7 | Yes | migration

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

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ def failed_hook(span, event):
6868

6969
from pymongo import monitoring
7070

71+
from opentelemetry.instrumentation._semconv import (
72+
_get_schema_url_for_signal_types,
73+
_OpenTelemetrySemanticConventionStability,
74+
_OpenTelemetryStabilitySignalType,
75+
_report_new,
76+
_report_old,
77+
_set_db_name,
78+
_set_db_statement,
79+
_set_db_system,
80+
_set_http_net_peer_name_client,
81+
_set_http_peer_port_client,
82+
_StabilityMode,
83+
)
7184
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
7285
from opentelemetry.instrumentation.pymongo.package import _instruments
7386
from opentelemetry.instrumentation.pymongo.utils import (
@@ -77,15 +90,10 @@ def failed_hook(span, event):
7790
from opentelemetry.instrumentation.utils import is_instrumentation_enabled
7891
from opentelemetry.semconv._incubating.attributes.db_attributes import (
7992
DB_MONGODB_COLLECTION,
80-
DB_NAME,
81-
DB_STATEMENT,
82-
DB_SYSTEM,
83-
)
84-
from opentelemetry.semconv._incubating.attributes.net_attributes import (
85-
NET_PEER_NAME,
86-
NET_PEER_PORT,
93+
DbSystemValues,
8794
)
88-
from opentelemetry.semconv.trace import DbSystemValues
95+
from opentelemetry.semconv.attributes.db_attributes import DB_COLLECTION_NAME
96+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
8997
from opentelemetry.trace import SpanKind, Tracer, get_tracer
9098
from opentelemetry.trace.span import Span
9199
from opentelemetry.trace.status import Status, StatusCode
@@ -115,6 +123,7 @@ def __init__(
115123
response_hook: ResponseHookT = dummy_callback,
116124
failed_hook: FailedHookT = dummy_callback,
117125
capture_statement: bool = False,
126+
semconv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
118127
):
119128
self._tracer = tracer
120129
self._span_dict = {}
@@ -123,6 +132,7 @@ def __init__(
123132
self.success_hook = response_hook
124133
self.failed_hook = failed_hook
125134
self.capture_statement = capture_statement
135+
self._semconv_opt_in_mode = semconv_opt_in_mode
126136

127137
def started(self, event: monitoring.CommandStartedEvent):
128138
"""Method to handle a pymongo CommandStartedEvent"""
@@ -136,14 +146,39 @@ def started(self, event: monitoring.CommandStartedEvent):
136146
try:
137147
span = self._tracer.start_span(span_name, kind=SpanKind.CLIENT)
138148
if span.is_recording():
139-
span.set_attribute(DB_SYSTEM, DbSystemValues.MONGODB.value)
140-
span.set_attribute(DB_NAME, event.database_name)
141-
span.set_attribute(DB_STATEMENT, statement)
149+
span_attributes: dict = {}
150+
_set_db_system(
151+
span_attributes,
152+
DbSystemValues.MONGODB.value,
153+
self._semconv_opt_in_mode,
154+
)
155+
_set_db_name(
156+
span_attributes,
157+
event.database_name,
158+
self._semconv_opt_in_mode,
159+
)
160+
_set_db_statement(
161+
span_attributes,
162+
statement,
163+
self._semconv_opt_in_mode,
164+
)
142165
if collection:
143-
span.set_attribute(DB_MONGODB_COLLECTION, collection)
166+
if _report_old(self._semconv_opt_in_mode):
167+
span_attributes[DB_MONGODB_COLLECTION] = collection
168+
if _report_new(self._semconv_opt_in_mode):
169+
span_attributes[DB_COLLECTION_NAME] = collection
144170
if event.connection_id is not None:
145-
span.set_attribute(NET_PEER_NAME, event.connection_id[0])
146-
span.set_attribute(NET_PEER_PORT, event.connection_id[1])
171+
_set_http_net_peer_name_client(
172+
span_attributes,
173+
event.connection_id[0],
174+
self._semconv_opt_in_mode,
175+
)
176+
_set_http_peer_port_client(
177+
span_attributes,
178+
event.connection_id[1],
179+
self._semconv_opt_in_mode,
180+
)
181+
span.set_attributes(span_attributes)
147182
try:
148183
self.start_hook(span, event)
149184
except (
@@ -156,6 +191,8 @@ def started(self, event: monitoring.CommandStartedEvent):
156191
except Exception as ex: # noqa pylint: disable=broad-except
157192
if span is not None and span.is_recording():
158193
span.set_status(Status(StatusCode.ERROR, str(ex)))
194+
if _report_new(self._semconv_opt_in_mode):
195+
span.set_attribute(ERROR_TYPE, type(ex).__qualname__)
159196
span.end()
160197
self._pop_span(event)
161198

@@ -189,6 +226,10 @@ def failed(self, event: monitoring.CommandFailedEvent):
189226
event.failure.get("errmsg", "Unknown error"),
190227
)
191228
)
229+
if _report_new(self._semconv_opt_in_mode):
230+
span.set_attribute(
231+
ERROR_TYPE, event.failure.get("codeName", "UnknownError")
232+
)
192233
try:
193234
self.failed_hook(span, event)
194235
except (
@@ -254,11 +295,17 @@ def _instrument(self, **kwargs: Any):
254295
capture_statement = kwargs.get("capture_statement")
255296
# Create and register a CommandTracer only the first time
256297
if self._commandtracer_instance is None:
298+
_OpenTelemetrySemanticConventionStability._initialize()
299+
semconv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
300+
_OpenTelemetryStabilitySignalType.DATABASE,
301+
)
257302
tracer = get_tracer(
258303
__name__,
259304
__version__,
260305
tracer_provider,
261-
schema_url="https://opentelemetry.io/schemas/1.11.0",
306+
schema_url=_get_schema_url_for_signal_types(
307+
[_OpenTelemetryStabilitySignalType.DATABASE]
308+
),
262309
)
263310

264311
self._commandtracer_instance = CommandTracer(
@@ -267,6 +314,7 @@ def _instrument(self, **kwargs: Any):
267314
response_hook=response_hook,
268315
failed_hook=failed_hook,
269316
capture_statement=capture_statement,
317+
semconv_opt_in_mode=semconv_opt_in_mode,
270318
)
271319
monitoring.register(self._commandtracer_instance)
272320
# If already created, just enable it

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33

44

55
_instruments = ("pymongo >= 3.1, < 5.0",)
6+
7+
_semconv_status = "migration"

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

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
# Copyright The OpenTelemetry Authors
22
# SPDX-License-Identifier: Apache-2.0
33

4+
import contextlib
45
from unittest import mock
56

67
from opentelemetry import trace as trace_api
8+
from opentelemetry.instrumentation._semconv import (
9+
OTEL_SEMCONV_STABILITY_OPT_IN,
10+
_OpenTelemetrySemanticConventionStability,
11+
_StabilityMode,
12+
)
713
from opentelemetry.instrumentation.pymongo import (
814
CommandTracer,
915
PymongoInstrumentor,
@@ -19,16 +25,52 @@
1925
NET_PEER_NAME,
2026
NET_PEER_PORT,
2127
)
28+
from opentelemetry.semconv.attributes.db_attributes import (
29+
DB_COLLECTION_NAME,
30+
DB_NAMESPACE,
31+
DB_QUERY_TEXT,
32+
DB_SYSTEM_NAME,
33+
)
34+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
35+
from opentelemetry.semconv.attributes.server_attributes import (
36+
SERVER_ADDRESS,
37+
SERVER_PORT,
38+
)
2239
from opentelemetry.test.test_base import TestBase
2340

2441

42+
@contextlib.contextmanager
43+
def use_semconv_opt_in(sem_conv_mode):
44+
env_patch = mock.patch.dict(
45+
"os.environ",
46+
{OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode},
47+
)
48+
_OpenTelemetrySemanticConventionStability._initialized = False
49+
env_patch.start()
50+
try:
51+
yield
52+
finally:
53+
env_patch.stop()
54+
_OpenTelemetrySemanticConventionStability._initialized = False
55+
56+
57+
# pylint: disable=too-many-public-methods
2558
class TestPymongo(TestBase):
2659
def setUp(self):
2760
super().setUp()
2861
self.tracer = self.tracer_provider.get_tracer(__name__)
2962
self.start_callback = mock.MagicMock()
3063
self.success_callback = mock.MagicMock()
3164
self.failed_callback = mock.MagicMock()
65+
PymongoInstrumentor._instance = None
66+
PymongoInstrumentor._commandtracer_instance = None
67+
_OpenTelemetrySemanticConventionStability._initialized = False
68+
69+
def tearDown(self):
70+
super().tearDown()
71+
PymongoInstrumentor._instance = None
72+
PymongoInstrumentor._commandtracer_instance = None
73+
_OpenTelemetrySemanticConventionStability._initialized = False
3274

3375
def test_pymongo_instrumentor(self):
3476
mock_register = mock.Mock()
@@ -60,7 +102,7 @@ def test_started(self):
60102
self.assertEqual(span.attributes[DB_NAME], "database_name")
61103
self.assertEqual(span.attributes[DB_STATEMENT], "find")
62104
self.assertEqual(span.attributes[NET_PEER_NAME], "test.com")
63-
self.assertEqual(span.attributes[NET_PEER_PORT], "1234")
105+
self.assertEqual(span.attributes[NET_PEER_PORT], 1234)
64106
self.start_callback.assert_called_once_with(span, mock_event)
65107

66108
def test_succeeded(self):
@@ -301,6 +343,139 @@ def test_collection_name_attribute(self):
301343
)
302344
self.memory_exporter.clear()
303345

346+
def test_schema_url_default(self):
347+
with mock.patch("pymongo.monitoring.register"):
348+
instrumentor = PymongoInstrumentor()
349+
instrumentor.instrument(tracer_provider=self.tracer_provider)
350+
self.assertEqual(
351+
instrumentor._commandtracer_instance._tracer._instrumentation_scope.schema_url,
352+
"https://opentelemetry.io/schemas/1.11.0",
353+
)
354+
355+
def test_schema_url_new_semconv(self):
356+
with use_semconv_opt_in("database"):
357+
with mock.patch("pymongo.monitoring.register"):
358+
instrumentor = PymongoInstrumentor()
359+
instrumentor.instrument(tracer_provider=self.tracer_provider)
360+
self.assertEqual(
361+
instrumentor._commandtracer_instance._tracer._instrumentation_scope.schema_url,
362+
"https://opentelemetry.io/schemas/1.25.0",
363+
)
364+
365+
def test_started_new_semconv(self):
366+
command_tracer = CommandTracer(
367+
self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE
368+
)
369+
mock_event = MockEvent({"command_name": "find"}, ("testhost", 1234))
370+
command_tracer.started(event=mock_event)
371+
span = command_tracer._pop_span(mock_event) # pylint: disable=protected-access
372+
self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mongodb")
373+
self.assertEqual(span.attributes[DB_NAMESPACE], "database_name")
374+
self.assertEqual(span.attributes[DB_QUERY_TEXT], "find")
375+
self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost")
376+
self.assertEqual(span.attributes[SERVER_PORT], 1234)
377+
378+
def test_started_both_semconv(self):
379+
command_tracer = CommandTracer(
380+
self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE_DUP
381+
)
382+
mock_event = MockEvent({"command_name": "find"}, ("testhost", 1234))
383+
command_tracer.started(event=mock_event)
384+
span = command_tracer._pop_span(mock_event) # pylint: disable=protected-access
385+
self.assertEqual(span.attributes[DB_SYSTEM], "mongodb")
386+
self.assertEqual(span.attributes[DB_NAME], "database_name")
387+
self.assertEqual(span.attributes[DB_STATEMENT], "find")
388+
self.assertEqual(span.attributes[NET_PEER_NAME], "testhost")
389+
self.assertEqual(span.attributes[NET_PEER_PORT], 1234)
390+
self.assertEqual(span.attributes[DB_SYSTEM_NAME], "mongodb")
391+
self.assertEqual(span.attributes[DB_NAMESPACE], "database_name")
392+
self.assertEqual(span.attributes[DB_QUERY_TEXT], "find")
393+
self.assertEqual(span.attributes[SERVER_ADDRESS], "testhost")
394+
self.assertEqual(span.attributes[SERVER_PORT], 1234)
395+
396+
def test_collection_name_default_semconv(self):
397+
command_tracer = CommandTracer(self.tracer)
398+
mock_event = MockEvent({"command_name": "find", "find": "test_coll"})
399+
command_tracer.started(event=mock_event)
400+
command_tracer.succeeded(event=mock_event)
401+
spans = self.memory_exporter.get_finished_spans()
402+
self.assertEqual(len(spans), 1)
403+
span = spans[0]
404+
self.assertEqual(span.attributes[DB_MONGODB_COLLECTION], "test_coll")
405+
self.assertNotIn(DB_COLLECTION_NAME, span.attributes)
406+
407+
def test_collection_name_new_semconv(self):
408+
command_tracer = CommandTracer(
409+
self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE
410+
)
411+
mock_event = MockEvent({"command_name": "find", "find": "test_coll"})
412+
command_tracer.started(event=mock_event)
413+
command_tracer.succeeded(event=mock_event)
414+
spans = self.memory_exporter.get_finished_spans()
415+
self.assertEqual(len(spans), 1)
416+
span = spans[0]
417+
self.assertEqual(span.attributes[DB_COLLECTION_NAME], "test_coll")
418+
self.assertNotIn(DB_MONGODB_COLLECTION, span.attributes)
419+
420+
def test_collection_name_both_semconv(self):
421+
command_tracer = CommandTracer(
422+
self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE_DUP
423+
)
424+
mock_event = MockEvent({"command_name": "find", "find": "test_coll"})
425+
command_tracer.started(event=mock_event)
426+
command_tracer.succeeded(event=mock_event)
427+
spans = self.memory_exporter.get_finished_spans()
428+
self.assertEqual(len(spans), 1)
429+
span = spans[0]
430+
self.assertEqual(span.attributes[DB_MONGODB_COLLECTION], "test_coll")
431+
self.assertEqual(span.attributes[DB_COLLECTION_NAME], "test_coll")
432+
433+
def test_failed_error_type_not_set_on_default(self):
434+
command_tracer = CommandTracer(self.tracer)
435+
mock_event = MockEvent({})
436+
command_tracer.started(event=mock_event)
437+
mock_event.mark_as_failed()
438+
command_tracer.failed(event=mock_event)
439+
spans = self.memory_exporter.get_finished_spans()
440+
self.assertEqual(len(spans), 1)
441+
span = spans[0]
442+
self.assertFalse(span.status.is_ok)
443+
self.assertNotIn(ERROR_TYPE, span.attributes)
444+
445+
def test_failed_error_type_set_on_new_semconv(self):
446+
command_tracer = CommandTracer(
447+
self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE
448+
)
449+
mock_event = MockEvent({})
450+
command_tracer.started(event=mock_event)
451+
mock_event.failure = {
452+
"errmsg": "operation failed",
453+
"codeName": "OperationFailed",
454+
}
455+
command_tracer.failed(event=mock_event)
456+
spans = self.memory_exporter.get_finished_spans()
457+
self.assertEqual(len(spans), 1)
458+
span = spans[0]
459+
self.assertFalse(span.status.is_ok)
460+
self.assertEqual(span.attributes[ERROR_TYPE], "OperationFailed")
461+
462+
def test_failed_error_type_set_on_both_semconv(self):
463+
command_tracer = CommandTracer(
464+
self.tracer, semconv_opt_in_mode=_StabilityMode.DATABASE_DUP
465+
)
466+
mock_event = MockEvent({})
467+
command_tracer.started(event=mock_event)
468+
mock_event.failure = {
469+
"errmsg": "operation failed",
470+
"codeName": "OperationFailed",
471+
}
472+
command_tracer.failed(event=mock_event)
473+
spans = self.memory_exporter.get_finished_spans()
474+
self.assertEqual(len(spans), 1)
475+
span = spans[0]
476+
self.assertFalse(span.status.is_ok)
477+
self.assertEqual(span.attributes[ERROR_TYPE], "OperationFailed")
478+
304479

305480
class MockCommand:
306481
def __init__(self, command_attrs):

0 commit comments

Comments
 (0)