Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
- firestore
- grpc
- kafka
- oracledb
- memcached
- mongodb3
- mongodb8
Expand Down Expand Up @@ -800,6 +801,73 @@ jobs:
if-no-files-found: error
retention-days: 1

oracledb:
env:
TOTAL_GROUPS: 1

strategy:
fail-fast: false
matrix:
group-number: [1]

runs-on: ubuntu-24.04
container:
image: ghcr.io/newrelic/newrelic-python-agent-ci:latest
options: >-
--add-host=host.docker.internal:host-gateway
timeout-minutes: 30
services:
oracledb:
image: container-registry.oracle.com/database/free:latest-lite
ports:
- 8080:1521
- 8081:1521
env:
ORACLE_CHARACTERSET: utf8
ORACLE_PWD: oracle
# Set health checks to wait until container has started
options: >-
--health-cmd "timeout 5 bash -c 'cat < /dev/null > /dev/udp/127.0.0.1/11211'"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2

- name: Fetch git tags
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git fetch --tags origin

- name: Configure pip cache
run: |
mkdir -p /github/home/.cache/pip
chown -R "$(whoami)" /github/home/.cache/pip

- name: Get Environments
id: get-envs
run: |
echo "envs=$(tox -l | grep '^${{ github.job }}\-' | ./.github/workflows/get-envs.py)" >> "$GITHUB_OUTPUT"
env:
GROUP_NUMBER: ${{ matrix.group-number }}

- name: Test
run: |
tox -vv -e ${{ steps.get-envs.outputs.envs }} -p auto
env:
TOX_PARALLEL_NO_SPINNER: 1
PY_COLORS: 0

- name: Upload Coverage Artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2
with:
name: coverage-${{ github.job }}-${{ strategy.job-index }}
path: ./**/.coverage.*
include-hidden-files: true
if-no-files-found: error
retention-days: 1

memcached:
env:
TOTAL_GROUPS: 2
Expand Down
2 changes: 2 additions & 0 deletions newrelic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2878,6 +2878,8 @@ def _process_module_builtin_defaults():

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

_process_module_definition("oracledb", "newrelic.hooks.database_oracledb", "instrument_oracledb")

_process_module_definition("ibm_db_dbi", "newrelic.hooks.database_ibm_db_dbi", "instrument_ibm_db_dbi")

_process_module_definition("mysql.connector", "newrelic.hooks.database_mysql", "instrument_mysql_connector")
Expand Down
143 changes: 143 additions & 0 deletions newrelic/hooks/database_oracledb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Copyright 2010 New Relic, Inc.
#
# 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.

from newrelic.api.database_trace import register_database_client
from newrelic.common.object_wrapper import wrap_object
from newrelic.hooks.database_dbapi2 import ConnectionFactory as DBAPI2ConnectionFactory
from newrelic.hooks.database_dbapi2 import ConnectionWrapper as DBAPI2ConnectionWrapper
from newrelic.hooks.database_dbapi2 import CursorWrapper as DBAPI2CursorWrapper
from newrelic.hooks.database_dbapi2_async import AsyncConnectionFactory as DBAPI2AsyncConnectionFactory
from newrelic.hooks.database_dbapi2_async import AsyncConnectionWrapper as DBAPI2AsyncConnectionWrapper
from newrelic.hooks.database_dbapi2_async import AsyncCursorWrapper as DBAPI2AsyncCursorWrapper


class CursorWrapper(DBAPI2CursorWrapper):
def __enter__(self):
self.__wrapped__.__enter__()
return self


class ConnectionWrapper(DBAPI2ConnectionWrapper):
__cursor_wrapper__ = CursorWrapper

def __enter__(self):
self.__wrapped__.__enter__()
return self


class ConnectionFactory(DBAPI2ConnectionFactory):
__connection_wrapper__ = ConnectionWrapper


class AsyncCursorWrapper(DBAPI2AsyncCursorWrapper):
async def __aenter__(self):
await self.__wrapped__.__aenter__()
return self


class AsyncConnectionWrapper(DBAPI2AsyncConnectionWrapper):
__cursor_wrapper__ = AsyncCursorWrapper

async def __aenter__(self):
await self.__wrapped__.__aenter__()
return self

def __await__(self):
# Handle bidirectional generator protocol using code from generator_wrapper
g = self.__wrapped__.__await__()
try:
yielded = g.send(None)
while True:
try:
sent = yield yielded
except GeneratorExit:
g.close()
raise
except BaseException as e:
yielded = g.throw(e)
else:
yielded = g.send(sent)
except StopIteration as e:
# Catch the StopIteration and return the wrapped connection instead of the unwrapped.
if e.value is self.__wrapped__:
connection = self
else:
connection = e.value

# Return here instead of raising StopIteration to properly follow generator protocol
return connection


class AsyncConnectionFactory(DBAPI2AsyncConnectionFactory):
__connection_wrapper__ = AsyncConnectionWrapper

# Use the synchronous __call__ method as connection_async() is synchronous in oracledb.
__call__ = DBAPI2ConnectionFactory.__call__


def instance_info(args, kwargs):
from oracledb import ConnectParams

dsn = args[0] if args else None

host = None
port = None
service_name = None

params_from_kwarg = kwargs.pop("params", None)

params_from_dsn = None
if dsn:
try:
params_from_dsn = ConnectParams()
if "@" in dsn:
_, _, connect_string = params_from_dsn.parse_dsn_with_credentials(dsn)
else:
connect_string = dsn
params_from_dsn.parse_connect_string(connect_string)
except Exception:
params_from_dsn = None

host = (
getattr(params_from_kwarg, "host", None)
or kwargs.get("host", None)
or getattr(params_from_dsn, "host", None)
or "unknown"
)
port = str(
getattr(params_from_kwarg, "port", None)
or kwargs.get("port", None)
or getattr(params_from_dsn, "port", None)
or "1521"
)
service_name = (
getattr(params_from_kwarg, "service_name", None)
or kwargs.get("service_name", None)
or getattr(params_from_dsn, "service_name", None)
or "unknown"
)

return host, port, service_name


def instrument_oracledb(module):
register_database_client(
module, database_product="Oracle", quoting_style="single+oracle", instance_info=instance_info
)

if hasattr(module, "connect"):
wrap_object(module, "connect", ConnectionFactory, (module,))

if hasattr(module, "connect_async"):
wrap_object(module, "connect_async", AsyncConnectionFactory, (module,))
33 changes: 33 additions & 0 deletions tests/datastore_oracledb/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2010 New Relic, Inc.
#
# 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.


from testing_support.fixture.event_loop import event_loop as loop
from testing_support.fixtures import collector_agent_registration_fixture, collector_available_fixture

_default_settings = {
"package_reporting.enabled": False, # Turn off package reporting for testing as it causes slow downs.
"transaction_tracer.explain_threshold": 0.0,
"transaction_tracer.transaction_threshold": 0.0,
"transaction_tracer.stack_trace_threshold": 0.0,
"debug.log_data_collector_payloads": True,
"debug.record_transaction_failure": True,
"debug.log_explain_plan_queries": True,
}

collector_agent_registration = collector_agent_registration_fixture(
app_name="Python Agent Test (datastore_oracledb)",
default_settings=_default_settings,
linked_applications=["Python Agent Test (datastore)"],
)
Loading
Loading