Skip to content

Commit 5508844

Browse files
authored
Merge branch 'main' into fix-add-missing-postgresql-tests
2 parents 75dba36 + 0867109 commit 5508844

File tree

8 files changed

+755
-236
lines changed

8 files changed

+755
-236
lines changed

.github/workflows/tests.yml

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ jobs:
4040
- grpc
4141
- kafka
4242
- memcached
43-
- mongodb
43+
- mongodb3
44+
- mongodb8
4445
- mssql
4546
- mysql
4647
- nginx
@@ -1004,7 +1005,7 @@ jobs:
10041005
path: ./**/.coverage.*
10051006
retention-days: 1
10061007

1007-
mongodb:
1008+
mongodb3:
10081009
env:
10091010
TOTAL_GROUPS: 1
10101011

@@ -1066,6 +1067,68 @@ jobs:
10661067
path: ./**/.coverage.*
10671068
retention-days: 1
10681069

1070+
mongodb8:
1071+
env:
1072+
TOTAL_GROUPS: 1
1073+
1074+
strategy:
1075+
fail-fast: false
1076+
matrix:
1077+
group-number: [1]
1078+
1079+
runs-on: ubuntu-latest
1080+
container:
1081+
image: ghcr.io/newrelic/newrelic-python-agent-ci:latest
1082+
options: >-
1083+
--add-host=host.docker.internal:host-gateway
1084+
timeout-minutes: 30
1085+
services:
1086+
mongodb:
1087+
image: mongo:8.0.3
1088+
ports:
1089+
- 8080:27017
1090+
- 8081:27017
1091+
# Set health checks to wait until mongodb has started
1092+
options: >-
1093+
--health-cmd "echo 'db.runCommand(\"ping\").ok' | mongosh localhost:27017/test --quiet || exit 1"
1094+
--health-interval 10s
1095+
--health-timeout 5s
1096+
--health-retries 5
1097+
1098+
steps:
1099+
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 4.1.1
1100+
1101+
- name: Fetch git tags
1102+
run: |
1103+
git config --global --add safe.directory "$GITHUB_WORKSPACE"
1104+
git fetch --tags origin
1105+
1106+
- name: Configure pip cache
1107+
run: |
1108+
mkdir -p /github/home/.cache/pip
1109+
chown -R $(whoami) /github/home/.cache/pip
1110+
1111+
- name: Get Environments
1112+
id: get-envs
1113+
run: |
1114+
echo "envs=$(tox -l | grep '^${{ github.job }}\-' | ./.github/workflows/get-envs.py)" >> $GITHUB_OUTPUT
1115+
env:
1116+
GROUP_NUMBER: ${{ matrix.group-number }}
1117+
1118+
- name: Test
1119+
run: |
1120+
tox -vv -e ${{ steps.get-envs.outputs.envs }} -p auto
1121+
env:
1122+
TOX_PARALLEL_NO_SPINNER: 1
1123+
PY_COLORS: 0
1124+
1125+
- name: Upload Coverage Artifacts
1126+
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # 4.3.1
1127+
with:
1128+
name: coverage-${{ github.job }}-${{ strategy.job-index }}
1129+
path: ./**/.coverage.*
1130+
retention-days: 1
1131+
10691132
elasticsearchserver07:
10701133
env:
10711134
TOTAL_GROUPS: 1

newrelic/config.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3584,34 +3584,51 @@ def _process_module_builtin_defaults():
35843584
_process_module_definition(
35853585
"pymongo.synchronous.pool",
35863586
"newrelic.hooks.datastore_pymongo",
3587-
"instrument_pymongo_pool",
3587+
"instrument_pymongo_synchronous_pool",
35883588
)
3589+
_process_module_definition(
3590+
"pymongo.asynchronous.pool",
3591+
"newrelic.hooks.datastore_pymongo",
3592+
"instrument_pymongo_asynchronous_pool",
3593+
)
3594+
35893595
_process_module_definition(
35903596
"pymongo.synchronous.collection",
35913597
"newrelic.hooks.datastore_pymongo",
3592-
"instrument_pymongo_collection",
3598+
"instrument_pymongo_synchronous_collection",
35933599
)
3600+
_process_module_definition(
3601+
"pymongo.asynchronous.collection",
3602+
"newrelic.hooks.datastore_pymongo",
3603+
"instrument_pymongo_asynchronous_collection",
3604+
)
3605+
35943606
_process_module_definition(
35953607
"pymongo.synchronous.mongo_client",
35963608
"newrelic.hooks.datastore_pymongo",
3597-
"instrument_pymongo_mongo_client",
3609+
"instrument_pymongo_synchronous_mongo_client",
3610+
)
3611+
_process_module_definition(
3612+
"pymongo.asynchronous.mongo_client",
3613+
"newrelic.hooks.datastore_pymongo",
3614+
"instrument_pymongo_asynchronous_mongo_client",
35983615
)
35993616

