open-telemetry / open-telemetry/opentelemetry-python-contrib
[SQLAlchemy] Instrumentor can fail silently due to import order sensitivity
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 1.1k
- Avg merge
- 4d 15h
- Merged PRs (30d)
- 16
Description
Describe your environment
OS: Ubuntu
Python version: Python 3.12.3
Package version: opentelemetry-instrumentation-sqlalchemy 0.57b0
What happened?
The instrumentor works by patching sqlalchemy.create_engine. If a user imports create_engine before calling .instrument(), the patch is applied too late and has no effect. The code runs without errors, but no spans are generated for queries. This seems like an issue because having imports at the top of a file is standard Python practice.
Steps to Reproduce
# 1. Import necessary OpenTelemetry components
from opentelemetry import trace
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
ConsoleSpanExporter,
SimpleSpanProcessor,
)
from sqlalchemy import create_engine, text
# 2. Set up the TracerProvider, Processor, and Exporter
# This is the boilerplate needed to see spans printed to the console.
provider = TracerProvider()
processor = SimpleSpanProcessor(ConsoleSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
# Get a tracer for the instrumentor
tracer_provider = trace.get_tracer_provider()
# --- Your original script starts here ---
print("--- Instrumenting SQLAlchemy ---")
# We now pass the tracer_provider to the instrumentor
SQLAlchemyInstrumentor().instrument(tracer_provider=tracer_provider)
print("-" * 50)
# The import is here to demonstrate the silent failure
# from sqlalchemy import create_engine, text
print("--- Creating two separate database engines ---")
engine1 = create_engine("sqlite:///:memory:")
engine2 = create_engine("sqlite:///:memory:")
print("Engines created.")
print("-" * 50)
print("--- Performing operations on engine1 ---")
try:
with engine1.connect() as connection1:
print("Creating table 'users' on engine1...")
connection1.execute(text("CREATE TABLE users (id INT, name VARCHAR)"))
print("Inserting data into 'users' on engine1...")
connection1.execute(text("INSERT INTO users (id, name) VALUES (1, 'Alice')"))
connection1.commit()
print("Querying data from 'users' on engine1...")
result = connection1.execute(text("SELECT * FROM users")).first()
print(f"Query result from engine1: {result}")
except Exception as e:
print(f"An error occurred with engine1: {e}")
print("-" * 50)
print("--- Performing a simple query on engine2 ---")
try:
with engine2.connect() as connection2:
print("Executing 'SELECT 1' on engine2...")
result = connection2.execute(text("SELECT 1")).scalar()
print(f"Query result from engine2: {result}")
except Exception as e:
print(f"An error occurred with engine2: {e}")
print("-" * 50)
print("\n--- SCRIPT FINISHED ---")
print("Observe the console output above.")
print("Expected: Spans for connect, create, insert, and select operations.")
print(
"Actual: You will only see spans for the 'connect' calls because of the import order."
)
Expected Result
Spans for connect, create, insert, and select operations on the output.
Actual Result
--- Instrumenting SQLAlchemy ---
--------------------------------------------------
--- Creating two separate database engines ---
Engines created.
--------------------------------------------------
--- Performing operations on engine1 ---
{
"name": "connect",
"context": {
"trace_id": "0x84ec80f248d5179410a1ba89b94d2b65",
"span_id": "0x883bbedb7836172b",
"trace_state": "[]"
},
"kind": "SpanKind.CLIENT",
"parent_id": null,
"start_time": "2025-08-15T20:43:15.560176Z",
"end_time": "2025-08-15T20:43:15.560666Z",
"status": {
"status_code": "UNSET"
},
"attributes": {
"db.name": ":memory:",
"db.system": "sqlite"
},
"events": [],
"links": [],
"resource": {
"attributes": {
"telemetry.sdk.language": "python",
"telemetry.sdk.name": "opentelemetry",
"telemetry.sdk.version": "1.36.0",
"service.name": "unknown_service"
},
"schema_url": ""
}
}
Creating table 'users' on engine1...
Inserting data into 'users' on engine1...
Querying data from 'users' on engine1...
Query result from engine1: (1, 'Alice')
--------------------------------------------------
--- Performing a simple query on engine2 ---
{
"name": "connect",
"context": {
"trace_id": "0x0be18387d6a07d0881267b3aa7fb6970",
"span_id": "0x0cdb909884ab7ab5",
"trace_state": "[]"
},
"kind": "SpanKind.CLIENT",
"parent_id": null,
"start_time": "2025-08-15T20:43:15.561977Z",
"end_time": "2025-08-15T20:43:15.562217Z",
"status": {
"status_code": "UNSET"
},
"attributes": {
"db.name": ":memory:",
"db.system": "sqlite"
},
"events": [],
"links": [],
"resource": {
"attributes": {
"telemetry.sdk.language": "python",
"telemetry.sdk.name": "opentelemetry",
"telemetry.sdk.version": "1.36.0",
"service.name": "unknown_service"
},
"schema_url": ""
}
}
Executing 'SELECT 1' on engine2...
Query result from engine2: 1
--------------------------------------------------
--- SCRIPT FINISHED ---
Observe the console output above.
Expected: Spans for connect, create, insert, and select operations.
Actual: You will only see spans for the 'connect' calls because of the import order.
Additional context
After investigating issues #1648 and #1860, it seems to me SQLAlchemyInstrumentor().instrument() can fail silently because of the import order. if you move the line from sqlalchemy import create_engine, text until after the instrument line, all of the spans are registered.
Instead of failing silently, the .instrument() method should detect this situation. A potential fix would be to check if sqlalchemy.create_engine has already been imported into another module's namespace. If so, it should raise a RuntimeWarning that explains the problem and suggests a solution. Maybe there is something else that could be done to patch the create_engine method even with the wrong import order, but I don't know.
Would you like to implement a fix?
None
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at SQLAlchemyInstrumentor().instrument() and the handling of sqlalchemy.create_engine, then reproduce the reported import order with the example in the issue. Review issues #1648 and #1860 for related behavior. Done should be covered by a regression test showing that the chosen behavior prevents or clearly reports the silent loss of query spans.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, sqlalchemy
- Domain
- databases, observability-sre
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100