Source code for opentelemetry.instrumentation.sqlalchemy

# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

"""
Instrument `sqlalchemy`_ to report SQL queries.

There are two options for instrumenting code. The first option is to use
the ``opentelemetry-instrument`` executable which will automatically
instrument your SQLAlchemy engine. The second is to programmatically enable
instrumentation via the following code:

.. _sqlalchemy: https://pypi.org/project/sqlalchemy/

Usage
-----
.. code:: python

    from sqlalchemy import create_engine

    from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
    import sqlalchemy

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

.. code:: python

    # of the async variant of SQLAlchemy

    from sqlalchemy.ext.asyncio import create_async_engine

    from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
    import sqlalchemy

    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
    SQLAlchemyInstrumentor().instrument(
        engine=engine.sync_engine
    )

Configuration
-------------

SQLCommenter
************
You can optionally enable sqlcommenter which enriches the query with contextual
information. Queries made after setting up trace integration with sqlcommenter
enabled will have configurable key-value pairs appended to them, e.g.
``"select * from auth_users; /*traceparent=00-01234567-abcd-01*/"``. This
supports context propagation between database client and server when database log
records are enabled. For more information, see:

* `Semantic Conventions - Database Spans <https://github.com/open-telemetry/semantic-conventions/blob/main/docs/db/database-spans.md#sql-commenter>`_
* `sqlcommenter <https://google.github.io/sqlcommenter/>`_

.. code:: python

    from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor

    SQLAlchemyInstrumentor().instrument(enable_commenter=True)

SQLCommenter with commenter_options
***********************************
The key-value pairs appended to the query can be configured using
``commenter_options``. When sqlcommenter is enabled, all available KVs/tags
are calculated by default. ``commenter_options`` supports *opting out*
of specific KVs.

.. code:: python

    from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor

    # Opts into sqlcomment for SQLAlchemy trace integration.
    # Opts out of tags for db_driver, db_framework.
    SQLAlchemyInstrumentor().instrument(
        enable_commenter=True,
        commenter_options={
            "db_driver": False,
            "db_framework": False,
        }
    )

Available commenter_options
###########################

The following sqlcomment key-values can be opted out of through ``commenter_options``:

+---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+
| Commenter Option          | Description                                               | Example                                                                   |
+===========================+===========================================================+===========================================================================+
| ``db_driver``             | Database driver name.                                     | ``db_driver='psycopg2'``                                                  |
+---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+
| ``db_framework``          | Database framework name with version.                     | ``db_framework='sqlalchemy:1.4.0'``                                       |
+---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+
| ``opentelemetry_values``  | OpenTelemetry context as traceparent at time of query.    | ``traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'`` |
+---------------------------+-----------------------------------------------------------+---------------------------------------------------------------------------+

SQLComment in span attribute
****************************
If sqlcommenter is enabled, you can opt into the inclusion of sqlcomment in
the query span ``db.statement`` and/or ``db.query.text`` attribute for your
needs. If ``commenter_options`` have been set, the span attribute comment
will also be configured by this setting.

.. code:: python

    from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor

    # Opts into sqlcomment for SQLAlchemy trace integration.
    # Opts into sqlcomment for `db.statement` and/or `db.query.text` span attribute.
    SQLAlchemyInstrumentor().instrument(
        enable_commenter=True,
        commenter_options={},
        enable_attribute_commenter=True,
    )

Warning:
    Capture of sqlcomment in ``db.statement``/``db.query.text`` may have high cardinality without platform normalization. See `Semantic Conventions for database spans <https://opentelemetry.io/docs/specs/semconv/database/database-spans/#generating-a-summary-of-the-query-text>`_ for more information.

API
---
"""

from collections.abc import Sequence
from typing import Collection

import sqlalchemy
from packaging.version import parse as parse_version
from sqlalchemy.engine.base import Engine
from wrapt import wrap_function_wrapper as _w

