Skip to content

Commit d4e359a

Browse files
authored
oracledb Package Instrumentation (#1423)
* Oracledb basic test setup * Basic instrumentation scaffold * First oracledb test file * Fix __enter__ methods * Instance info for oracledb * Async instrumentation * Async oracledb testing * Minor tweaks to instance info * Remove unsupported pypy environment * Update oracledb to test older major versions
1 parent 69692b3 commit d4e359a

File tree

9 files changed

+607
-0
lines changed

9 files changed

+607
-0
lines changed

.github/workflows/tests.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jobs:
4444
- firestore
4545
- grpc
4646
- kafka
47+
- oracledb
4748
- memcached
4849
- mongodb3
4950
- mongodb8
@@ -800,6 +801,73 @@ jobs:
800801
if-no-files-found: error
801802
retention-days: 1
802803

804+
oracledb:
805+
env:
806+
TOTAL_GROUPS: 1
807+
808+
strategy:
809+
fail-fast: false
810+
matrix:
811+
group-number: [1]
812+
813+
runs-on: ubuntu-24.04
814+
container:
815+
image: ghcr.io/newrelic/newrelic-python-agent-ci:latest
816+
options: >-
817+
--add-host=host.docker.internal:host-gateway
818+
timeout-minutes: 30
819+
services:
820+
oracledb:
821+
image: container-registry.oracle.com/database/free:latest-lite
822+
ports:
823+
- 8080:1521
824+
- 8081:1521
825+
env:
826+
ORACLE_CHARACTERSET: utf8
827+
ORACLE_PWD: oracle
828+
# Set health checks to wait until container has started
829+
options: >-
830+
--health-cmd "timeout 5 bash -c 'cat < /dev/null > /dev/udp/127.0.0.1/11211'"
831+
--health-interval 10s
832+
--health-timeout 5s
833+
--health-retries 5
834+
835+
steps:
836+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
837+
838+
- name: Fetch git tags
839+
run: |
840+
git config --global --add safe.directory "$GITHUB_WORKSPACE"
841+
git fetch --tags origin
842+
843+
- name: Configure pip cache
844+
run: |
845+
mkdir -p /github/home/.cache/pip
846+
chown -R "$(whoami)" /github/home/.cache/pip
847+
848+
- name: Get Environments
849+
id: get-envs
850+
run: |
851+
echo "envs=$(tox -l | grep '^${{ github.job }}\-' | ./.github/workflows/get-envs.py)" >> "$GITHUB_OUTPUT"
852+
env:
853+
GROUP_NUMBER: ${{ matrix.group-number }}
854+
855+
- name: Test
856+
run: |
857+
tox -vv -e ${{ steps.get-envs.outputs.envs }} -p auto
858+
env:
859+
TOX_PARALLEL_NO_SPINNER: 1
860+
PY_COLORS: 0
861+
862+
- name: Upload Coverage Artifacts
863+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2
864+
with:
865+
name: coverage-${{ github.job }}-${{ strategy.job-index }}
866+
path: ./**/.coverage.*
867+
include-hidden-files: true
868+
if-no-files-found: error
869+
retention-days: 1
870+
803871
memcached:
804872
env:
805873
TOTAL_GROUPS: 2

newrelic/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,6 +2878,8 @@ def _process_module_builtin_defaults():
28782878

28792879
_process_module_definition("cx_Oracle", "newrelic.hooks.database_cx_oracle", "instrument_cx_oracle")
28802880

2881+
_process_module_definition("oracledb", "newrelic.hooks.database_oracledb", "instrument_oracledb")
2882+
28812883
_process_module_definition("ibm_db_dbi", "newrelic.hooks.database_ibm_db_dbi", "instrument_ibm_db_dbi")
28822884

28832885
_process_module_definition("mysql.connector", "newrelic.hooks.database_mysql", "instrument_mysql_connector")

newrelic/hooks/database_oracledb.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Copyright 2010 New Relic, Inc.
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+
from newrelic.api.database_trace import register_database_client
16+
from newrelic.common.object_wrapper import wrap_object
17+
from newrelic.hooks.database_dbapi2 import ConnectionFactory as DBAPI2ConnectionFactory
18+
from newrelic.hooks.database_dbapi2 import ConnectionWrapper as DBAPI2ConnectionWrapper
19+
from newrelic.hooks.database_dbapi2 import CursorWrapper as DBAPI2CursorWrapper
20+
from newrelic.hooks.database_dbapi2_async import AsyncConnectionFactory as DBAPI2AsyncConnectionFactory
21+
from newrelic.hooks.database_dbapi2_async import AsyncConnectionWrapper as DBAPI2AsyncConnectionWrapper
22+
from newrelic.hooks.database_dbapi2_async import AsyncCursorWrapper as DBAPI2AsyncCursorWrapper
23+
24+
25+
class CursorWrapper(DBAPI2CursorWrapper):
26+
def __enter__(self):
27+
self.__wrapped__.__enter__()
28+
return self
29+
30+
31+
class ConnectionWrapper(DBAPI2ConnectionWrapper):
32+
__cursor_wrapper__ = CursorWrapper
33+
34+
def __enter__(self):
35+
self.__wrapped__.__enter__()
36+
return self
37+
38+
39+
class ConnectionFactory(DBAPI2ConnectionFactory):
40+
__connection_wrapper__ = ConnectionWrapper
41+
42+
43+
class AsyncCursorWrapper(DBAPI2AsyncCursorWrapper):
44+
async def __aenter__(self):
45+
await self.__wrapped__.__aenter__()
46+
return self
47+
48+
49+
class AsyncConnectionWrapper(DBAPI2AsyncConnectionWrapper):
50+
__cursor_wrapper__ = AsyncCursorWrapper
51+
52+
async def __aenter__(self):
53+
await self.__wrapped__.__aenter__()
54+
return self
55+
56+
def __await__(self):
57+
# Handle bidirectional generator protocol using code from generator_wrapper
58+
g = self.__wrapped__.__await__()
59+
try:
60+
yielded = g.send(None)
61+
while True:
62+
try:
63+
sent = yield yielded
64+
except GeneratorExit:
65+
g.close()
66+
raise
67+
except BaseException as e:
68+
yielded = g.throw(e)
69+
else:
70+
yielded = g.send(sent)
71+
except StopIteration as e:
72+
# Catch the StopIteration and return the wrapped connection instead of the unwrapped.
73+
if e.value is self.__wrapped__:
74+
connection = self
75+
else:
76+
connection = e.value
77+
78+
# Return here instead of raising StopIteration to properly follow generator protocol
79+
return connection
80+
81+
82+
class AsyncConnectionFactory(DBAPI2AsyncConnectionFactory):
83+
__connection_wrapper__ = AsyncConnectionWrapper
84+
85+
# Use the synchronous __call__ method as connection_async() is synchronous in oracledb.
86+
__call__ = DBAPI2ConnectionFactory.__call__
87+
88+
89+
def instance_info(args, kwargs):
90+
from oracledb import ConnectParams
91+
92+
dsn = args[0] if args else None
93+
94+
host = None
95+
port = None
96+
service_name = None
97+
98+
params_from_kwarg = kwargs.pop("params", None)
99+
100+
params_from_dsn = None
101+
if dsn:
102+
try:
103+
params_from_dsn = ConnectParams()
104+
if "@" in dsn:
105+
_, _, connect_string = params_from_dsn.parse_dsn_with_credentials(dsn)
106+
else:
107+
connect_string = dsn
108+
params_from_dsn.parse_connect_string(connect_string)
109+
except Exception:
110+
params_from_dsn = None
111+
112+
host = (
113+
getattr(params_from_kwarg, "host", None)
114+
or kwargs.get("host", None)
115+
or getattr(params_from_dsn, "host", None)
116+
or "unknown"
117+
)
118+
port = str(
119+
getattr(params_from_kwarg, "port", None)
120+
or kwargs.get("port", None)
121+
or getattr(params_from_dsn, "port", None)
122+
or "1521"
123+
)
124+
service_name = (
125+
getattr(params_from_kwarg, "service_name", None)
126+
or kwargs.get("service_name", None)
127+
or getattr(params_from_dsn, "service_name", None)
128+
or "unknown"
129+
)
130+
131+
return host, port, service_name
132+
133+
134+
def instrument_oracledb(module):
135+
register_database_client(
136+
module, database_product="Oracle", quoting_style="single+oracle", instance_info=instance_info
137+
)
138+
139+
if hasattr(module, "connect"):
140+
wrap_object(module, "connect", ConnectionFactory, (module,))
141+
142+
if hasattr(module, "connect_async"):
143+
wrap_object(module, "connect_async", AsyncConnectionFactory, (module,))

tests/datastore_oracledb/conftest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2010 New Relic, Inc.
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+
from testing_support.fixture.event_loop import event_loop as loop
17+
from testing_support.fixtures import collector_agent_registration_fixture, collector_available_fixture
18+
19+
_default_settings = {
20+
"package_reporting.enabled": False, # Turn off package reporting for testing as it causes slow downs.
21+
"transaction_tracer.explain_threshold": 0.0,
22+
"transaction_tracer.transaction_threshold": 0.0,
23+
"transaction_tracer.stack_trace_threshold": 0.0,
24+
"debug.log_data_collector_payloads": True,
25+
"debug.record_transaction_failure": True,
26+
"debug.log_explain_plan_queries": True,
27+
}
28+
29+
collector_agent_registration = collector_agent_registration_fixture(
30+
app_name="Python Agent Test (datastore_oracledb)",
31+
default_settings=_default_settings,
32+
linked_applications=["Python Agent Test (datastore)"],
33+
)

0 commit comments

Comments
 (0)