diff --git a/logfire/__init__.py b/logfire/__init__.py index 60b2c160e..eeb9104dd 100644 --- a/logfire/__init__.py +++ b/logfire/__init__.py @@ -41,6 +41,7 @@ instrument_sqlalchemy = DEFAULT_LOGFIRE_INSTANCE.instrument_sqlalchemy instrument_redis = DEFAULT_LOGFIRE_INSTANCE.instrument_redis instrument_pymongo = DEFAULT_LOGFIRE_INSTANCE.instrument_pymongo +instrument_sqlite3 = DEFAULT_LOGFIRE_INSTANCE.instrument_sqlite3 shutdown = DEFAULT_LOGFIRE_INSTANCE.shutdown with_tags = DEFAULT_LOGFIRE_INSTANCE.with_tags # with_trace_sample_rate = DEFAULT_LOGFIRE_INSTANCE.with_trace_sample_rate diff --git a/logfire/_internal/integrations/sqlite3.py b/logfire/_internal/integrations/sqlite3.py new file mode 100644 index 000000000..c56a9e38c --- /dev/null +++ b/logfire/_internal/integrations/sqlite3.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from typing import Any + +try: + from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor +except ModuleNotFoundError: + raise RuntimeError( + 'The `logfire.instrument_sqlite3()` requires the `opentelemetry-instrumentation-sqlite3` package.\n' + 'You can install this with:\n' + " pip install 'logfire[sqlite3]'" + ) + + +def instrument_sqlite3(**kwargs: Any): + """Instrument the `sqlite3` module so that spans are automatically created for each query. + + See the `Logfire.instrument_sqlite3` method for details. + """ + SQLite3Instrumentor().instrument(**kwargs) # type: ignore[reportUnknownMemberType] diff --git a/logfire/_internal/main.py b/logfire/_internal/main.py index fbbce5dd0..f9f28d99d 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -1199,6 +1199,11 @@ def instrument_redis(self, **kwargs: Any): self._warn_if_not_initialized_for_instrumentation() return instrument_redis(**kwargs) + def instrument_sqlite3(self, **kwargs: Any): + from .integrations.sqlite3 import instrument_sqlite3 + + return instrument_sqlite3(**kwargs) + def metric_counter(self, name: str, *, unit: str = '', description: str = '') -> Counter: """Create a counter metric. diff --git a/pyproject.toml b/pyproject.toml index 5d07b6a5a..e26cd5c04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,9 @@ psycopg2 = ["opentelemetry-instrumentation-psycopg2 >= 0.42b0", "packaging"] pymongo = ["opentelemetry-instrumentation-pymongo >= 0.42b0"] redis = ["opentelemetry-instrumentation-redis >= 0.42b0"] requests = ["opentelemetry-instrumentation-requests >= 0.42b0"] +sqlite3 = [ + "opentelemetry-instrumentation-sqlite3>=0.46b0", +] [project.scripts] logfire = "logfire.cli:main" @@ -134,6 +137,7 @@ dev-dependencies = [ "anthropic>=0.27.0", # Can remove this when https://github.com/python/typing_extensions/commit/53bcdded534494674f893112f71d3be344d65363 is released "typing-extensions<4.12", + "opentelemetry-instrumentation-sqlite3>=0.46b0", ] [tool.rye.scripts] diff --git a/requirements-dev.lock b/requirements-dev.lock index 227b25966..8559c777e 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -6,7 +6,6 @@ # features: [] # all-features: false # with-sources: false -# generate-hashes: false -e file:. aiohttp==3.9.5 @@ -56,6 +55,7 @@ colorama==0.4.6 # via griffe # via mkdocs-material coverage==7.5.3 + # via coverage deprecated==1.2.14 # via opentelemetry-api # via opentelemetry-exporter-otlp-proto-http @@ -91,6 +91,8 @@ ghp-import==2.1.0 # via mkdocs googleapis-common-protos==1.63.1 # via opentelemetry-exporter-otlp-proto-http +greenlet==3.0.3 + # via sqlalchemy griffe==0.45.2 # via mkdocstrings-python h11==0.14.0 @@ -193,6 +195,7 @@ opentelemetry-api==1.25.0 # via opentelemetry-instrumentation-redis # via opentelemetry-instrumentation-requests # via opentelemetry-instrumentation-sqlalchemy + # via opentelemetry-instrumentation-sqlite3 # via opentelemetry-instrumentation-starlette # via opentelemetry-instrumentation-system-metrics # via opentelemetry-instrumentation-wsgi @@ -218,6 +221,7 @@ opentelemetry-instrumentation==0.46b0 # via opentelemetry-instrumentation-redis # via opentelemetry-instrumentation-requests # via opentelemetry-instrumentation-sqlalchemy + # via opentelemetry-instrumentation-sqlite3 # via opentelemetry-instrumentation-starlette # via opentelemetry-instrumentation-system-metrics # via opentelemetry-instrumentation-wsgi @@ -229,6 +233,7 @@ opentelemetry-instrumentation-asyncpg==0.46b0 opentelemetry-instrumentation-dbapi==0.46b0 # via opentelemetry-instrumentation-psycopg # via opentelemetry-instrumentation-psycopg2 + # via opentelemetry-instrumentation-sqlite3 opentelemetry-instrumentation-django==0.46b0 opentelemetry-instrumentation-fastapi==0.46b0 opentelemetry-instrumentation-flask==0.46b0 @@ -239,6 +244,7 @@ opentelemetry-instrumentation-pymongo==0.46b0 opentelemetry-instrumentation-redis==0.46b0 opentelemetry-instrumentation-requests==0.46b0 opentelemetry-instrumentation-sqlalchemy==0.46b0 +opentelemetry-instrumentation-sqlite3==0.46b0 opentelemetry-instrumentation-starlette==0.46b0 opentelemetry-instrumentation-system-metrics==0.46b0 opentelemetry-instrumentation-wsgi==0.46b0 @@ -363,8 +369,6 @@ rich==13.7.1 # via pytest-pretty # via typer ruff==0.4.8 -setuptools==70.0.0 - # via opentelemetry-instrumentation shellingham==1.5.4 # via typer six==1.16.0 @@ -412,6 +416,7 @@ urllib3==2.2.1 # via requests uvicorn==0.30.1 # via fastapi + # via uvicorn uvloop==0.19.0 # via uvicorn virtualenv==20.26.2 @@ -435,3 +440,5 @@ yarl==1.9.4 # via aiohttp zipp==3.19.2 # via importlib-metadata +setuptools==69.5.1 + # via opentelemetry-instrumentation diff --git a/requirements.lock b/requirements.lock index cf843d6ff..bfd9d5a9c 100644 --- a/requirements.lock +++ b/requirements.lock @@ -6,7 +6,6 @@ # features: [] # all-features: false # with-sources: false -# generate-hashes: false -e file:. certifi==2024.6.2 @@ -57,8 +56,6 @@ requests==2.32.3 # via opentelemetry-exporter-otlp-proto-http rich==13.7.1 # via logfire -setuptools==70.0.0 - # via opentelemetry-instrumentation typing-extensions==4.11.0 # via logfire # via opentelemetry-sdk @@ -69,3 +66,5 @@ wrapt==1.16.0 # via opentelemetry-instrumentation zipp==3.19.2 # via importlib-metadata +setuptools==69.5.1 + # via opentelemetry-instrumentation diff --git a/tests/otel_integrations/test_sqlite3.py b/tests/otel_integrations/test_sqlite3.py new file mode 100644 index 000000000..17e64004b --- /dev/null +++ b/tests/otel_integrations/test_sqlite3.py @@ -0,0 +1,33 @@ +import importlib +import sqlite3 +from unittest import mock + +import pytest +from inline_snapshot import snapshot + +import logfire +import logfire._internal +import logfire._internal.integrations +import logfire._internal.integrations.sqlite3 +from logfire import instrument_sqlite3 +from logfire._internal.integrations.sqlite3 import SQLite3Instrumentor + + +def test_missing_opentelemetry_dependency() -> None: + with mock.patch.dict('sys.modules', {'opentelemetry.instrumentation.sqlite3': None}): + with pytest.raises(RuntimeError) as exc_info: + importlib.reload(logfire._internal.integrations.sqlite3) + assert str(exc_info.value) == snapshot("""\ +The `logfire.instrument_sqlite3()` requires the `opentelemetry-instrumentation-sqlite3` package. +You can install this with: + pip install 'logfire[sqlite3]'\ +""") + + +def test_instrument_sqlite3(): + original_connect = sqlite3.connect + + instrument_sqlite3() + assert original_connect is not sqlite3.connect + SQLite3Instrumentor().uninstrument() # type: ignore + assert original_connect is sqlite3.connect