Skip to content

SQLAlchemy instrumentation does not respect suppress_instrumentation functionality #3454

Closed
@marcm-ml

Description

@marcm-ml

Describe your environment

MacOS
python 3.12
opentelemetry-instrumentation-sqlalchemy v0.53b1
opentelemetry-api v1.32.1
opentelemetry-instrumentation v0.53b1
opentelemetry-semantic-conventions v0.53b1

What happened?

We have some healthcheck functions that produce spans from sqlalchemy instrumentation we would like to suppress.
In order to do that, I guess it is best practice to use the suppress_instrumentation method from opentelemetry.instrumentations.util module. However, the SQLAlchemy instrumentation does not respect this and still produces spans and metrics.

Steps to Reproduce

using uv you can simply run this as is with uv run reproduce.py

reproduce.py:

# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "opentelemetry-api",
#     "opentelemetry-instrumentation-sqlalchemy",
#     "opentelemetry-sdk",
#     "sqlalchemy",
# ]
# ///
from opentelemetry import trace
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.utils import suppress_instrumentation
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from sqlalchemy import create_engine

exporter = ConsoleSpanExporter()
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)

engine = create_engine("sqlite:///:memory:")
SQLAlchemyInstrumentor().instrument(engine=engine)

with engine.connect():  # produces spans
    pass

with suppress_instrumentation(), engine.connect():  # should not produce spans but does
    pass

Expected Result

OTEL signals should not be produced when suppress_instrumentation is used.

Actual Result

OTEL signals are produced when suppress_instrumentation is used

Additional context

A fix can be implemented by using the is_instrumentation_enabled from opentelemetry.instrumentation.utils module everywhere in the engine.py module, i.e.:

engine.py

def _wrap_connect(tracer):
    # pylint: disable=unused-argument
    def _wrap_connect_internal(func, module, args, kwargs):
        if not is_instrumentation_enabled(): # this is new
            return func(*args, **kwargs)

        with tracer.start_as_current_span(
            "connect", kind=trace.SpanKind.CLIENT
        ) as span:
            if span.is_recording():
                attrs, _ = _get_attributes_from_url(module.url)
                span.set_attributes(attrs)
                span.set_attribute(
                    SpanAttributes.DB_SYSTEM, _normalize_vendor(module.name)
                )
            return func(*args, **kwargs)

    return _wrap_connect_internal

If i use the code above and the reproduction script, I get the expected result where only 1 span is produced.

Would you like to implement a fix?

Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions