Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#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))
- `opentelemetry-instrumentation-sqlalchemy`: implement new semantic convention opt-in migration
([#4110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4110))

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
| [opentelemetry-instrumentation-redis](./opentelemetry-instrumentation-redis) | redis >= 2.6 | No | development
| [opentelemetry-instrumentation-remoulade](./opentelemetry-instrumentation-remoulade) | remoulade >= 0.50 | No | development
| [opentelemetry-instrumentation-requests](./opentelemetry-instrumentation-requests) | requests ~= 2.0 | Yes | migration
| [opentelemetry-instrumentation-sqlalchemy](./opentelemetry-instrumentation-sqlalchemy) | sqlalchemy >= 1.0.0, < 2.1.0 | Yes | development
| [opentelemetry-instrumentation-sqlalchemy](./opentelemetry-instrumentation-sqlalchemy) | sqlalchemy >= 1.0.0, < 2.1.0 | Yes | migration
| [opentelemetry-instrumentation-sqlite3](./opentelemetry-instrumentation-sqlite3) | sqlite3 | No | development
| [opentelemetry-instrumentation-starlette](./opentelemetry-instrumentation-starlette) | starlette >= 0.13 | Yes | development
| [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No | development
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,24 +110,24 @@
SQLComment in span attribute
****************************
If sqlcommenter is enabled, you can opt into the inclusion of sqlcomment in
the query span ``db.statement`` attribute for your needs. If ``commenter_options``
have been set, the span attribute comment will also be configured by this
setting.
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` span attribute.
# 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`` 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.
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
---
Expand All @@ -141,6 +141,11 @@
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,
Expand Down Expand Up @@ -181,20 +186,32 @@ def _instrument(self, **kwargs):
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="https://opentelemetry.io/schemas/1.11.0",
schema_url=schema_url,
)

meter_provider = kwargs.get("meter_provider")
meter = get_meter(
__name__,
__version__,
meter_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0",
schema_url=schema_url,
)

connections_usage = meter.create_up_down_counter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,36 @@
)

from opentelemetry import trace
from opentelemetry.instrumentation._semconv import (
_OpenTelemetrySemanticConventionStability,
_OpenTelemetryStabilitySignalType,
_set_db_name,
_set_db_statement,
_set_db_system,
_set_db_user,
_set_http_net_peer_name_client,
_set_http_peer_port_client,
)
from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment
from opentelemetry.instrumentation.utils import (
_get_opentelemetry_values,
is_instrumentation_enabled,
)
from opentelemetry.semconv._incubating.attributes.db_attributes import (
DB_NAME,
DB_STATEMENT,
DB_SYSTEM,
DB_USER,
)
from opentelemetry.semconv._incubating.attributes.net_attributes import (
NET_PEER_NAME,
NET_PEER_PORT,
NET_TRANSPORT,
NetTransportValues,
)
from opentelemetry.trace.status import Status, StatusCode


def _get_db_name_from_cursor(vendor, cursor):
if vendor == "postgresql":
info = getattr(getattr(cursor, "connection", None), "info", None)
if info and info.dbname:
return info.dbname
return None


def _normalize_vendor(vendor):
"""Return a canonical name for a type of database."""
if not vendor:
Expand Down Expand Up @@ -119,13 +129,30 @@ def _wrap_connect_internal(func, module, args, kwargs):
if not is_instrumentation_enabled():
return func(*args, **kwargs)

# Initialize semantic conventions opt-in if needed
_OpenTelemetrySemanticConventionStability._initialize()
sem_conv_opt_in_mode_db = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.DATABASE,
)
sem_conv_opt_in_mode_http = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.HTTP,
)

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

return _wrap_connect_internal
Expand All @@ -143,6 +170,15 @@ def __init__(
commenter_options=None,
enable_attribute_commenter=False,
):
# Initialize semantic conventions opt-in if needed
_OpenTelemetrySemanticConventionStability._initialize()
self._sem_conv_opt_in_mode_db = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.DATABASE,
)
self._sem_conv_opt_in_mode_http = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.HTTP,
)

self.tracer = tracer
self.connections_usage = connections_usage
self.vendor = _normalize_vendor(engine.name)
Expand Down Expand Up @@ -278,9 +314,10 @@ def _get_commenter_data(self, conn) -> dict:

def _set_db_client_span_attributes(self, span, statement, attrs) -> None:
"""Uses statement and attrs to set attributes of provided Otel span"""
span.set_attribute(DB_STATEMENT, statement)
span.set_attribute(DB_SYSTEM, self.vendor)
for key, value in attrs.items():
span_attrs = dict(attrs)
_set_db_statement(span_attrs, statement, self._sem_conv_opt_in_mode_db)
_set_db_system(span_attrs, self.vendor, self._sem_conv_opt_in_mode_db)
for key, value in span_attrs.items():
span.set_attribute(key, value)

def _before_cur_exec(
Expand All @@ -289,11 +326,26 @@ def _before_cur_exec(
if not is_instrumentation_enabled():
return statement, params

attrs, found = _get_attributes_from_url(conn.engine.url)
attrs, found = _get_attributes_from_url(
conn.engine.url,
self._sem_conv_opt_in_mode_db,
self._sem_conv_opt_in_mode_http,
)
if not found:
attrs = _get_attributes_from_cursor(self.vendor, cursor, attrs)
attrs = _get_attributes_from_cursor(
self.vendor,
cursor,
attrs,
self._sem_conv_opt_in_mode_db,
self._sem_conv_opt_in_mode_http,
)

# Extract db_name for operation name
if self.vendor == "sqlite":
db_name = conn.engine.url.database
else:
db_name = _get_db_name_from_cursor(self.vendor, cursor)

db_name = attrs.get(DB_NAME, "")
span = self.tracer.start_span(
self._operation_name(db_name, statement),
kind=trace.SpanKind.CLIENT,
Expand All @@ -307,7 +359,7 @@ def _before_cur_exec(
# just to handle type safety
statement = str(statement)

# sqlcomment is added to executed query and db.statement span attribute
# sqlcomment is added to executed query and db.statement and/or db.query.text span attribute
statement = _add_sql_comment(
statement, **commenter_data
)
Expand All @@ -317,7 +369,7 @@ def _before_cur_exec(

else:
# sqlcomment is only added to executed query
# so db.statement is set before add_sql_comment
# so db.statement and/or db.query.text is set before add_sql_comment
self._set_db_client_span_attributes(
span, statement, attrs
)
Expand Down Expand Up @@ -358,42 +410,59 @@ def _handle_error(context):
span.end()


def _get_attributes_from_url(url):
def _get_attributes_from_url(
url, sem_conv_opt_in_mode_db, sem_conv_opt_in_mode_http
):
"""Set connection tags from the url. return true if successful."""
attrs = {}
if url.host:
attrs[NET_PEER_NAME] = url.host
_set_http_net_peer_name_client(
attrs, url.host, sem_conv_opt_in_mode_http
)
if url.port:
attrs[NET_PEER_PORT] = url.port
_set_http_peer_port_client(attrs, url.port, sem_conv_opt_in_mode_http)
if url.database:
attrs[DB_NAME] = url.database
_set_db_name(attrs, url.database, sem_conv_opt_in_mode_db)
if url.username:
attrs[DB_USER] = url.username
_set_db_user(attrs, url.username, sem_conv_opt_in_mode_db)
return attrs, bool(url.host)


def _get_attributes_from_cursor(vendor, cursor, attrs):
def _get_attributes_from_cursor(
vendor, cursor, attrs, sem_conv_opt_in_mode_db, sem_conv_opt_in_mode_http
):
"""Attempt to set db connection attributes by introspecting the cursor."""
if vendor == "postgresql":
info = getattr(getattr(cursor, "connection", None), "info", None)
if not info:
return attrs

attrs[DB_NAME] = info.dbname
db_name = _get_db_name_from_cursor(vendor, cursor)
_set_db_name(attrs, db_name, sem_conv_opt_in_mode_db)
is_unix_socket = info.host and info.host.startswith("/")

if is_unix_socket:
attrs[NET_TRANSPORT] = NetTransportValues.OTHER.value
if info.port:
# postgresql enforces this pattern on all socket names
attrs[NET_PEER_NAME] = os.path.join(
info.host, f".s.PGSQL.{info.port}"
_set_http_net_peer_name_client(
attrs,
os.path.join(info.host, f".s.PGSQL.{info.port}"),
sem_conv_opt_in_mode_http,
)
else:
attrs[NET_TRANSPORT] = NetTransportValues.IP_TCP.value
attrs[NET_PEER_NAME] = info.host
_set_http_net_peer_name_client(
attrs, info.host, sem_conv_opt_in_mode_http
)
if info.port:
attrs[NET_PEER_PORT] = int(info.port)
_set_http_peer_port_client(
attrs, int(info.port), sem_conv_opt_in_mode_http
)
elif vendor == "sqlite":
db_name = _get_db_name_from_cursor(vendor, cursor)
_set_db_name(attrs, db_name, sem_conv_opt_in_mode_db)
# SQLite has no network attributes
return attrs


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
_instruments = ("sqlalchemy >= 1.0.0, < 2.1.0",)

_supports_metrics = True

_semconv_status = "migration"
Loading
Loading