Skip to content

Commit a938258

Browse files
committed
feat: add optional traceback as spanattribute for psycopg2 instrumentation
1 parent b1f714e commit a938258

File tree

3 files changed

+120
-11
lines changed

3 files changed

+120
-11
lines changed

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

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
import functools
4343
import logging
4444
import re
45+
import sys
46+
import traceback
4547
from typing import Any, Callable, Generic, TypeVar
4648

4749
import wrapt
@@ -54,7 +56,19 @@
5456
_get_opentelemetry_values,
5557
unwrap,
5658
)
57-
from opentelemetry.semconv.trace import SpanAttributes
59+
from opentelemetry.semconv._incubating.attributes.code_attributes import (
60+
CODE_STACKTRACE,
61+
)
62+
from opentelemetry.semconv._incubating.attributes.db_attributes import (
63+
DB_NAME,
64+
DB_STATEMENT,
65+
DB_SYSTEM,
66+
DB_USER,
67+
)
68+
from opentelemetry.semconv._incubating.attributes.net_attributes import (
69+
NET_PEER_NAME,
70+
NET_PEER_PORT,
71+
)
5872
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
5973
from opentelemetry.util._importlib_metadata import version as util_version
6074

@@ -78,6 +92,7 @@ def trace_integration(
7892
enable_commenter: bool = False,
7993
db_api_integration_factory: type[DatabaseApiIntegration] | None = None,
8094
enable_attribute_commenter: bool = False,
95+
enable_traceback: bool = False,
8196
):
8297
"""Integrate with DB API library.
8398
https://www.python.org/dev/peps/pep-0249/
@@ -96,6 +111,7 @@ def trace_integration(
96111
db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the
97112
default one is used.
98113
enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True.
114+
enable_traceback: Enable traceback for every trace.
99115
"""
100116
wrap_connect(
101117
__name__,
@@ -109,6 +125,7 @@ def trace_integration(
109125
enable_commenter=enable_commenter,
110126
db_api_integration_factory=db_api_integration_factory,
111127
enable_attribute_commenter=enable_attribute_commenter,
128+
enable_traceback=enable_traceback,
112129
)
113130

114131

@@ -125,6 +142,7 @@ def wrap_connect(
125142
db_api_integration_factory: type[DatabaseApiIntegration] | None = None,
126143
commenter_options: dict[str, Any] | None = None,
127144
enable_attribute_commenter: bool = False,
145+
enable_traceback: bool = False,
128146
):
129147
"""Integrate with DB API library.
130148
https://www.python.org/dev/peps/pep-0249/
@@ -144,6 +162,7 @@ def wrap_connect(
144162
default one is used.
145163
commenter_options: Configurations for tags to be appended at the sql query.
146164
enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True.
165+
enable_traceback: Enable traceback for every trace.
147166
148167
"""
149168
db_api_integration_factory = (
@@ -168,6 +187,7 @@ def wrap_connect_(
168187
commenter_options=commenter_options,
169188
connect_module=connect_module,
170189
enable_attribute_commenter=enable_attribute_commenter,
190+
enable_traceback=enable_traceback,
171191
)
172192
return db_integration.wrapped_connection(wrapped, args, kwargs)
173193

@@ -204,6 +224,7 @@ def instrument_connection(
204224
commenter_options: dict[str, Any] | None = None,
205225
connect_module: Callable[..., Any] | None = None,
206226
enable_attribute_commenter: bool = False,
227+
enable_traceback: bool = False,
207228
db_api_integration_factory: type[DatabaseApiIntegration] | None = None,
208229
) -> TracedConnectionProxy[ConnectionT]:
209230
"""Enable instrumentation in a database connection.
@@ -222,6 +243,7 @@ def instrument_connection(
222243
commenter_options: Configurations for tags to be appended at the sql query.
223244
connect_module: Module name where connect method is available.
224245
enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True.
246+
enable_traceback: Enable traceback for every trace.
225247
db_api_integration_factory: A class or factory function to use as a
226248
replacement for :class:`DatabaseApiIntegration`. Can be used to
227249
obtain connection attributes from the connect method instead of
@@ -249,6 +271,7 @@ def instrument_connection(
249271
commenter_options=commenter_options,
250272
connect_module=connect_module,
251273
enable_attribute_commenter=enable_attribute_commenter,
274+
enable_traceback=enable_traceback,
252275
)
253276
db_integration.get_connection_attributes(connection)
254277
return get_traced_connection_proxy(connection, db_integration)
@@ -285,6 +308,7 @@ def __init__(
285308
commenter_options: dict[str, Any] | None = None,
286309
connect_module: Callable[..., Any] | None = None,
287310
enable_attribute_commenter: bool = False,
311+
enable_traceback: bool = False,
288312
):
289313
if connection_attributes is None:
290314
self.connection_attributes = {
@@ -307,6 +331,7 @@ def __init__(
307331
self.enable_commenter = enable_commenter
308332
self.commenter_options = commenter_options
309333
self.enable_attribute_commenter = enable_attribute_commenter
334+
self.enable_traceback = enable_traceback
310335
self.database_system = database_system
311336
self.connection_props: dict[str, Any] = {}
312337
self.span_attributes: dict[str, Any] = {}
@@ -401,13 +426,13 @@ def get_connection_attributes(self, connection: object) -> None:
401426
if user and isinstance(user, bytes):
402427
user = user.decode()
403428
if user is not None:
404-
self.span_attributes[SpanAttributes.DB_USER] = str(user)
429+
self.span_attributes[DB_USER] = str(user)
405430
host = self.connection_props.get("host")
406431
if host is not None:
407-
self.span_attributes[SpanAttributes.NET_PEER_NAME] = host
432+
self.span_attributes[NET_PEER_NAME] = host
408433
port = self.connection_props.get("port")
409434
if port is not None:
410-
self.span_attributes[SpanAttributes.NET_PEER_PORT] = port
435+
self.span_attributes[NET_PEER_PORT] = port
411436

412437

413438
# pylint: disable=abstract-method
@@ -464,6 +489,7 @@ def __init__(self, db_api_integration: DatabaseApiIntegration) -> None:
464489
self._enable_attribute_commenter = (
465490
self._db_api_integration.enable_attribute_commenter
466491
)
492+
self._enable_traceback = self._db_api_integration.enable_traceback
467493
self._connect_module = self._db_api_integration.connect_module
468494
self._leading_comment_remover = re.compile(r"^/\*.*?\*/")
469495

@@ -516,13 +542,12 @@ def _populate_span(
516542
if not span.is_recording():
517543
return
518544
statement = self.get_statement(cursor, args)
519-
span.set_attribute(
520-
SpanAttributes.DB_SYSTEM, self._db_api_integration.database_system
521-
)
522-
span.set_attribute(
523-
SpanAttributes.DB_NAME, self._db_api_integration.database
524-
)
525-
span.set_attribute(SpanAttributes.DB_STATEMENT, statement)
545+
span.set_attribute(DB_SYSTEM, self._db_api_integration.database_system)
546+
span.set_attribute(DB_NAME, self._db_api_integration.database)
547+
span.set_attribute(DB_STATEMENT, statement)
548+
549+
if self._enable_traceback and (tb := self.get_traceback()):
550+
span.set_attribute(CODE_STACKTRACE, tb)
526551

527552
for (
528553
attribute_key,
@@ -549,6 +574,24 @@ def get_statement(self, cursor: CursorT, args: tuple[Any, ...]): # pylint: disa
549574
return statement.decode("utf8", "replace")
550575
return statement
551576

577+
def get_traceback(self):
578+
filtered_stack_trace = []
579+
for frame, lineno in traceback.walk_stack(
580+
sys._getframe().f_back.f_back.f_back
581+
):
582+
filename = frame.f_code.co_filename
583+
# if frame.f_locals.get("__name__", "").startswith("jobs"):
584+
frame_summary = traceback.FrameSummary(
585+
filename, lineno, frame.f_code.co_name
586+
)
587+
filtered_stack_trace.append(frame_summary)
588+
589+
if filtered_stack_trace:
590+
formatted_stack_trace = traceback.StackSummary.from_list(
591+
filtered_stack_trace
592+
).format()
593+
return "".join(formatted_stack_trace)
594+
552595
def traced_execution(
553596
self,
554597
cursor: CursorT,

instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
from opentelemetry import trace as trace_api
2323
from opentelemetry.instrumentation import dbapi
2424
from opentelemetry.sdk import resources
25+
from opentelemetry.semconv._incubating.attributes.code_attributes import (
26+
CODE_STACKTRACE,
27+
)
2528
from opentelemetry.semconv.trace import SpanAttributes
2629
from opentelemetry.test.test_base import TestBase
2730

@@ -292,6 +295,67 @@ def test_executemany_comment(self):
292295
"Select 1;",
293296
)
294297

298+
def test_enable_traceback(self):
299+
connect_module = mock.MagicMock()
300+
connect_module.__name__ = "test"
301+
connect_module.__version__ = mock.MagicMock()
302+
connect_module.__libpq_version__ = 123
303+
connect_module.apilevel = 123
304+
connect_module.threadsafety = 123
305+
connect_module.paramstyle = "test"
306+
307+
db_integration = dbapi.DatabaseApiIntegration(
308+
"instrumenting_module_test_name",
309+
"postgresql",
310+
enable_traceback=True,
311+
commenter_options={"db_driver": False, "dbapi_level": False},
312+
connect_module=connect_module,
313+
enable_attribute_commenter=True,
314+
)
315+
mock_connection = db_integration.wrapped_connection(
316+
mock_connect, {}, {}
317+
)
318+
cursor = mock_connection.cursor()
319+
cursor.executemany("Select 1;")
320+
321+
spans_list = self.memory_exporter.get_finished_spans()
322+
self.assertEqual(len(spans_list), 1)
323+
span = spans_list[0]
324+
self.assertIsInstance(
325+
span.attributes[CODE_STACKTRACE],
326+
str,
327+
)
328+
329+
def test_disabled_traceback(self):
330+
connect_module = mock.MagicMock()
331+
connect_module.__name__ = "test"
332+
connect_module.__version__ = mock.MagicMock()
333+
connect_module.__libpq_version__ = 123
334+
connect_module.apilevel = 123
335+
connect_module.threadsafety = 123
336+
connect_module.paramstyle = "test"
337+
338+
db_integration = dbapi.DatabaseApiIntegration(
339+
"instrumenting_module_test_name",
340+
"postgresql",
341+
enable_traceback=False,
342+
commenter_options={"db_driver": False, "dbapi_level": False},
343+
connect_module=connect_module,
344+
enable_attribute_commenter=True,
345+
)
346+
mock_connection = db_integration.wrapped_connection(
347+
mock_connect, {}, {}
348+
)
349+
cursor = mock_connection.cursor()
350+
cursor.executemany("Select 1;")
351+
352+
spans_list = self.memory_exporter.get_finished_spans()
353+
self.assertEqual(len(spans_list), 1)
354+
span = spans_list[0]
355+
self.assertIsNone(
356+
span.attributes.get(CODE_STACKTRACE),
357+
)
358+
295359
def test_executemany_comment_stmt_enabled(self):
296360
connect_module = mock.MagicMock()
297361
connect_module.__name__ = "test"

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ def _instrument(self, **kwargs):
199199
enable_attribute_commenter = kwargs.get(
200200
"enable_attribute_commenter", False
201201
)
202+
enable_traceback = kwargs.get("enable_traceback", False)
202203
dbapi.wrap_connect(
203204
__name__,
204205
psycopg2,
@@ -211,6 +212,7 @@ def _instrument(self, **kwargs):
211212
enable_commenter=enable_sqlcommenter,
212213
commenter_options=commenter_options,
213214
enable_attribute_commenter=enable_attribute_commenter,
215+
enable_traceback=enable_traceback,
214216
)
215217

216218
def _uninstrument(self, **kwargs):

0 commit comments

Comments
 (0)