Skip to content

Commit 96980a5

Browse files
committed
Add pymssql instrumentation
1 parent 03d97ff commit 96980a5

File tree

12 files changed

+541
-3
lines changed

12 files changed

+541
-3
lines changed

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- `opentelemetry-instrumentation-pymssql` Add pymssql instrumentation
13+
([#394](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/394))
14+
1015
### Fixed
1116

1217
- `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies.
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OpenTelemetry pymssql Instrumentation
2+
=====================================
3+
4+
.. automodule:: opentelemetry.instrumentation.pymssql
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

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

+9-3
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,15 @@ def cursor(self, *args, **kwargs):
336336
self._connection.cursor(*args, **kwargs), db_api_integration
337337
)
338338

339-
# For some reason this is necessary as trying to access the close
340-
# method of self._connection via __getattr__ leads to unexplained
341-
# errors.
339+
# For some reason this is necessary as trying to access some methods
340+
# such as commit and close method of self._connection via __getattr__
341+
# leads to unexplained errors.
342+
def commit(self):
343+
self._connection.commit()
344+
345+
def rollback(self):
346+
self._connection.rollback()
347+
342348
def close(self):
343349
self._connection.close()
344350

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
OpenTelemetry pymssql Instrumentation
2+
=====================================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-pymssql.svg
7+
:target: https://pypi.org/project/opentelemetry-instrumentation-pymssql/
8+
9+
Installation
10+
------------
11+
12+
::
13+
14+
pip install opentelemetry-instrumentation-pymssql
15+
16+
17+
References
18+
----------
19+
* `OpenTelemetry pymssql Instrumentation <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/pymssql/pymssql.html>`_
20+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
21+
* `OpenTelemetry Python Examples <https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples>`_
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
[metadata]
16+
name = opentelemetry-instrumentation-pymssql
17+
description = OpenTelemetry pymssql instrumentation
18+
long_description = file: README.rst
19+
long_description_content_type = text/x-rst
20+
author = OpenTelemetry Authors
21+
author_email = [email protected]
22+
url = https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-pymssql
23+
platforms = any
24+
license = Apache-2.0
25+
classifiers =
26+
Development Status :: 4 - Beta
27+
Intended Audience :: Developers
28+
License :: OSI Approved :: Apache Software License
29+
Programming Language :: Python
30+
Programming Language :: Python :: 3
31+
Programming Language :: Python :: 3.7
32+
Programming Language :: Python :: 3.8
33+
Programming Language :: Python :: 3.9
34+
Programming Language :: Python :: 3.10
35+
36+
[options]
37+
python_requires = >=3.7
38+
package_dir=
39+
=src
40+
packages=find_namespace:
41+
install_requires =
42+
opentelemetry-api ~= 1.12
43+
opentelemetry-instrumentation-dbapi == 0.33b0
44+
opentelemetry-instrumentation == 0.33b0
45+
46+
[options.extras_require]
47+
test =
48+
opentelemetry-test-utils == 0.33b0
49+
50+
[options.packages.find]
51+
where = src
52+
53+
[options.entry_points]
54+
opentelemetry_instrumentor =
55+
pymssql = opentelemetry.instrumentation.pymssql:PyMSSQLInstrumentor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
16+
# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM templates/instrumentation_setup.py.txt.
17+
# RUN `python scripts/generate_setup.py` TO REGENERATE.
18+
19+
20+
import distutils.cmd
21+
import json
22+
import os
23+
from configparser import ConfigParser
24+
25+
import setuptools
26+
27+
config = ConfigParser()
28+
config.read("setup.cfg")
29+
30+
# We provide extras_require parameter to setuptools.setup later which
31+
# overwrites the extras_require section from setup.cfg. To support extras_require
32+
# section in setup.cfg, we load it here and merge it with the extras_require param.
33+
extras_require = {}
34+
if "options.extras_require" in config:
35+
for key, value in config["options.extras_require"].items():
36+
extras_require[key] = [v for v in value.split("\n") if v.strip()]
37+
38+
BASE_DIR = os.path.dirname(__file__)
39+
PACKAGE_INFO = {}
40+
41+
VERSION_FILENAME = os.path.join(
42+
BASE_DIR,
43+
"src",
44+
"opentelemetry",
45+
"instrumentation",
46+
"pymssql",
47+
"version.py",
48+
)
49+
with open(VERSION_FILENAME, encoding="utf-8") as f:
50+
exec(f.read(), PACKAGE_INFO)
51+
52+
PACKAGE_FILENAME = os.path.join(
53+
BASE_DIR,
54+
"src",
55+
"opentelemetry",
56+
"instrumentation",
57+
"pymssql",
58+
"package.py",
59+
)
60+
with open(PACKAGE_FILENAME, encoding="utf-8") as f:
61+
exec(f.read(), PACKAGE_INFO)
62+
63+
# Mark any instruments/runtime dependencies as test dependencies as well.
64+
extras_require["instruments"] = PACKAGE_INFO["_instruments"]
65+
test_deps = extras_require.get("test", [])
66+
for dep in extras_require["instruments"]:
67+
test_deps.append(dep)
68+
69+
extras_require["test"] = test_deps
70+
71+
72+
class JSONMetadataCommand(distutils.cmd.Command):
73+
74+
description = (
75+
"print out package metadata as JSON. This is used by OpenTelemetry dev scripts to ",
76+
"auto-generate code in other places",
77+
)
78+
user_options = []
79+
80+
def initialize_options(self):
81+
pass
82+
83+
def finalize_options(self):
84+
pass
85+
86+
def run(self):
87+
metadata = {
88+
"name": config["metadata"]["name"],
89+
"version": PACKAGE_INFO["__version__"],
90+
"instruments": PACKAGE_INFO["_instruments"],
91+
}
92+
print(json.dumps(metadata))
93+
94+
95+
setuptools.setup(
96+
cmdclass={"meta": JSONMetadataCommand},
97+
version=PACKAGE_INFO["__version__"],
98+
extras_require=extras_require,
99+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
"""
16+
The integration with pymssql supports the `pymssql`_ library and can be enabled
17+
by using ``PyMSSQLInstrumentor``.
18+
19+
.. _pymssql: https://pypi.org/project/pymssql/
20+
21+
Usage
22+
-----
23+
24+
.. code:: python
25+
26+
import pymssql
27+
from opentelemetry.instrumentation.pymssql import PyMSSQLInstrumentor
28+
29+
PyMSSQLInstrumentor().instrument()
30+
31+
cnx = pymssql.connect(database="MSSQL_Database")
32+
cursor = cnx.cursor()
33+
cursor.execute("INSERT INTO test (testField) VALUES (123)"
34+
cnx.commit()
35+
cursor.close()
36+
cnx.close()
37+
38+
API
39+
---
40+
"""
41+
from typing import Any, Callable, Collection, Dict, NamedTuple, Tuple
42+
43+
import pymssql
44+
45+
from opentelemetry.instrumentation import dbapi
46+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
47+
from opentelemetry.instrumentation.pymssql.package import _instruments
48+
from opentelemetry.instrumentation.pymssql.version import __version__
49+
50+
_DATABASE_SYSTEM = "mssql"
51+
52+
53+
class PyMSSQLConnectMethodArgsTuple(NamedTuple):
54+
server: str = None
55+
user: str = None
56+
password: str = None
57+
database: str = None
58+
timeout: int = None
59+
login_timeout: int = None
60+
charset: str = None
61+
as_dict: bool = None
62+
host: str = None
63+
appname: str = None
64+
port: str = None
65+
conn_properties: str = None
66+
autocommit: bool = None
67+
tds_version: str = None
68+
69+
70+
class PyMSSQLDatabaseApiIntegration(dbapi.DatabaseApiIntegration):
71+
def wrapped_connection(
72+
self,
73+
connect_method: Callable[..., Any],
74+
args: Tuple[Any, Any],
75+
kwargs: Dict[Any, Any],
76+
):
77+
"""Add object proxy to connection object."""
78+
connection = connect_method(*args, **kwargs)
79+
connect_method_args = PyMSSQLConnectMethodArgsTuple(*args)
80+
81+
self.name = self.database_system
82+
self.database = kwargs.get("database") or connect_method_args.database
83+
84+
user = kwargs.get("user") or connect_method_args.user
85+
if user is not None:
86+
self.span_attributes["db.user"] = user
87+
88+
port = kwargs.get("port") or connect_method_args.port
89+
host = kwargs.get("server") or connect_method_args.server
90+
if host is None:
91+
host = kwargs.get("host") or connect_method_args.host
92+
if host is not None:
93+
# The host string can include the port, separated by either a coma or
94+
# a column
95+
for sep in (":", ","):
96+
if sep in host:
97+
tokens = host.rsplit(sep)
98+
host = tokens[0]
99+
if len(tokens) > 1:
100+
port = tokens[1]
101+
if host is not None:
102+
self.span_attributes["net.peer.name"] = host
103+
if port is not None:
104+
self.span_attributes["net.peer.port"] = port
105+
106+
charset = kwargs.get("charset") or connect_method_args.charset
107+
if charset is not None:
108+
self.span_attributes["db.charset"] = charset
109+
110+
tds_version = (
111+
kwargs.get("tds_version") or connect_method_args.tds_version
112+
)
113+
if tds_version is not None:
114+
self.span_attributes["db.protocol.tds.version"] = tds_version
115+
116+
return dbapi.get_traced_connection_proxy(connection, self)
117+
118+
119+
120+
class PyMSSQLInstrumentor(BaseInstrumentor):
121+
122+
def instrumentation_dependencies(self) -> Collection[str]:
123+
return _instruments
124+
125+
def _instrument(self, **kwargs):
126+
"""Integrate with the pymssql library.
127+
https://github.com/pymssql/pymssql/
128+
"""
129+
tracer_provider = kwargs.get("tracer_provider")
130+
131+
dbapi.wrap_connect(
132+
__name__,
133+
pymssql,
134+
"connect",
135+
_DATABASE_SYSTEM,
136+
version=__version__,
137+
tracer_provider=tracer_provider,
138+
# pymssql does not keep the connection attributes in its connection object;
139+
# instead, we get the attributes from the connect method (which is done
140+
# via PyMSSQLDatabaseApiIntegration.wrapped_connection)
141+
db_api_integration_factory=PyMSSQLDatabaseApiIntegration,
142+
)
143+
144+
def _uninstrument(self, **kwargs):
145+
""" "Disable pymssql instrumentation"""
146+
dbapi.unwrap_connect(pymssql, "connect")
147+
148+
@staticmethod
149+
def instrument_connection(connection, tracer_provider=None):
150+
"""Enable instrumentation in a pymssql connection.
151+
152+
Args:
153+
connection: The connection to instrument.
154+
tracer_provider: The optional tracer provider to use. If omitted
155+
the current globally configured one is used.
156+
157+
Returns:
158+
An instrumented connection.
159+
"""
160+
161+
return dbapi.instrument_connection(
162+
__name__,
163+
connection,
164+
_DATABASE_SYSTEM,
165+
version=__version__,
166+
tracer_provider=tracer_provider,
167+
)
168+
169+
@staticmethod
170+
def uninstrument_connection(connection):
171+
"""Disable instrumentation in a pymssql connection.
172+
173+
Args:
174+
connection: The connection to uninstrument.
175+
176+
Returns:
177+
An uninstrumented connection.
178+
"""
179+
return dbapi.uninstrument_connection(connection)
180+

0 commit comments

Comments
 (0)