Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dbapi: Add db.collection.name, use connection kwargs for connection attributes #2869

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a737fc7
feat: augment span name, add span attributes
keithZmudzinski Sep 12, 2024
b54bbca
chore: only add attributes if they are present
keithZmudzinski Sep 12, 2024
f2971f4
tests: add tests for collection name, span name
keithZmudzinski Sep 12, 2024
e32234e
chore: lint
keithZmudzinski Sep 12, 2024
95ed7bb
docs: add to changelog
keithZmudzinski Sep 12, 2024
38029b5
chore: be case insensitive
keithZmudzinski Sep 12, 2024
f70c613
Merge branch 'main' into dbapi-more-attributes
keithZmudzinski Sep 12, 2024
ecab5a3
Merge branch 'main' into dbapi-more-attributes
keithZmudzinski Sep 13, 2024
ba0466a
fix: don't allow single quotes around collection name
keithZmudzinski Sep 13, 2024
038cf47
Merge branch 'dbapi-more-attributes' of github.com:zyBooks/openteleme…
keithZmudzinski Sep 13, 2024
1b78b97
Merge branch 'main' into dbapi-more-attributes
keithZmudzinski Sep 24, 2024
09d761d
chore: allow for collection names with schema qualifiers
keithZmudzinski Sep 24, 2024
fbc0faf
test: test schema name and quote removal
keithZmudzinski Sep 24, 2024
93f13bf
docs: move contribution to correct release
keithZmudzinski Sep 24, 2024
65628c4
Merge branch 'main' into dbapi-more-attributes
keithZmudzinski Sep 26, 2024
a9ed2b5
chore: don't break existing subclasses in consuming libraries
keithZmudzinski Sep 30, 2024
68095ef
Merge branch 'dbapi-more-attributes' of github.com:zyBooks/openteleme…
keithZmudzinski Sep 30, 2024
a8273b6
chore: remove unused call
keithZmudzinski Sep 30, 2024
192d202
test: update number of tests and expected names
keithZmudzinski Oct 7, 2024
0329dad
chore: lint
keithZmudzinski Oct 7, 2024
6e9117d
chore: merge conflict
keithZmudzinski Oct 7, 2024
b2b6849
Merge branch 'main' into dbapi-more-attributes
keithZmudzinski Oct 14, 2024
4df2fc4
test: update sqlite3 tests, correctly handle CREATE TABLE
keithZmudzinski Oct 14, 2024
ef5719b
Merge branch 'dbapi-more-attributes' of github.com:zyBooks/openteleme…
keithZmudzinski Oct 14, 2024
2b8cdea
fix: update remaining tests to expect collection name
keithZmudzinski Oct 14, 2024
cbe2aae
Merge branch 'main' into dbapi-more-attributes
keithZmudzinski Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2082](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2082))
- `opentelemetry-instrumentation-redis` Add additional attributes for methods create_index and search, rename those spans
([#2635](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2635))
- `opentelemetry-instrumentation-dbapi` Add db.collection.name, use connection kwargs for connection attributes
([#2869](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2869))

### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
_get_opentelemetry_values,
unwrap,
)
from opentelemetry.semconv._incubating.attributes.db_attributes import (
DB_COLLECTION_NAME,
)
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer

Expand Down Expand Up @@ -217,7 +220,7 @@ def instrument_connection(
enable_commenter=enable_commenter,
commenter_options=commenter_options,
)
db_integration.get_connection_attributes(connection)
db_integration.get_connection_attributes(connection=connection)
return get_traced_connection_proxy(connection, db_integration)


Expand Down Expand Up @@ -284,12 +287,17 @@ def wrapped_connection(
):
"""Add object proxy to connection object."""
connection = connect_method(*args, **kwargs)
self.get_connection_attributes(connection)
self.get_connection_attributes(connection=connection, kwargs=kwargs)
return get_traced_connection_proxy(connection, self)

def get_connection_attributes(self, connection):
# Populate span fields using connection
def get_connection_attributes(self, connection, kwargs=None):
# Populate span fields using kwargs and connection
for key, value in self.connection_attributes.items():
# First set from kwargs
if kwargs and value in kwargs:
self.connection_props[key] = kwargs.get(value)
Comment on lines +296 to +298
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting in this PR!

  1. Uses more sources for the db.user, net.peer.name, and net.peer.port span attributes.

Could some tests be added to show expectations for this?


# Then override from connection object
# Allow attributes nested in connection object
attribute = functools.reduce(
lambda attribute, attribute_value: getattr(
Expand Down Expand Up @@ -373,14 +381,19 @@ def _populate_span(
):
if not span.is_recording():
return

statement = self.get_statement(cursor, args)
collection_name = self.get_collection_name(statement)

span.set_attribute(
SpanAttributes.DB_SYSTEM, self._db_api_integration.database_system
)
span.set_attribute(
SpanAttributes.DB_NAME, self._db_api_integration.database
)
span.set_attribute(SpanAttributes.DB_STATEMENT, statement)
if collection_name:
span.set_attribute(DB_COLLECTION_NAME, collection_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But as I said in the comments before, I wouldn't introduce new semantic convention attributes in this PR.


for (
attribute_key,
Expand All @@ -391,12 +404,32 @@ def _populate_span(
if self._db_api_integration.capture_parameters and len(args) > 1:
span.set_attribute("db.statement.parameters", str(args[1]))

def get_operation_name(self, cursor, args): # pylint: disable=no-self-use
def get_span_name(self, cursor, args):
operation_name = self.get_operation_name(cursor, args)
statement = self.get_statement(cursor, args)
collection_name = CursorTracer.get_collection_name(statement)
return " ".join(
name for name in (operation_name, collection_name) if name
)
Comment on lines +411 to +413
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these changes also implement these parts of the spec?

If there is no (low-cardinality) db.operation.name available, database span names SHOULD be {target}.

If neither {db.operation.name} nor {target} are available, span name SHOULD be {db.system}.

I think Yes for the first point, would be good to see some test cases for that as well.


def get_operation_name(self, cursor, args):
if args and isinstance(args[0], str):
# Strip leading comments so we get the operation name.
return self._leading_comment_remover.sub("", args[0]).split()[0]
return ""

@staticmethod
def get_collection_name(statement):
collection_name = ""
match = re.search(
r"\b(?:FROM|JOIN|INTO|UPDATE|TABLE(?: IF NOT EXISTS)?)\s+(['`]?(\w+)['`]?(?:\s*\.\s*['`]?(\w+)['`]?)?)",
statement,
)
if match:
collection_name = match.group(1)

return collection_name

def get_statement(self, cursor, args): # pylint: disable=no-self-use
if not args:
return ""
Expand All @@ -412,7 +445,7 @@ def traced_execution(
*args: typing.Tuple[typing.Any, typing.Any],
**kwargs: typing.Dict[typing.Any, typing.Any],
):
name = self.get_operation_name(cursor, args)
name = self.get_span_name(cursor, args)
if not name:
name = (
self._db_api_integration.database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
from opentelemetry import trace as trace_api
from opentelemetry.instrumentation import dbapi
from opentelemetry.sdk import resources
from opentelemetry.semconv._incubating.attributes.db_attributes import (
DB_COLLECTION_NAME,
)
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.test.test_base import TestBase

Expand Down Expand Up @@ -49,11 +52,12 @@ def test_span_succeeded(self):
mock_connect, {}, connection_props
)
cursor = mock_connection.cursor()
cursor.execute("Test query", ("param1Value", False))
expected_query = "Test query FROM test_table"
cursor.execute(expected_query, ("param1Value", False))
spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)
span = spans_list[0]
self.assertEqual(span.name, "Test")
self.assertEqual(span.name, "Test test_table")
self.assertIs(span.kind, trace_api.SpanKind.CLIENT)

self.assertEqual(
Expand All @@ -63,8 +67,9 @@ def test_span_succeeded(self):
span.attributes[SpanAttributes.DB_NAME], "testdatabase"
)
self.assertEqual(
span.attributes[SpanAttributes.DB_STATEMENT], "Test query"
span.attributes[SpanAttributes.DB_STATEMENT], expected_query
)
self.assertEqual(span.attributes[DB_COLLECTION_NAME], "test_table")
self.assertFalse("db.statement.parameters" in span.attributes)
self.assertEqual(span.attributes[SpanAttributes.DB_USER], "testuser")
self.assertEqual(
Expand All @@ -91,14 +96,22 @@ def test_span_name(self):
cursor.execute("/* leading comment */ query")
cursor.execute("/* leading comment */ query /* trailing comment */")
cursor.execute("query /* trailing comment */")
cursor.execute("SELECT * FROM test_table")
cursor.execute("SELECT * FROM schema.test_table")
cursor.execute("SELECT * FROM `schema`.`test_table`")
cursor.execute("SELECT * FROM 'schema'.'test_table'")
spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 6)
self.assertEqual(len(spans_list), 10)
self.assertEqual(spans_list[0].name, "Test")
self.assertEqual(spans_list[1].name, "multi")
self.assertEqual(spans_list[2].name, "tab")
self.assertEqual(spans_list[3].name, "query")
self.assertEqual(spans_list[4].name, "query")
self.assertEqual(spans_list[5].name, "query")
self.assertEqual(spans_list[6].name, "SELECT test_table")
self.assertEqual(spans_list[7].name, "SELECT schema.test_table")
self.assertEqual(spans_list[8].name, "SELECT `schema`.`test_table`")
self.assertEqual(spans_list[9].name, "SELECT 'schema'.'test_table'")

def test_span_succeeded_with_capture_of_statement_parameters(self):
connection_props = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ def test_execute(self):
stmt = "CREATE TABLE IF NOT EXISTS test (id integer)"
with self._tracer.start_as_current_span("rootSpan"):
self._cursor.execute(stmt)
self.validate_spans("CREATE")
self.validate_spans("CREATE test")

with self._tracer.start_as_current_span("rootSpan"):
self._cursor2.execute(stmt)
self.validate_spans("CREATE")
self.validate_spans("CREATE test")

def test_executemany(self):
"""Should create a child span for executemany"""
Expand All @@ -87,11 +87,11 @@ def test_executemany(self):
data = [("1",), ("2",), ("3",)]
with self._tracer.start_as_current_span("rootSpan"):
self._cursor.executemany(stmt, data)
self.validate_spans("INSERT")
self.validate_spans("INSERT test")

with self._tracer.start_as_current_span("rootSpan"):
self._cursor2.executemany(stmt, data)
self.validate_spans("INSERT")
self.validate_spans("INSERT test")

def test_callproc(self):
"""Should create a child span for callproc"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_execute(self):
stmt = "CREATE TABLE IF NOT EXISTS test (id INT)"
with self._tracer.start_as_current_span("rootSpan"):
self._cursor.execute(stmt)
self.validate_spans("CREATE")
self.validate_spans("CREATE test")

def test_execute_with_connection_context_manager(self):
"""Should create a child span for execute with connection context"""
Expand All @@ -93,23 +93,23 @@ def test_execute_with_connection_context_manager(self):
with self._connection as conn:
cursor = conn.cursor()
cursor.execute(stmt)
self.validate_spans("CREATE")
self.validate_spans("CREATE test")

def test_execute_with_cursor_context_manager(self):
"""Should create a child span for execute with cursor context"""
stmt = "CREATE TABLE IF NOT EXISTS test (id INT)"
with self._tracer.start_as_current_span("rootSpan"):
with self._connection.cursor() as cursor:
cursor.execute(stmt)
self.validate_spans("CREATE")
self.validate_spans("CREATE test")

def test_executemany(self):
"""Should create a child span for executemany"""
stmt = "INSERT INTO test (id) VALUES (%s)"
with self._tracer.start_as_current_span("rootSpan"):
data = (("1",), ("2",), ("3",))
self._cursor.executemany(stmt, data)
self.validate_spans("INSERT")
self.validate_spans("INSERT test")

def test_callproc(self):
"""Should create a child span for callproc"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,23 @@ def test_execute(self):
stmt = "CREATE TABLE IF NOT EXISTS test (id INT)"
with self._tracer.start_as_current_span("rootSpan"):
self._cursor.execute(stmt)
self.validate_spans("CREATE")
self.validate_spans("CREATE test")
Copy link
Member

@emdneto emdneto Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having some trouble running the docker-tests on my machine. Before I dive into it @emdneto , I just wanted to make sure that the failing test isn't flaky as far as you know?

@keithZmudzinski, they are legit tests that need to be fixed if we want this PR to be merged.


def test_execute_with_cursor_context_manager(self):
"""Should create a child span for execute with cursor context"""
stmt = "CREATE TABLE IF NOT EXISTS test (id INT)"
with self._tracer.start_as_current_span("rootSpan"):
with self._connection.cursor() as cursor:
cursor.execute(stmt)
self.validate_spans("CREATE")
self.validate_spans("CREATE test")

def test_executemany(self):
"""Should create a child span for executemany"""
stmt = "INSERT INTO test (id) VALUES (%s)"
with self._tracer.start_as_current_span("rootSpan"):
data = (("1",), ("2",), ("3",))
self._cursor.executemany(stmt, data)
self.validate_spans("INSERT")
self.validate_spans("INSERT test")

def test_callproc(self):
"""Should create a child span for callproc"""
Expand All @@ -116,12 +116,12 @@ def test_commit(self):
data = (("4",), ("5",), ("6",))
self._cursor.executemany(stmt, data)
self._connection.commit()
self.validate_spans("INSERT")
self.validate_spans("INSERT test")

def test_rollback(self):
stmt = "INSERT INTO test (id) VALUES (%s)"
with self._tracer.start_as_current_span("rootSpan"):
data = (("7",), ("8",), ("9",))
self._cursor.executemany(stmt, data)
self._connection.rollback()
self.validate_spans("INSERT")
self.validate_spans("INSERT test")
Loading