Skip to content

Commit dbb35a2

Browse files
Sqlcommenter integration into SQLAlchemy (#924)
* Integrating sqlcommenter into psycopg2 * Integrating sqlcommenter into psycopg2 - Converted public local variable into private * Added test cases for sqlcommenter & PR Changes * Code refactoring for generate sqlcommenter * Added testcase for sqlcommenter integration into sqlalchemy * updated change log * updated to accept latest logs * Updated lint changes * Fixed errors due to linting * Fixed linting errors * Fixed linting errors * Fixed linting errors * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: Srikanth Chekuri <[email protected]> Co-authored-by: Srikanth Chekuri <[email protected]>
1 parent 2f5bbc4 commit dbb35a2

File tree

6 files changed

+115
-3
lines changed

6 files changed

+115
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
### Added
1414

15+
- `opentelemetry-instrumentation-sqlalchemy` added experimental sql commenter capability
16+
([#924](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/924))
1517
- `opentelemetry-instrumentation-dbapi` add experimental sql commenter capability
1618
([#908](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/908))
1719
- `opentelemetry-instrumentation-requests` make span attribute available to samplers

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def _instrument(self, **kwargs):
106106
return EngineTracer(
107107
_get_tracer(kwargs.get("engine"), tracer_provider),
108108
kwargs.get("engine"),
109+
kwargs.get("enable_commenter", False),
109110
)
110111
return None
111112

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717

1818
from opentelemetry import trace
1919
from opentelemetry.instrumentation.sqlalchemy.version import __version__
20+
from opentelemetry.instrumentation.utils import (
21+
_generate_opentelemetry_traceparent,
22+
_generate_sql_comment,
23+
)
2024
from opentelemetry.semconv.trace import NetTransportValues, SpanAttributes
25+
from opentelemetry.trace import Span
2126
from opentelemetry.trace.status import Status, StatusCode
2227

2328

@@ -70,12 +75,15 @@ def _wrap_create_engine_internal(func, module, args, kwargs):
7075

7176

7277
class EngineTracer:
73-
def __init__(self, tracer, engine):
78+
def __init__(self, tracer, engine, enable_commenter=False):
7479
self.tracer = tracer
7580
self.engine = engine
7681
self.vendor = _normalize_vendor(engine.name)
82+
self.enable_commenter = enable_commenter
7783

78-
listen(engine, "before_cursor_execute", self._before_cur_exec)
84+
listen(
85+
engine, "before_cursor_execute", self._before_cur_exec, retval=True
86+
)
7987
listen(engine, "after_cursor_execute", _after_cur_exec)
8088
listen(engine, "handle_error", _handle_error)
8189

@@ -115,6 +123,18 @@ def _before_cur_exec(
115123
span.set_attribute(key, value)
116124

117125
context._otel_span = span
126+
if self.enable_commenter:
127+
statement = statement + EngineTracer._generate_comment(span=span)
128+
129+
return statement, params
130+
131+
@staticmethod
132+
def _generate_comment(span: Span) -> str:
133+
span_context = span.get_span_context()
134+
meta = {}
135+
if span_context.is_valid:
136+
meta.update(_generate_opentelemetry_traceparent(span))
137+
return _generate_sql_comment(**meta)
118138

119139

120140
# pylint: disable=unused-argument

instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import asyncio
15+
import logging
1516
from unittest import mock
1617

1718
import pytest
@@ -20,12 +21,17 @@
2021

2122
from opentelemetry import trace
2223
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
24+
from opentelemetry.instrumentation.sqlalchemy.engine import EngineTracer
2325
from opentelemetry.sdk.resources import Resource
2426
from opentelemetry.sdk.trace import TracerProvider, export
2527
from opentelemetry.test.test_base import TestBase
2628

2729

2830
class TestSqlalchemyInstrumentation(TestBase):
31+
@pytest.fixture(autouse=True)
32+
def inject_fixtures(self, caplog):
33+
self.caplog = caplog # pylint: disable=attribute-defined-outside-init
34+
2935
def tearDown(self):
3036
super().tearDown()
3137
SQLAlchemyInstrumentor().uninstrument()
@@ -150,3 +156,22 @@ async def run():
150156
self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
151157

152158
asyncio.get_event_loop().run_until_complete(run())
159+
160+
def test_generate_commenter(self):
161+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
162+
engine = create_engine("sqlite:///:memory:")
163+
SQLAlchemyInstrumentor().instrument(
164+
engine=engine,
165+
tracer_provider=self.tracer_provider,
166+
enable_commenter=True,
167+
)
168+
169+
cnx = engine.connect()
170+
cnx.execute("SELECT 1 + 1;").fetchall()
171+
spans = self.memory_exporter.get_finished_spans()
172+
self.assertEqual(len(spans), 1)
173+
span = spans[0]
174+
self.assertIn(
175+
EngineTracer._generate_comment(span),
176+
self.caplog.records[-2].getMessage(),
177+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
from sqlalchemy import create_engine
17+
18+
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
19+
from opentelemetry.test.test_base import TestBase
20+
21+
22+
class TestSqlalchemyInstrumentationWithSQLCommenter(TestBase):
23+
@pytest.fixture(autouse=True)
24+
def inject_fixtures(self, caplog):
25+
self.caplog = caplog # pylint: disable=attribute-defined-outside-init
26+
27+
def tearDown(self):
28+
super().tearDown()
29+
SQLAlchemyInstrumentor().uninstrument()
30+
31+
def test_sqlcommenter_enabled(self):
32+
engine = create_engine("sqlite:///:memory:")
33+
SQLAlchemyInstrumentor().instrument(
34+
engine=engine,
35+
tracer_provider=self.tracer_provider,
36+
enable_commenter=True,
37+
)
38+
cnx = engine.connect()
39+
cnx.execute("SELECT 1;").fetchall()
40+
self.assertRegex(
41+
self.caplog.records[-2].getMessage(),
42+
r"SELECT 1; /\*traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/",
43+
)
44+
45+
def test_sqlcommenter_disabled(self):
46+
engine = create_engine("sqlite:///:memory:", echo=True)
47+
SQLAlchemyInstrumentor().instrument(
48+
engine=engine, tracer_provider=self.tracer_provider
49+
)
50+
cnx = engine.connect()
51+
cnx.execute("SELECT 1;").fetchall()
52+
53+
self.assertEqual(self.caplog.records[-2].getMessage(), "SELECT 1;")

opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
# pylint: disable=E0611
2424
from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401
2525
from opentelemetry.propagate import extract
26-
from opentelemetry.trace import StatusCode
26+
from opentelemetry.trace import Span, StatusCode
2727

2828

2929
def extract_attributes_from_object(
@@ -152,3 +152,14 @@ def _url_quote(s): # pylint: disable=invalid-name
152152
# thus in our quoting, we need to escape it too to finally give
153153
# foo,bar --> foo%%2Cbar
154154
return quoted.replace("%", "%%")
155+
156+
157+
def _generate_opentelemetry_traceparent(span: Span) -> str:
158+
meta = {}
159+
_version = "00"
160+
_span_id = trace.format_span_id(span.context.span_id)
161+
_trace_id = trace.format_trace_id(span.context.trace_id)
162+
_flags = str(trace.TraceFlags.SAMPLED)
163+
_traceparent = _version + "-" + _trace_id + "-" + _span_id + "-" + _flags
164+
meta.update({"traceparent": _traceparent})
165+
return meta

0 commit comments

Comments
 (0)