Skip to content

Commit a4d8232

Browse files
authored
Merge branch 'main' into celery-custom-task-instrumentation
2 parents b7fa0ec + d4e359a commit a4d8232

File tree

13 files changed

+638
-36
lines changed

13 files changed

+638
-36
lines changed

.github/workflows/deploy-python.yml renamed to .github/workflows/deploy.yml

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ on:
1919
types:
2020
- published
2121

22+
permissions:
23+
contents: read
24+
2225
jobs:
2326
build-linux-py3-legacy:
2427
runs-on: ubuntu-24.04
@@ -110,18 +113,22 @@ jobs:
110113
persist-credentials: false
111114
fetch-depth: 0
112115

116+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
117+
with:
118+
python-version: "3.12"
119+
113120
- name: Install Dependencies
114121
run: |
115122
pip install -U pip
116-
pip install -U setuptools packaging
123+
pip install -U build
117124
118125
- name: Build Source Package
119126
run: |
120-
python setup.py sdist
127+
python -m build --sdist
121128
122129
- name: Prepare MD5 Hash File
123130
run: |
124-
tarball="$(python setup.py --fullname).tar.gz"
131+
tarball="$(basename ./dist/*.tar.gz)"
125132
md5_file="${tarball}.md5"
126133
openssl md5 -binary "dist/${tarball}" | xxd -p | tr -d '\n' > "dist/${md5_file}"
127134
@@ -135,58 +142,46 @@ jobs:
135142
if-no-files-found: error
136143
retention-days: 1
137144

138-
deploy:
145+
publish:
139146
runs-on: ubuntu-24.04
147+
environment: pypi
148+
permissions:
149+
contents: read
150+
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
151+
attestations: write
140152

141153
needs:
142154
- build-linux-py3-legacy
143155
- build-linux-py3
144156
- build-sdist
145157

146158
steps:
147-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
148-
with:
149-
persist-credentials: false
150-
fetch-depth: 0
151-
152-
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0
153-
with:
154-
python-version: "3.x"
155-
architecture: x64
156-
157-
- name: Install Dependencies
158-
run: |
159-
pip install -U pip
160-
pip install -U wheel setuptools packaging twine
161-
162-
- name: Download Artifacts
163-
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # 4.3.0@95815c38cf2ff2164869cbab79da8d1f422bc89e # 4.3.0
159+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # 4.3.0
164160
with:
165-
path: ./artifacts/
166-
167-
- name: Unpack Artifacts
168-
run: |
169-
mkdir -p dist/
170-
mv artifacts/**/*{.whl,.tar.gz,.tar.gz.md5} dist/
161+
path: ./dist/
162+
merge-multiple: true
171163

172164
- name: Upload Package to S3
173165
run: |
174-
tarball="$(python setup.py --fullname).tar.gz"
166+
tarball="$(basename ./dist/*.tar.gz)"
175167
md5_file="${tarball}.md5"
176168
aws s3 cp "dist/${md5_file}" "${S3_DST}/${md5_file}"
177169
aws s3 cp "dist/${tarball}" "${S3_DST}/${tarball}"
170+
rm "dist/${md5_file}"
178171
env:
179172
S3_DST: s3://nr-downloads-main/python_agent/release
180173
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
181174
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
182175
AWS_DEFAULT_REGION: us-west-2
183176

184-
- name: Upload Package to PyPI
185-
run: |
186-
twine upload --non-interactive dist/*.tar.gz dist/*.whl
187-
env:
188-
TWINE_USERNAME: __token__
189-
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
177+
- name: Upload Package
178+
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # 1.12.4
179+
180+
- name: Attest
181+
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # 2.3.0
182+
id: attest
183+
with:
184+
subject-path: ./dist/*
190185

191186
- name: Wait for release to be available
192187
id: wait

.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

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pip install newrelic
4444
Start](https://docs.newrelic.com/docs/agents/python-agent/getting-started/python-agent-quick-start).)
4545

4646
1. Generate the agent configuration file with your [license
47-
key](https://docs.newrelic.com/docs/accounts-partnerships/accounts/account-setup/license-key).
47+
key](https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/).
4848

4949
```bash
5050
newrelic-admin generate-config $YOUR_LICENSE_KEY newrelic.ini

newrelic/api/message_transaction.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(
5555
self.routing_key = routing_key
5656
self.exchange_type = exchange_type
5757
self.queue_name = queue_name
58+
self.destination_name = destination_name
5859
self.reply_to = reply_to
5960
self.correlation_id = correlation_id
6061

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,))

newrelic/hooks/messagebroker_kombu.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ def wrap_consumer_recieve_callback(wrapped, instance, args, kwargs):
194194
source=wrapped,
195195
)
196196
created_transaction.__enter__()
197-
created_transaction.destination_name = destination_name
198197

199198
# Obtain consumer client_id to send up as agent attribute
200199
if hasattr(message, "channel") and hasattr(message.channel, "channel_id"):

0 commit comments

Comments
 (0)