Skip to content

Commit b2dd4b8

Browse files
authored
Fix pyodbc cursor error in SQLA instrumentation (#469)
1 parent fe4e2d4 commit b2dd4b8

File tree

6 files changed

+145
-2
lines changed

6 files changed

+145
-2
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.3.0-0.22b0...HEAD)
88

9+
### Changed
910
- `opentelemetry-instrumentation-tornado` properly instrument work done in tornado on_finish method.
1011
([#499](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/499))
1112
- `opentelemetry-instrumentation` Fixed cases where trying to use an instrumentation package without the
1213
target library was crashing auto instrumentation agent.
1314
([#530](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/530))
15+
- Fix weak reference error for pyodbc cursor in SQLAlchemy instrumentation.
16+
([#469](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/469))
1417

1518
## [0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01
1619

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# limitations under the License.
1414

1515
from threading import local
16-
from weakref import WeakKeyDictionary
1716

1817
from sqlalchemy.event import listen # pylint: disable=no-name-in-module
1918

@@ -60,7 +59,7 @@ def __init__(self, tracer, engine):
6059
self.tracer = tracer
6160
self.engine = engine
6261
self.vendor = _normalize_vendor(engine.name)
63-
self.cursor_mapping = WeakKeyDictionary()
62+
self.cursor_mapping = {}
6463
self.local = local()
6564

6665
listen(engine, "before_cursor_execute", self._before_cur_exec)
@@ -116,6 +115,7 @@ def _after_cur_exec(self, conn, cursor, statement, *args):
116115
return
117116

118117
span.end()
118+
self._cleanup(cursor)
119119

120120
def _handle_error(self, context):
121121
span = self.current_thread_span
@@ -129,6 +129,13 @@ def _handle_error(self, context):
129129
)
130130
finally:
131131
span.end()
132+
self._cleanup(context.cursor)
133+
134+
def _cleanup(self, cursor):
135+
try:
136+
del self.cursor_mapping[cursor]
137+
except KeyError:
138+
pass
132139

133140

134141
def _get_attributes_from_url(url):

tests/opentelemetry-docker-tests/tests/check_availability.py

+17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import mysql.connector
1919
import psycopg2
2020
import pymongo
21+
import pyodbc
2122
import redis
2223

2324
MONGODB_COLLECTION_NAME = "test"
@@ -36,6 +37,11 @@
3637
POSTGRES_USER = os.getenv("POSTGRESQL_USER", "testuser")
3738
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
3839
REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379"))
40+
MSSQL_DB_NAME = os.getenv("MSSQL_DB_NAME", "opentelemetry-tests")
41+
MSSQL_HOST = os.getenv("MSSQL_HOST", "localhost")
42+
MSSQL_PORT = int(os.getenv("MSSQL_PORT", "1433"))
43+
MSSQL_USER = os.getenv("MSSQL_USER", "sa")
44+
MSSQL_PASSWORD = os.getenv("MSSQL_PASSWORD", "yourStrong(!)Password")
3945
RETRY_COUNT = 8
4046
RETRY_INTERVAL = 5 # Seconds
4147

@@ -104,12 +110,23 @@ def check_redis_connection():
104110
connection.hgetall("*")
105111

106112

113+
@retryable
114+
def check_mssql_connection():
115+
connection = pyodbc.connect(
116+
f"DRIVER={{ODBC Driver 17 for SQL Server}};SERVER={MSSQL_HOST},"
117+
f"{MSSQL_PORT};DATABASE={MSSQL_DB_NAME};UID={MSSQL_USER};"
118+
f"PWD={MSSQL_PASSWORD}"
119+
)
120+
connection.close()
121+
122+
107123
def check_docker_services_availability():
108124
# Check if Docker services accept connections
109125
check_pymongo_connection()
110126
check_mysql_connection()
111127
check_postgres_connection()
112128
check_redis_connection()
129+
check_mssql_connection()
113130

114131

115132
check_docker_services_availability()

tests/opentelemetry-docker-tests/tests/docker-compose.yml

+8
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,11 @@ services:
3939
- "16686:16686"
4040
- "14268:14268"
4141
- "9411:9411"
42+
otmssql:
43+
image: mcr.microsoft.com/mssql/server:2017-latest
44+
ports:
45+
- "1433:1433"
46+
environment:
47+
ACCEPT_EULA: "Y"
48+
SA_PASSWORD: "yourStrong(!)Password"
49+
command: /bin/sh -c "sleep 10s && /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P yourStrong\(!\)Password -d master -Q 'CREATE DATABASE [opentelemetry-tests]' & /opt/mssql/bin/sqlservr"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 os
16+
import unittest
17+
18+
import pytest
19+
from sqlalchemy.exc import ProgrammingError
20+
21+
from opentelemetry import trace
22+
from opentelemetry.semconv.trace import SpanAttributes
23+
24+
from .mixins import Player, SQLAlchemyTestMixin
25+
26+
MSSQL_CONFIG = {
27+
"host": "127.0.0.1",
28+
"port": int(os.getenv("TEST_MSSQL_PORT", "1433")),
29+
"user": os.getenv("TEST_MSSQL_USER", "sa"),
30+
"password": os.getenv("TEST_MSSQL_PASSWORD", "yourStrong(!)Password"),
31+
"database": os.getenv("TEST_MSSQL_DATABASE", "opentelemetry-tests"),
32+
"driver": os.getenv("TEST_MSSQL_DRIVER", "ODBC+Driver+17+for+SQL+Server"),
33+
}
34+
35+
36+
class MssqlConnectorTestCase(SQLAlchemyTestMixin):
37+
"""TestCase for pyodbc engine"""
38+
39+
__test__ = True
40+
41+
VENDOR = "mssql"
42+
SQL_DB = "opentelemetry-tests"
43+
ENGINE_ARGS = {
44+
"url": "mssql+pyodbc://%(user)s:%(password)s@%(host)s:%(port)s/%(database)s?driver=%(driver)s"
45+
% MSSQL_CONFIG
46+
}
47+
48+
def check_meta(self, span):
49+
# check database connection tags
50+
self.assertEqual(
51+
span.attributes.get(SpanAttributes.NET_PEER_NAME),
52+
MSSQL_CONFIG["host"],
53+
)
54+
self.assertEqual(
55+
span.attributes.get(SpanAttributes.NET_PEER_PORT),
56+
MSSQL_CONFIG["port"],
57+
)
58+
self.assertEqual(
59+
span.attributes.get(SpanAttributes.DB_NAME),
60+
MSSQL_CONFIG["database"],
61+
)
62+
self.assertEqual(
63+
span.attributes.get(SpanAttributes.DB_USER), MSSQL_CONFIG["user"]
64+
)
65+
66+
def test_engine_execute_errors(self):
67+
# ensures that SQL errors are reported
68+
with pytest.raises(ProgrammingError):
69+
with self.connection() as conn:
70+
conn.execute("SELECT * FROM a_wrong_table").fetchall()
71+
72+
spans = self.memory_exporter.get_finished_spans()
73+
self.assertEqual(len(spans), 1)
74+
span = spans[0]
75+
# span fields
76+
self.assertEqual(span.name, "SELECT opentelemetry-tests")
77+
self.assertEqual(
78+
span.attributes.get(SpanAttributes.DB_STATEMENT),
79+
"SELECT * FROM a_wrong_table",
80+
)
81+
self.assertEqual(
82+
span.attributes.get(SpanAttributes.DB_NAME), self.SQL_DB
83+
)
84+
self.check_meta(span)
85+
self.assertTrue(span.end_time - span.start_time > 0)
86+
# check the error
87+
self.assertIs(
88+
span.status.status_code, trace.StatusCode.ERROR,
89+
)
90+
self.assertIn("a_wrong_table", span.status.description)
91+
92+
def test_orm_insert(self):
93+
# ensures that the ORM session is traced
94+
wayne = Player(id=1, name="wayne")
95+
self.session.add(wayne)
96+
self.session.commit()
97+
98+
spans = self.memory_exporter.get_finished_spans()
99+
# identity insert on before the insert, insert, and identity insert off after the insert
100+
self.assertEqual(len(spans), 3)
101+
span = spans[1]
102+
self._check_span(span, "INSERT")
103+
self.assertIn(
104+
"INSERT INTO players",
105+
span.attributes.get(SpanAttributes.DB_STATEMENT),
106+
)
107+
self.check_meta(span)

tox.ini

+1
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ deps =
404404
celery[pytest] >= 4.0, < 6.0
405405
protobuf>=3.13.0
406406
requests==2.25.0
407+
pyodbc~=4.0.30
407408
changedir =
408409
tests/opentelemetry-docker-tests/tests
409410

0 commit comments

Comments
 (0)