|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
| 15 | +from newrelic.api.datastore_trace import DatastoreTrace |
| 16 | +from newrelic.api.function_trace import wrap_function_trace |
15 | 17 | from newrelic.common.object_wrapper import wrap_function_wrapper |
16 | 18 |
|
17 | | -# This is NOT a fully-featured instrumentation for the motor library. Instead |
18 | | -# this is a monkey-patch of the motor library to work around a bug that causes |
19 | | -# the __name__ lookup on a MotorCollection object to fail. This bug was causing |
20 | | -# customer's applications to fail when they used motor in Tornado applications. |
| 19 | +_motor_client_sync_methods = ( |
| 20 | + "aggregate_raw_batches", |
| 21 | + "aggregate", |
| 22 | + "find_raw_batches", |
| 23 | + "find", |
| 24 | + "list_indexes", |
| 25 | + "list_search_indexes", |
| 26 | + "watch", |
| 27 | +) |
21 | 28 |
|
| 29 | +_motor_client_async_methods = ( |
| 30 | + "bulk_write", |
| 31 | + "count_documents", |
| 32 | + "create_index", |
| 33 | + "create_indexes", |
| 34 | + "create_search_index", |
| 35 | + "create_search_indexes", |
| 36 | + "delete_many", |
| 37 | + "delete_one", |
| 38 | + "distinct", |
| 39 | + "drop_index", |
| 40 | + "drop_indexes", |
| 41 | + "drop_search_index", |
| 42 | + "drop", |
| 43 | + "estimated_document_count", |
| 44 | + "find_one_and_delete", |
| 45 | + "find_one_and_replace", |
| 46 | + "find_one_and_update", |
| 47 | + "find_one", |
| 48 | + "index_information", |
| 49 | + "insert_many", |
| 50 | + "insert_one", |
| 51 | + "options", |
| 52 | + "rename", |
| 53 | + "replace_one", |
| 54 | + "update_many", |
| 55 | + "update_one", |
| 56 | + "update_search_index", |
| 57 | +) |
22 | 58 |
|
23 | | -def _nr_wrapper_Motor_getattr_(wrapped, instance, args, kwargs): |
24 | 59 |
|
25 | | - def _bind_params(name, *args, **kwargs): |
26 | | - return name |
| 60 | +def instance_info(collection): |
| 61 | + try: |
| 62 | + nodes = collection.database.client.nodes |
| 63 | + if len(nodes) == 1: |
| 64 | + return next(iter(nodes)) |
| 65 | + except Exception: |
| 66 | + pass |
27 | 67 |
|
28 | | - name = _bind_params(*args, **kwargs) |
| 68 | + # If there are 0 nodes we're not currently connected, return nothing. |
| 69 | + # If there are 2+ nodes we're in a load balancing setup. |
| 70 | + # Unfortunately we can't rely on a deeper method to determine the actual server we're connected to in all cases. |
| 71 | + # We can't report more than 1 server for instance info, so we opt here to ignore reporting the host/port and |
| 72 | + # leave it empty to avoid confusing customers by guessing and potentially reporting the wrong server. |
| 73 | + return None, None |
29 | 74 |
|
30 | | - if name.startswith('__') or name.startswith('_nr_'): |
31 | | - raise AttributeError(f'{instance.__class__.__name__} class has no attribute {name}. To access use object[{name!r}].') |
32 | 75 |
|
33 | | - return wrapped(*args, **kwargs) |
| 76 | +def wrap_motor_method(module, class_name, method_name, is_async=False): |
| 77 | + cls = getattr(module, class_name) |
| 78 | + if not hasattr(cls, method_name): |
| 79 | + return |
34 | 80 |
|
| 81 | + # Define wrappers as closures to preserve method_name |
| 82 | + def _wrap_motor_method_sync(wrapped, instance, args, kwargs): |
| 83 | + target = getattr(instance, "name", None) |
| 84 | + database_name = getattr(getattr(instance, "database", None), "name", None) |
| 85 | + with DatastoreTrace( |
| 86 | + product="MongoDB", target=target, operation=method_name, database_name=database_name |
| 87 | + ) as trace: |
| 88 | + response = wrapped(*args, **kwargs) |
35 | 89 |
|
36 | | -def patch_motor(module): |
37 | | - if (hasattr(module, 'version_tuple') and |
38 | | - module.version_tuple >= (0, 6)): |
39 | | - return |
| 90 | + # Gather instance info after response to ensure client is conncected |
| 91 | + address = instance_info(instance) |
| 92 | + trace.host = address[0] |
| 93 | + trace.port_path_or_id = address[1] |
| 94 | + |
| 95 | + return response |
| 96 | + |
| 97 | + async def _wrap_motor_method_async(wrapped, instance, args, kwargs): |
| 98 | + target = getattr(instance, "name", None) |
| 99 | + database_name = getattr(getattr(instance, "database", None), "name", None) |
| 100 | + with DatastoreTrace( |
| 101 | + product="MongoDB", target=target, operation=method_name, database_name=database_name |
| 102 | + ) as trace: |
| 103 | + response = await wrapped(*args, **kwargs) |
| 104 | + |
| 105 | + # Gather instance info after response to ensure client is conncected |
| 106 | + address = instance_info(instance) |
| 107 | + trace.host = address[0] |
| 108 | + trace.port_path_or_id = address[1] |
| 109 | + |
| 110 | + return response |
| 111 | + |
| 112 | + wrapper = _wrap_motor_method_async if is_async else _wrap_motor_method_sync |
| 113 | + wrap_function_wrapper(module, f"{class_name}.{method_name}", wrapper) |
| 114 | + |
| 115 | + |
| 116 | +def instrument_motor_motor_asyncio(module): |
| 117 | + if hasattr(module, "AsyncIOMotorClient"): |
| 118 | + rollup = ("Datastore/all", "Datastore/MongoDB/all") |
| 119 | + # Name function explicitly as motor and pymongo have a history of overriding the |
| 120 | + # __getattr__() method in a way that breaks introspection. |
| 121 | + wrap_function_trace( |
| 122 | + module, "AsyncIOMotorClient.__init__", name=f"{module.__name__}:AsyncIOMotorClient.__init__", rollup=rollup |
| 123 | + ) |
| 124 | + |
| 125 | + if hasattr(module, "AsyncIOMotorCollection"): |
| 126 | + for method_name in _motor_client_sync_methods: |
| 127 | + wrap_motor_method(module, "AsyncIOMotorCollection", method_name, is_async=False) |
| 128 | + for method_name in _motor_client_async_methods: |
| 129 | + wrap_motor_method(module, "AsyncIOMotorCollection", method_name, is_async=True) |
| 130 | + |
| 131 | + |
| 132 | +def instrument_motor_motor_tornado(module): |
| 133 | + if hasattr(module, "MotorClient"): |
| 134 | + rollup = ("Datastore/all", "Datastore/MongoDB/all") |
| 135 | + # Name function explicitly as motor and pymongo have a history of overriding the |
| 136 | + # __getattr__() method in a way that breaks introspection. |
| 137 | + wrap_function_trace( |
| 138 | + module, "MotorClient.__init__", name=f"{module.__name__}:MotorClient.__init__", rollup=rollup |
| 139 | + ) |
40 | 140 |
|
41 | | - patched_classes = ['MotorClient', 'MotorReplicaSetClient', 'MotorDatabase', |
42 | | - 'MotorCollection'] |
43 | | - for patched_class in patched_classes: |
44 | | - if hasattr(module, patched_class): |
45 | | - wrap_function_wrapper(module, f"{patched_class}.__getattr__", |
46 | | - _nr_wrapper_Motor_getattr_) |
| 141 | + if hasattr(module, "MotorCollection"): |
| 142 | + for method_name in _motor_client_sync_methods: |
| 143 | + wrap_motor_method(module, "MotorCollection", method_name, is_async=False) |
| 144 | + for method_name in _motor_client_async_methods: |
| 145 | + wrap_motor_method(module, "MotorCollection", method_name, is_async=True) |
0 commit comments