Skip to content

Commit eca6411

Browse files
authored
Merge branch 'main' into fix-workflow-permissions
2 parents 941754b + 777e025 commit eca6411

File tree

14 files changed

+836
-4
lines changed

14 files changed

+836
-4
lines changed

.github/pull_request_template.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Include a link to the related GitHub issue, if applicable
99
# Testing
1010
The agent includes a suite of tests which should be used to
1111
verify your changes don't break existing functionality. These tests will run with
12-
Github Actions when a pull request is made. More details on running the tests locally can be found
13-
[here](https://github.com/newrelic/newrelic-python-agent/blob/main/CONTRIBUTING.md#testing-guidelines),
12+
Github Actions when a pull request is made. More details on running the tests locally can be found in our
13+
[testing guidelines](https://github.com/newrelic/newrelic-python-agent/blob/main/CONTRIBUTING.md#testing-guidelines),
1414
For most contributions it is strongly recommended to add additional tests which
1515
exercise your changes.

.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/common/utilization.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ class AWSUtilization(CommonUtilization):
162162
METADATA_TOKEN_PATH = "/latest/api/token" # noqa: S105
163163
HEADERS = {"X-aws-ec2-metadata-token-ttl-seconds": "21600"}
164164
VENDOR_NAME = "aws"
165+
_utilization_data = None
165166

166167
@classmethod
167168
def fetchAuthToken(cls):
@@ -184,14 +185,28 @@ def fetch(cls):
184185
try:
185186
authToken = cls.fetchAuthToken()
186187
if authToken is None:
187-
return
188+
metadata = cls._utilization_data
189+
return metadata
188190
cls.HEADERS = {"X-aws-ec2-metadata-token": authToken}
189191
with cls.CLIENT_CLS(cls.METADATA_HOST, timeout=cls.FETCH_TIMEOUT) as client:
190192
resp = client.send_request(
191193
method="GET", path=cls.METADATA_PATH, params=cls.METADATA_QUERY, headers=cls.HEADERS
192194
)
193195
if not 200 <= resp[0] < 300:
194196
raise ValueError(resp[0])
197+
# Cache this for forced agent restarts within the same
198+
# environment if return value is valid.
199+
try:
200+
response_dict = json.loads(resp[1].decode("utf-8"))
201+
availabilityZone = response_dict.get("availabilityZone", None)
202+
instanceId = response_dict.get("instanceId", None)
203+
instanceType = response_dict.get("instanceType", None)
204+
if all((availabilityZone, instanceId, instanceType)):
205+
# Cache the utilization data for reuse
206+
cls._utilization_data = resp[1]
207+
except:
208+
# Exits without caching if the response is not valid
209+
pass
195210
return resp[1]
196211
except Exception as e:
197212
_logger.debug(

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")
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/agent_unittests/aws.json

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
[
2+
{
3+
"testname": "auth token fails, no cached data",
4+
"auth_token_cls": "no_token",
5+
"uri": {
6+
"http://169.254.169.254/latest/dynamic/instance-identity/document": {
7+
"response": {
8+
"instanceId": null,
9+
"instanceType": null,
10+
"availabilityZone": null
11+
},
12+
"timeout": false
13+
}
14+
},
15+
"expected_vendors_hash": null,
16+
"expected_metrics": {
17+
"Supportability/utilization/aws/error": {
18+
"call_count": 0
19+
}
20+
}
21+
},
22+
{
23+
"testname": "auth token succeeds, but utilization data is not valid, no cached data",
24+
"auth_token_cls": "fake_token",
25+
"uri": {
26+
"http://169.254.169.254/latest/dynamic/instance-identity/document": {
27+
"response": {
28+
"instanceId": "i-test.19characters",
29+
"instanceType": null,
30+
"availabilityZone": "us-west-2b"
31+
},
32+
"timeout": false
33+
}
34+
},
35+
"expected_vendors_hash": null,
36+
"expected_metrics": {
37+
"Supportability/utilization/aws/error": {
38+
"call_count": 1
39+
}
40+
}
41+
},
42+
{
43+
"testname": "auth token succeeds, utilization data is valid, data is cached (part 1)",
44+
"auth_token_cls": "fake_token",
45+
"uri": {
46+
"http://169.254.169.254/latest/dynamic/instance-identity/document": {
47+
"response": {
48+
"instanceId": "i-test.19characters",
49+
"instanceType": "test.type",
50+
"availabilityZone": "us-west-2b"
51+
},
52+
"timeout": false
53+
}
54+
},
55+
"expected_vendors_hash": {
56+
"aws": {
57+
"instanceId": "i-test.19characters",
58+
"instanceType": "test.type",
59+
"availabilityZone": "us-west-2b"
60+
}
61+
}
62+
},
63+
{
64+
"testname": "auth token fails, but cached data exists, return cached data",
65+
"auth_token_cls": "no_token",
66+
"uri": {
67+
"http://169.254.169.254/latest/dynamic/instance-identity/document": {
68+
"response": {
69+
"instanceId": null,
70+
"instanceType": null,
71+
"availabilityZone": null
72+
},
73+
"timeout": false
74+
}
75+
},
76+
"expected_vendors_hash": {
77+
"aws": {
78+
"instanceId": "i-test.19characters",
79+
"instanceType": "test.type",
80+
"availabilityZone": "us-west-2b"
81+
}
82+
}
83+
},
84+
{
85+
"testname": "auth token succeeds, utilization data is valid, data is cached (part 2)",
86+
"auth_token_cls": "fake_token",
87+
"uri": {
88+
"http://169.254.169.254/latest/dynamic/instance-identity/document": {
89+
"response": {
90+
"instanceId": "i-test.19characters",
91+
"instanceType": "test.type",
92+
"availabilityZone": "us-east-2b"
93+
},
94+
"timeout": false
95+
}
96+
},
97+
"expected_vendors_hash": {
98+
"aws": {
99+
"instanceId": "i-test.19characters",
100+
"instanceType": "test.type",
101+
"availabilityZone": "us-east-2b"
102+
}
103+
}
104+
}
105+
]

0 commit comments

Comments
 (0)