from opentelemetry.instrumentation._semconv import (
    _get_schema_url_for_signal_types,
    _OpenTelemetrySemanticConventionStability,
    _OpenTelemetryStabilitySignalType,
)
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.sqlalchemy.engine import (
    EngineTracer,
    _wrap_connect,
    _wrap_create_async_engine,
    _wrap_create_engine,
)
from opentelemetry.instrumentation.sqlalchemy.package import _instruments
from opentelemetry.instrumentation.sqlalchemy.version import __version__
from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.metrics import get_meter
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.trace import get_tracer


[docs]class SQLAlchemyInstrumentor(BaseInstrumentor): """An instrumentor for SQLAlchemy See `BaseInstrumentor` """
[docs] def instrumentation_dependencies(self) -> Collection[str]: return _instruments
def _instrument(self, **kwargs): """Instruments SQLAlchemy engine creation methods and the engine if passed as an argument. Args: **kwargs: Optional arguments ``engine``: a SQLAlchemy engine instance ``engines``: a list of SQLAlchemy engine instances ``tracer_provider``: a TracerProvider, defaults to global ``meter_provider``: a MeterProvider, defaults to global ``enable_commenter``: bool to enable sqlcommenter, defaults to False ``commenter_options``: dict of sqlcommenter config, defaults to {} ``enable_attribute_commenter``: bool to enable sqlcomment addition to span attribute, defaults to False. Must also set `enable_commenter`. Returns: An instrumented engine if passed in as an argument or list of instrumented engines, None otherwise. """ # Initialize semantic conventions opt-in if needed _OpenTelemetrySemanticConventionStability._initialize() # Determine schema URL based on both DATABASE and HTTP signal types # and semconv opt-in mode schema_url = _get_schema_url_for_signal_types( [ _OpenTelemetryStabilitySignalType.DATABASE, _OpenTelemetryStabilitySignalType.HTTP, ] ) tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, __version__, tracer_provider, schema_url=schema_url, ) meter_provider = kwargs.get("meter_provider") meter = get_meter( __name__, __version__, meter_provider, schema_url=schema_url, ) connections_usage = meter.create_up_down_counter( name=MetricInstruments.DB_CLIENT_CONNECTIONS_USAGE, unit="connections", description="The number of connections that are currently in state described by the state attribute.", ) enable_commenter = kwargs.get("enable_commenter", False) commenter_options = kwargs.get("commenter_options", {}) enable_attribute_commenter = kwargs.get( "enable_attribute_commenter", False ) _w( "sqlalchemy", "create_engine", _wrap_create_engine( tracer, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ), ) _w( "sqlalchemy.engine", "create_engine", _wrap_create_engine( tracer, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ), ) # sqlalchemy.engine.create is not present in earlier versions of sqlalchemy (which we support) if parse_version(sqlalchemy.__version__).release >= (1, 4): _w( "sqlalchemy.engine.create", "create_engine", _wrap_create_engine( tracer, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ), ) _w( "sqlalchemy.engine.base", "Engine.connect", _wrap_connect(tracer), ) if parse_version(sqlalchemy.__version__).release >= (1, 4): _w( "sqlalchemy.ext.asyncio", "create_async_engine", _wrap_create_async_engine( tracer, connections_usage, enable_commenter, commenter_options, enable_attribute_commenter, ), ) if kwargs.get("engine") is not None: return EngineTracer( tracer, kwargs.get("engine"), connections_usage, kwargs.get("enable_commenter", False), kwargs.get("commenter_options", {}), kwargs.get("enable_attribute_commenter", False), ) if kwargs.get("engines") is not None and isinstance( kwargs.get("engines"), Sequence ): return [ EngineTracer( tracer, engine, connections_usage, kwargs.get("enable_commenter", False), kwargs.get("commenter_options", {}), kwargs.get("enable_attribute_commenter", False), ) for engine in kwargs.get("engines") ] return None def _uninstrument(self, **kwargs): unwrap(sqlalchemy, "create_engine") unwrap(sqlalchemy.engine, "create_engine") if parse_version(sqlalchemy.__version__).release >= (1, 4): unwrap(sqlalchemy.engine.create, "create_engine") unwrap(Engine, "connect") if parse_version(sqlalchemy.__version__).release >= (1, 4): unwrap(sqlalchemy.ext.asyncio, "create_async_engine") EngineTracer.remove_all_event_listeners()