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 <srikanth.chekuri92@gmail.com>

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Thiyagu55
2022-03-09 11:27:00 +05:30
committed by GitHub
parent 2f5bbc416a
commit dbb35a2946
6 changed files with 115 additions and 3 deletions

View File

@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- `opentelemetry-instrumentation-sqlalchemy` added experimental sql commenter capability
([#924](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/924))
- `opentelemetry-instrumentation-dbapi` add experimental sql commenter capability - `opentelemetry-instrumentation-dbapi` add experimental sql commenter capability
([#908](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/908)) ([#908](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/908))
- `opentelemetry-instrumentation-requests` make span attribute available to samplers - `opentelemetry-instrumentation-requests` make span attribute available to samplers

View File

@ -106,6 +106,7 @@ class SQLAlchemyInstrumentor(BaseInstrumentor):
return EngineTracer( return EngineTracer(
_get_tracer(kwargs.get("engine"), tracer_provider), _get_tracer(kwargs.get("engine"), tracer_provider),
kwargs.get("engine"), kwargs.get("engine"),
kwargs.get("enable_commenter", False),
) )
return None return None

View File

@ -17,7 +17,12 @@ from sqlalchemy.event import listen # pylint: disable=no-name-in-module
from opentelemetry import trace from opentelemetry import trace
from opentelemetry.instrumentation.sqlalchemy.version import __version__ from opentelemetry.instrumentation.sqlalchemy.version import __version__
from opentelemetry.instrumentation.utils import (
_generate_opentelemetry_traceparent,
_generate_sql_comment,
)
from opentelemetry.semconv.trace import NetTransportValues, SpanAttributes from opentelemetry.semconv.trace import NetTransportValues, SpanAttributes
from opentelemetry.trace import Span
from opentelemetry.trace.status import Status, StatusCode from opentelemetry.trace.status import Status, StatusCode
@ -70,12 +75,15 @@ def _wrap_create_engine(tracer_provider=None):
class EngineTracer: class EngineTracer:
def __init__(self, tracer, engine): def __init__(self, tracer, engine, enable_commenter=False):
self.tracer = tracer self.tracer = tracer
self.engine = engine self.engine = engine
self.vendor = _normalize_vendor(engine.name) self.vendor = _normalize_vendor(engine.name)
self.enable_commenter = enable_commenter
listen(engine, "before_cursor_execute", self._before_cur_exec) listen(
engine, "before_cursor_execute", self._before_cur_exec, retval=True
)
listen(engine, "after_cursor_execute", _after_cur_exec) listen(engine, "after_cursor_execute", _after_cur_exec)
listen(engine, "handle_error", _handle_error) listen(engine, "handle_error", _handle_error)
@ -115,6 +123,18 @@ class EngineTracer:
span.set_attribute(key, value) span.set_attribute(key, value)
context._otel_span = span context._otel_span = span
if self.enable_commenter:
statement = statement + EngineTracer._generate_comment(span=span)
return statement, params
@staticmethod
def _generate_comment(span: Span) -> str:
span_context = span.get_span_context()
meta = {}
if span_context.is_valid:
meta.update(_generate_opentelemetry_traceparent(span))
return _generate_sql_comment(**meta)
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import asyncio import asyncio
import logging
from unittest import mock from unittest import mock
import pytest import pytest
@ -20,12 +21,17 @@ from sqlalchemy import create_engine
from opentelemetry import trace from opentelemetry import trace
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.sqlalchemy.engine import EngineTracer
from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace import TracerProvider, export
from opentelemetry.test.test_base import TestBase from opentelemetry.test.test_base import TestBase
class TestSqlalchemyInstrumentation(TestBase): class TestSqlalchemyInstrumentation(TestBase):
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog):
self.caplog = caplog # pylint: disable=attribute-defined-outside-init
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
SQLAlchemyInstrumentor().uninstrument() SQLAlchemyInstrumentor().uninstrument()
@ -150,3 +156,22 @@ class TestSqlalchemyInstrumentation(TestBase):
self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT) self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
asyncio.get_event_loop().run_until_complete(run()) asyncio.get_event_loop().run_until_complete(run())
def test_generate_commenter(self):
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
engine = create_engine("sqlite:///:memory:")
SQLAlchemyInstrumentor().instrument(
engine=engine,
tracer_provider=self.tracer_provider,
enable_commenter=True,
)
cnx = engine.connect()
cnx.execute("SELECT 1 + 1;").fetchall()
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 1)
span = spans[0]
self.assertIn(
EngineTracer._generate_comment(span),
self.caplog.records[-2].getMessage(),
)

View File

@ -0,0 +1,53 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pytest
from sqlalchemy import create_engine
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.test.test_base import TestBase
class TestSqlalchemyInstrumentationWithSQLCommenter(TestBase):
@pytest.fixture(autouse=True)
def inject_fixtures(self, caplog):
self.caplog = caplog # pylint: disable=attribute-defined-outside-init
def tearDown(self):
super().tearDown()
SQLAlchemyInstrumentor().uninstrument()
def test_sqlcommenter_enabled(self):
engine = create_engine("sqlite:///:memory:")
SQLAlchemyInstrumentor().instrument(
engine=engine,
tracer_provider=self.tracer_provider,
enable_commenter=True,
)
cnx = engine.connect()
cnx.execute("SELECT 1;").fetchall()
self.assertRegex(
self.caplog.records[-2].getMessage(),
r"SELECT 1; /\*traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/",
)
def test_sqlcommenter_disabled(self):
engine = create_engine("sqlite:///:memory:", echo=True)
SQLAlchemyInstrumentor().instrument(
engine=engine, tracer_provider=self.tracer_provider
)
cnx = engine.connect()
cnx.execute("SELECT 1;").fetchall()
self.assertEqual(self.caplog.records[-2].getMessage(), "SELECT 1;")

View File

@ -23,7 +23,7 @@ from opentelemetry import context, trace
# pylint: disable=E0611 # pylint: disable=E0611
from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401 from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401
from opentelemetry.propagate import extract from opentelemetry.propagate import extract
from opentelemetry.trace import StatusCode from opentelemetry.trace import Span, StatusCode
def extract_attributes_from_object( def extract_attributes_from_object(
@ -152,3 +152,14 @@ def _url_quote(s): # pylint: disable=invalid-name
# thus in our quoting, we need to escape it too to finally give # thus in our quoting, we need to escape it too to finally give
# foo,bar --> foo%%2Cbar # foo,bar --> foo%%2Cbar
return quoted.replace("%", "%%") return quoted.replace("%", "%%")
def _generate_opentelemetry_traceparent(span: Span) -> str:
meta = {}
_version = "00"
_span_id = trace.format_span_id(span.context.span_id)
_trace_id = trace.format_trace_id(span.context.trace_id)
_flags = str(trace.TraceFlags.SAMPLED)
_traceparent = _version + "-" + _trace_id + "-" + _span_id + "-" + _flags
meta.update({"traceparent": _traceparent})
return meta