36003617
# Older pymongo module locations
36013618
_process_module_definition(
36023619
"pymongo.connection",
36033620
"newrelic.hooks.datastore_pymongo",
3604-
"instrument_pymongo_pool",
3621+
"instrument_pymongo_synchronous_pool",
36053622
)
36063623
_process_module_definition(
36073624
"pymongo.collection",
36083625
"newrelic.hooks.datastore_pymongo",
3609-
"instrument_pymongo_collection",
3626+
"instrument_pymongo_synchronous_collection",
36103627
)
36113628
_process_module_definition(
36123629
"pymongo.mongo_client",
36133630
"newrelic.hooks.datastore_pymongo",
3614-
"instrument_pymongo_mongo_client",
3631+
"instrument_pymongo_synchronous_mongo_client",
36153632
)
36163633

36173634
# Redis v4.2+

newrelic/hooks/datastore_pymongo.py

Lines changed: 148 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,54 +14,123 @@
1414

1515
import sys
1616

17-
from newrelic.api.datastore_trace import wrap_datastore_trace
17+
from newrelic.api.datastore_trace import DatastoreTrace
1818
from newrelic.api.function_trace import wrap_function_trace
19+
from newrelic.common.object_wrapper import wrap_function_wrapper
1920

20-
_pymongo_client_methods = (
21-
"save",
22-
"insert",
23-
"update",
24-
"drop",
25-
"remove",
26-
"find_one",
27-
"find",
28-
"count",
21+
_pymongo_client_async_methods = (
22+
"aggregate",
23+
"aggregate_raw_batches",
24+
"bulk_write",
25+
"count_documents",
2926
"create_index",
30-
"ensure_index",
31-
"drop_indexes",
27+
"create_indexes",
28+
"create_search_index",
29+
"create_search_indexes",
30+
"delete_many",
31+
"delete_one",
32+
"distinct",
33+
"drop",
3234
"drop_index",
33-
"reindex",
35+
"drop_indexes",
36+
"drop_search_index",
37+
"estimated_document_count",
38+
"find_one",
39+
"find_one_and_delete",
40+
"find_one_and_replace",
41+
"find_one_and_update",
3442
"index_information",
43+
"insert_many",
44+
"insert_one",
45+
"list_indexes",
46+
"list_search_indexes",
3547
"options",
36-
"group",
3748
"rename",
38-
"distinct",
39-
"map_reduce",
40-
"inline_map_reduce",
41-
"find_and_modify",
42-
"initialize_unordered_bulk_op",
43-
"initialize_ordered_bulk_op",
44-
"bulk_write",
45-
"insert_one",
46-
"insert_many",
4749
"replace_one",
48-
"update_one",
4950
"update_many",
50-
"delete_one",
51-
"delete_many",
51+
"update_one",
52+
"update_search_index",
53+
"watch",
54+
)
55+
56+
_pymongo_client_sync_methods = (
5257
"find_raw_batches",
58+
"find",
59+
# Legacy methods from PyMongo 3
60+
"count",
61+
"ensure_index",
62+
"find_and_modify",
63+
"group",
64+
"initialize_ordered_bulk_op",
65+
"initialize_unordered_bulk_op",
66+
"inline_map_reduce",
67+
"insert",
68+
"map_reduce",
5369
"parallel_scan",
54-
"create_indexes",
55-
"list_indexes",
56-
"aggregate",
57-
"aggregate_raw_batches",
58-
"find_one_and_delete",
59-
"find_one_and_replace",
60-
"find_one_and_update",
70+
"reindex",
71+
"remove",
72+
"save",
73+
"update",
6174
)
6275

6376

64-
def instrument_pymongo_pool(module):
77+
def instance_info(collection):
78+
try:
79+
nodes = collection.database.client.nodes
80+
if len(nodes) == 1:
81+
return next(iter(nodes))
82+
except Exception:
83+
pass
84+
85+
# If there are 0 nodes we're not currently connected, return nothing.
86+
# If there are 2+ nodes we're in a load balancing setup.
87+
# Unfortunately we can't rely on a deeper method to determine the actual server we're connected to in all cases.
88+
# We can't report more than 1 server for instance info, so we opt here to ignore reporting the host/port and
89+
# leave it empty to avoid confusing customers by guessing and potentially reporting the wrong server.
90+
return None, None
91+
92+
93+
def wrap_pymongo_method(module, class_name, method_name, is_async=False):
94+
cls = getattr(module, class_name)
95+
if not hasattr(cls, method_name):
96+
return
97+
98+
# Define wrappers as closures to preserve method_name
99+
def _wrap_pymongo_method_sync(wrapped, instance, args, kwargs):
100+
target = getattr(instance, "name", None)
101+
database_name = getattr(getattr(instance, "database", None), "name", None)
102+
with DatastoreTrace(
103+
product="MongoDB", target=target, operation=method_name, database_name=database_name
104+
) as trace:
105+
response = wrapped(*args, **kwargs)
106+
107+
# Gather instance info after response to ensure client is conncected
108+
address = instance_info(instance)
109+
trace.host = address[0]
110+
trace.port_path_or_id = address[1]
111+
112+
return response
113+
114+
async def _wrap_pymongo_method_async(wrapped, instance, args, kwargs):
115+
target = getattr(instance, "name", None)
116+
database_name = getattr(getattr(instance, "database", None), "name", None)
117+
with DatastoreTrace(
118+
product="MongoDB", target=target, operation=method_name, database_name=database_name
119+
) as trace:
120+
response = await wrapped(*args, **kwargs)
121+
122+
# Gather instance info after response to ensure client is conncected
123+
address = instance_info(instance)
124+
trace.host = address[0]
125+
trace.port_path_or_id = address[1]
126+
127+
return response
128+
129+
wrapper = _wrap_pymongo_method_async if is_async else _wrap_pymongo_method_sync
130+
wrap_function_wrapper(module, f"{class_name}.{method_name}", wrapper)
131+
132+
133+
def instrument_pymongo_synchronous_pool(module):
65134
# Exit early if this is a reimport of code from the newer module location
66135
moved_module = "pymongo.synchronous.pool"
67136
if module.__name__ != moved_module and moved_module in sys.modules:
@@ -77,7 +146,22 @@ def instrument_pymongo_pool(module):
77146
)
78147

