feat: add unit tests for tortoiseorm (#4141)

* feat: add unit tests for tortoiseorm

* update CHANGELOG.md

---------

Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
This commit is contained in:
Lukas Hering
2026-01-26 11:44:46 -05:00
committed by GitHub
parent c1fe0169cc
commit 0b379bef8e
6 changed files with 126 additions and 13 deletions

View File

@@ -49,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4140](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4140))
- `opentelemetry-instrumentation-pyramid` Implement new semantic convention opt-in migration
([#3982](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3982))
- `opentelemetry-instrumentation-tortoiseorm` Add unit tests for Tortoise ORM instrumentation
([#4141](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4141))
- `opentelemetry-instrumentation-pyramid`: pass request attributes at span creation
([#4139](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4139))

View File

@@ -48,7 +48,10 @@ from opentelemetry import trace
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.tortoiseorm.package import _instruments
from opentelemetry.instrumentation.tortoiseorm.version import __version__
from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.instrumentation.utils import (
is_instrumentation_enabled,
unwrap,
)
from opentelemetry.semconv._incubating.attributes.db_attributes import (
DB_NAME,
DB_STATEMENT,
@@ -270,6 +273,9 @@ class TortoiseORMInstrumentor(BaseInstrumentor):
return span_attributes
async def _do_execute(self, func, instance, args, kwargs):
if not is_instrumentation_enabled():
return await func(*args, **kwargs)
exception = None
name = args[0].split()[0]
@@ -297,6 +303,9 @@ class TortoiseORMInstrumentor(BaseInstrumentor):
return result
async def _from_queryset(self, func, modelcls, args, kwargs):
if not is_instrumentation_enabled():
return await func(*args, **kwargs)
exception = None
name = f"pydantic.{func.__name__}"

View File

@@ -7,14 +7,14 @@ iso8601==1.1.0
packaging==24.0
pluggy==1.5.0
py-cpuinfo==9.0.0
pydantic==2.8.2
pydantic_core==2.20.1
pydantic==2.12.5
pydantic_core==2.41.5
pypika-tortoise==0.1.6
pytest==7.4.4
pytz==2024.1
tomli==2.0.1
tortoise-orm==0.20.0
typing_extensions==4.12.2
typing_extensions==4.14.1
wrapt==1.16.0
zipp==3.19.2
-e opentelemetry-instrumentation

View File

@@ -12,10 +12,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
from tortoise import Tortoise, fields, models
from opentelemetry import trace
from opentelemetry.instrumentation.tortoiseorm import TortoiseORMInstrumentor
from opentelemetry.instrumentation.utils import suppress_instrumentation
from opentelemetry.semconv._incubating.attributes.db_attributes import (
DB_NAME,
DB_STATEMENT,
DB_SYSTEM,
)
from opentelemetry.test.test_base import TestBase
# pylint: disable=too-many-public-methods
class MockModel(models.Model):
id = fields.IntField(pk=True)
name = fields.TextField()
def __str__(self):
return self.name
class TestTortoiseORMInstrumentor(TestBase):
@@ -26,9 +43,94 @@ class TestTortoiseORMInstrumentor(TestBase):
def tearDown(self):
super().tearDown()
TortoiseORMInstrumentor().uninstrument()
self._async_call(Tortoise._drop_databases())
def test_tortoise(self):
# FIXME This instrumentation has no tests at all and should have some
# tests. This is being added just to make pytest not fail because no
# tests are being collected for tortoise at the moment.
pass
# pylint: disable-next=no-self-use
def _async_call(self, coro):
return asyncio.run(coro)
# pylint: disable-next=no-self-use
async def _init_tortoise(self):
await Tortoise.init(
db_url="sqlite://:memory:",
modules={"models": [__name__]},
)
await Tortoise.generate_schemas()
def test_trace_integration(self):
async def run():
await self._init_tortoise()
await MockModel.create(name="Test 1")
await MockModel.filter(name="Test 1").first()
self._async_call(run())
spans = self.memory_exporter.get_finished_spans()
crud_spans = [s for s in spans if s.name in ("INSERT", "SELECT")]
self.assertGreaterEqual(len(crud_spans), 2)
insert_span = next(s for s in crud_spans if s.name == "INSERT")
self.assertEqual(insert_span.kind, trace.SpanKind.CLIENT)
self.assertEqual(insert_span.attributes[DB_SYSTEM], "sqlite")
self.assertEqual(insert_span.attributes[DB_NAME], ":memory:")
self.assertIn("INSERT", insert_span.attributes[DB_STATEMENT])
select_span = next(s for s in crud_spans if s.name == "SELECT")
self.assertEqual(select_span.kind, trace.SpanKind.CLIENT)
self.assertEqual(select_span.attributes[DB_SYSTEM], "sqlite")
self.assertEqual(select_span.attributes[DB_NAME], ":memory:")
self.assertIn("SELECT", select_span.attributes[DB_STATEMENT])
def test_capture_parameters(self):
TortoiseORMInstrumentor().uninstrument()
TortoiseORMInstrumentor().instrument(capture_parameters=True)
async def run():
await self._init_tortoise()
await MockModel.create(name="Test Parameterized")
self._async_call(run())
spans = self.memory_exporter.get_finished_spans()
insert_span = next(s for s in spans if s.name == "INSERT")
self.assertIn("db.statement.parameters", insert_span.attributes)
self.assertIn(
"Test Parameterized",
insert_span.attributes["db.statement.parameters"],
)
def test_uninstrument(self):
TortoiseORMInstrumentor().uninstrument()
async def run():
await self._init_tortoise()
await MockModel.create(name="Test Uninstrumented")
self._async_call(run())
spans = self.memory_exporter.get_finished_spans()
crud_spans = [s for s in spans if s.name in ("INSERT", "SELECT")]
self.assertEqual(len(crud_spans), 0)
def test_suppress_instrumentation(self):
async def run():
await self._init_tortoise()
with suppress_instrumentation():
await MockModel.create(name="Test Suppressed")
self._async_call(run())
spans = self.memory_exporter.get_finished_spans()
crud_spans = [s for s in spans if s.name in ("INSERT", "SELECT")]
self.assertEqual(len(crud_spans), 0)
def test_no_op_tracer_provider(self):
TortoiseORMInstrumentor().uninstrument()
TortoiseORMInstrumentor().instrument(
tracer_provider=trace.NoOpTracerProvider()
)
async def run():
await self._init_tortoise()
await MockModel.create(name="Test NoOp")
self._async_call(run())
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 0)

View File

@@ -57,7 +57,7 @@ dependencies = [
"opentelemetry-instrumentation-system-metrics",
"opentelemetry-instrumentation-threading",
"opentelemetry-instrumentation-tornado",
"opentelemetry-instrumentation-tortoiseorm",
"opentelemetry-instrumentation-tortoiseorm[instruments]",
"opentelemetry-instrumentation-urllib",
"opentelemetry-instrumentation-urllib3[instruments]",
"opentelemetry-instrumentation-wsgi",

4
uv.lock generated
View File

@@ -4134,7 +4134,7 @@ dependencies = [
{ name = "opentelemetry-instrumentation-system-metrics" },
{ name = "opentelemetry-instrumentation-threading" },
{ name = "opentelemetry-instrumentation-tornado" },
{ name = "opentelemetry-instrumentation-tortoiseorm" },
{ name = "opentelemetry-instrumentation-tortoiseorm", extra = ["instruments"] },
{ name = "opentelemetry-instrumentation-urllib" },
{ name = "opentelemetry-instrumentation-urllib3", extra = ["instruments"] },
{ name = "opentelemetry-instrumentation-vertexai", extra = ["instruments"] },
@@ -4208,7 +4208,7 @@ requires-dist = [
{ name = "opentelemetry-instrumentation-system-metrics", editable = "instrumentation/opentelemetry-instrumentation-system-metrics" },
{ name = "opentelemetry-instrumentation-threading", editable = "instrumentation/opentelemetry-instrumentation-threading" },
{ name = "opentelemetry-instrumentation-tornado", editable = "instrumentation/opentelemetry-instrumentation-tornado" },
{ name = "opentelemetry-instrumentation-tortoiseorm", editable = "instrumentation/opentelemetry-instrumentation-tortoiseorm" },
{ name = "opentelemetry-instrumentation-tortoiseorm", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-tortoiseorm" },
{ name = "opentelemetry-instrumentation-urllib", editable = "instrumentation/opentelemetry-instrumentation-urllib" },
{ name = "opentelemetry-instrumentation-urllib3", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-urllib3" },
{ name = "opentelemetry-instrumentation-vertexai", extras = ["instruments"], editable = "instrumentation-genai/opentelemetry-instrumentation-vertexai" },