Skip to content

Commit c49a1cf

Browse files
committed
Add async client instrumentation
1 parent c857358 commit c49a1cf

File tree

4 files changed

+131
-6
lines changed

4 files changed

+131
-6
lines changed

newrelic/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2279,6 +2279,11 @@ def _process_module_builtin_defaults():
22792279
"newrelic.hooks.datastore_firestore",
22802280
"instrument_google_cloud_firestore_v1_client",
22812281
)
2282+
_process_module_definition(
2283+
"google.cloud.firestore_v1.async_client",
2284+
"newrelic.hooks.datastore_firestore",
2285+
"instrument_google_cloud_firestore_v1_async_client",
2286+
)
22822287
_process_module_definition(
22832288
"google.cloud.firestore_v1.document",
22842289
"newrelic.hooks.datastore_firestore",

newrelic/hooks/datastore_firestore.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import functools
16+
1517
from newrelic.common.object_wrapper import wrap_function_wrapper
1618
from newrelic.api.datastore_trace import wrap_datastore_trace
1719
from newrelic.api.function_trace import wrap_function_trace
18-
from newrelic.common.async_wrapper import generator_wrapper
20+
from newrelic.common.async_wrapper import generator_wrapper, async_generator_wrapper
1921
from newrelic.api.datastore_trace import DatastoreTrace
2022

2123

@@ -40,11 +42,16 @@ def _get_collection_ref_id(obj, *args, **kwargs):
4042
return None
4143

4244

43-
def wrap_generator_method(module, class_name, method_name, target):
45+
def wrap_generator_method(module, class_name, method_name, target, is_async=False):
46+
if is_async:
47+
async_wrapper = async_generator_wrapper
48+
else:
49+
async_wrapper = generator_wrapper
50+
4451
def _wrapper(wrapped, instance, args, kwargs):
4552
target_ = target(instance) if callable(target) else target
4653
trace = DatastoreTrace(product="Firestore", target=target_, operation=method_name)
47-
wrapped = generator_wrapper(wrapped, trace)
54+
wrapped = async_wrapper(wrapped, trace)
4855
return wrapped(*args, **kwargs)
4956

5057
class_ = getattr(module, class_name)
@@ -53,6 +60,9 @@ def _wrapper(wrapped, instance, args, kwargs):
5360
wrap_function_wrapper(module, "%s.%s" % (class_name, method_name), _wrapper)
5461

5562

63+
wrap_async_generator_method = functools.partial(wrap_generator_method, is_async=True)
64+
65+
5666
def instrument_google_cloud_firestore_v1_base_client(module):
5767
rollup = ("Datastore/all", "Datastore/Firestore/all")
5868
wrap_function_trace(
@@ -68,6 +78,14 @@ def instrument_google_cloud_firestore_v1_client(module):
6878
wrap_generator_method(module, "Client", method, target=None)
6979

7080

81+
def instrument_google_cloud_firestore_v1_async_client(module):
82+
if hasattr(module, "AsyncClient"):
83+
class_ = module.AsyncClient
84+
for method in ("collections", "get_all"):
85+
if hasattr(class_, method):
86+
wrap_async_generator_method(module, "AsyncClient", method, target=None)
87+
88+
7189
def instrument_google_cloud_firestore_v1_collection(module):
7290
if hasattr(module, "CollectionReference"):
7391
class_ = module.CollectionReference

tests/datastore_firestore/conftest.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

1717
import pytest
1818

19-
from google.cloud.firestore import Client
19+
from google.cloud.firestore import Client, AsyncClient
2020

2121
from newrelic.api.time_trace import current_trace
2222
from newrelic.api.datastore_trace import DatastoreTrace
2323
from testing_support.db_settings import firestore_settings
24+
from testing_support.fixture.event_loop import event_loop as loop # noqa: F401; pylint: disable=W0611
2425
from testing_support.fixtures import collector_agent_registration_fixture, collector_available_fixture # noqa: F401; pylint: disable=W0611
2526

26-
2727
DB_SETTINGS = firestore_settings()[0]
2828
FIRESTORE_HOST = DB_SETTINGS["host"]
2929
FIRESTORE_PORT = DB_SETTINGS["port"]
@@ -56,6 +56,20 @@ def collection(client):
5656
yield client.collection("firestore_collection_" + str(uuid.uuid4()))
5757

5858

59+
@pytest.fixture(scope="session")
60+
def async_client(loop):
61+
os.environ["FIRESTORE_EMULATOR_HOST"] = "%s:%d" % (FIRESTORE_HOST, FIRESTORE_PORT)
62+
client = AsyncClient()
63+
loop.run_until_complete(client.collection("healthcheck").document("healthcheck").set({}, retry=None, timeout=5)) # Ensure connection is available
64+
return client
65+
66+
67+
@pytest.fixture(scope="function")
68+
def async_collection(async_client, collection):
69+
# Use the same collection name as the collection fixture
70+
yield async_client.collection(collection.id)
71+
72+
5973
@pytest.fixture(scope="function", autouse=True)
6074
def reset_firestore(client):
6175
for coll in client.collections():
@@ -75,4 +89,24 @@ def _assert_trace_for_generator(generator_func, *args, **kwargs):
7589
assert _trace_check and all(_trace_check) # All checks are True, and at least 1 is present.
7690
assert current_trace() is txn # Generator trace has exited.
7791

78-
return _assert_trace_for_generator
92+
return _assert_trace_for_generator
93+
94+
95+
@pytest.fixture(scope="session")
96+
def assert_trace_for_async_generator(loop):
97+
def _assert_trace_for_async_generator(generator_func, *args, **kwargs):
98+
_trace_check = []
99+
txn = current_trace()
100+
assert not isinstance(txn, DatastoreTrace)
101+
102+
async def coro():
103+
# Check for generator trace on collections
104+
async for _ in generator_func(*args, **kwargs):
105+
_trace_check.append(isinstance(current_trace(), DatastoreTrace))
106+
107+
loop.run_until_complete(coro())
108+
109+
assert _trace_check and all(_trace_check) # All checks are True, and at least 1 is present.
110+
assert current_trace() is txn # Generator trace has exited.
111+
112+
return _assert_trace_for_async_generator
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
import pytest
15+
16+
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
17+
from newrelic.api.background_task import background_task
18+
from testing_support.validators.validate_database_duration import (
19+
validate_database_duration,
20+
)
21+
22+
23+
@pytest.fixture()
24+
def existing_document(collection, reset_firestore):
25+
# reset_firestore must be run before, not after this fixture
26+
doc = collection.document("document")
27+
doc.set({"x": 1})
28+
return doc
29+
30+
31+
async def _exercise_async_client(async_client, existing_document):
32+
assert len([_ async for _ in async_client.collections()]) >= 1
33+
doc = [_ async for _ in async_client.get_all([existing_document])][0]
34+
assert doc.to_dict()["x"] == 1
35+
36+
37+
def test_firestore_async_client(loop, async_client, existing_document):
38+
_test_scoped_metrics = [
39+
("Datastore/operation/Firestore/collections", 1),
40+
("Datastore/operation/Firestore/get_all", 1),
41+
]
42+
43+
_test_rollup_metrics = [
44+
("Datastore/all", 2),
45+
("Datastore/allOther", 2),
46+
]
47+
48+
@validate_database_duration()
49+
@validate_transaction_metrics(
50+
"test_firestore_async_client",
51+
scoped_metrics=_test_scoped_metrics,
52+
rollup_metrics=_test_rollup_metrics,
53+
background_task=True,
54+
)
55+
@background_task(name="test_firestore_async_client")
56+
def _test():
57+
loop.run_until_complete(_exercise_async_client(async_client, existing_document))
58+
59+
_test()
60+
61+
62+
@background_task()
63+
def test_firestore_async_client_generators(async_client, collection, assert_trace_for_async_generator):
64+
doc = collection.document("test")
65+
doc.set({})
66+
67+
assert_trace_for_async_generator(async_client.collections)
68+
assert_trace_for_async_generator(async_client.get_all, [doc])

0 commit comments

Comments
 (0)