79148

80-
def instrument_pymongo_mongo_client(module):
149+
def instrument_pymongo_asynchronous_pool(module):
150+
rollup = ("Datastore/all", "Datastore/MongoDB/all")
151+
152+
# Must name function explicitly as pymongo overrides the
153+
# __getattr__() method in a way that breaks introspection.
154+
155+
wrap_function_trace(
156+
module,
157+
"AsyncConnection.__init__",
158+
name=f"{module.__name__}:AsyncConnection.__init__",
159+
terminal=True,
160+
rollup=rollup,
161+
)
162+
163+
164+
def instrument_pymongo_synchronous_mongo_client(module):
81165
# Exit early if this is a reimport of code from the newer module location
82166
moved_module = "pymongo.synchronous.mongo_client"
83167
if module.__name__ != moved_module and moved_module in sys.modules:
@@ -93,17 +177,38 @@ def instrument_pymongo_mongo_client(module):
93177
)
94178

95179

96-
def instrument_pymongo_collection(module):
180+
def instrument_pymongo_asynchronous_mongo_client(module):
181+
rollup = ("Datastore/all", "Datastore/MongoDB/all")
182+
183+
# Must name function explicitly as pymongo overrides the
184+
# __getattr__() method in a way that breaks introspection.
185+
186+
wrap_function_trace(
187+
module,
188+
"AsyncMongoClient.__init__",
189+
name=f"{module.__name__}:AsyncMongoClient.__init__",
190+
terminal=True,
191+
rollup=rollup,
192+
)
193+
194+
195+
def instrument_pymongo_synchronous_collection(module):
97196
# Exit early if this is a reimport of code from the newer module location
98197
moved_module = "pymongo.synchronous.collection"
99198
if module.__name__ != moved_module and moved_module in sys.modules:
100199
return
101200

102-
def _collection_name(collection, *args, **kwargs):
103-
return collection.name
201+
if hasattr(module, "Collection"):
202+
for method_name in _pymongo_client_sync_methods:
203+
wrap_pymongo_method(module, "Collection", method_name, is_async=False)
204+
for method_name in _pymongo_client_async_methods:
205+
# Intentionally set is_async=False for sync collection
206+
wrap_pymongo_method(module, "Collection", method_name, is_async=False)
207+
104208

105-
for name in _pymongo_client_methods:
106-
if hasattr(module.Collection, name):
107-
wrap_datastore_trace(
108-
module, f"Collection.{name}", product="MongoDB", target=_collection_name, operation=name
109-
)
209+
def instrument_pymongo_asynchronous_collection(module):
210+
if hasattr(module, "AsyncCollection"):
211+
for method_name in _pymongo_client_sync_methods:
212+
wrap_pymongo_method(module, "AsyncCollection", method_name, is_async=False)
213+
for method_name in _pymongo_client_async_methods:
214+
wrap_pymongo_method(module, "AsyncCollection", method_name, is_async=True)

tests/datastore_pymongo/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from testing_support.fixture.event_loop import event_loop as loop # noqa
1516
from testing_support.fixtures import ( # noqa: F401; pylint: disable=W0611
1617
collector_agent_registration_fixture,
1718
collector_available_fixture,

0 commit comments

Comments
 (0)