1414
1515import sys
1616
17- from newrelic .api .datastore_trace import wrap_datastore_trace
17+ from newrelic .api .datastore_trace import DatastoreTrace
1818from 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 )
0 commit comments