Skip to content

Commit 8ff6d29

Browse files
authored
Wildcarding in Import Hook Config (#271)
* Add globbing to import hooks config * Add module globbing unittest * Add additional tests and comments
1 parent 697a326 commit 8ff6d29

File tree

3 files changed

+132
-70
lines changed

3 files changed

+132
-70
lines changed

newrelic/config.py

+102-70
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+
import fnmatch
1516
import os
1617
import sys
1718
import logging
@@ -1267,19 +1268,50 @@ def _process_module_configuration():
12671268
_raise_configuration_error(section)
12681269

12691270

1271+
def _module_function_glob(module, object_path):
1272+
"""Match functions and class methods in a module to file globbing syntax."""
1273+
if not any([c in object_path for c in {"*", "?", "["}]): # Identify globbing patterns
1274+
return (object_path,) # Returned value must be iterable
1275+
else:
1276+
# Gather module functions
1277+
try:
1278+
available_functions = {k: v for k, v in module.__dict__.items() if callable(v) and not isinstance(v, type)}
1279+
except Exception:
1280+
# Default to empty dict if no functions available
1281+
available_functions = dict()
1282+
1283+
# Gather module classes and methods
1284+
try:
1285+
available_classes = {k: v for k, v in module.__dict__.items() if isinstance(v, type)}
1286+
for cls in available_classes:
1287+
try:
1288+
# Skip adding individual class's methods on failure
1289+
available_functions.update({"%s.%s" % (cls, k): v for k, v in available_classes.get(cls).__dict__.items() if callable(v) and not isinstance(v, type)})
1290+
except Exception:
1291+
pass
1292+
except Exception:
1293+
# Skip adding all class methods on failure
1294+
pass
1295+
1296+
# Under the hood uses fnmatch, which uses os.path.normcase
1297+
# On windows this would cause issues with case insensitivity,
1298+
# but on all other operating systems there should be no issues.
1299+
return fnmatch.filter(available_functions, object_path)
1300+
1301+
12701302
# Setup wsgi application wrapper defined in configuration file.
12711303

12721304

12731305
def _wsgi_application_import_hook(object_path, application):
12741306
def _instrument(target):
1275-
_logger.debug(
1276-
"wrap wsgi-application %s" % ((target, object_path, application),)
1277-
)
1278-
12791307
try:
1280-
newrelic.api.wsgi_application.wrap_wsgi_application(
1281-
target, object_path, application
1282-
)
1308+
for func in _module_function_glob(target, object_path):
1309+
_logger.debug(
1310+
"wrap wsgi-application %s" % ((target, func, application),)
1311+
)
1312+
newrelic.api.wsgi_application.wrap_wsgi_application(
1313+
target, func, application
1314+
)
12831315
except Exception:
12841316
_raise_instrumentation_error("wsgi-application", locals())
12851317

@@ -1327,15 +1359,15 @@ def _process_wsgi_application_configuration():
13271359

13281360
def _background_task_import_hook(object_path, application, name, group):
13291361
def _instrument(target):
1330-
_logger.debug(
1331-
"wrap background-task %s"
1332-
% ((target, object_path, application, name, group),)
1333-
)
1334-
13351362
try:
1336-
newrelic.api.background_task.wrap_background_task(
1337-
target, object_path, application, name, group
1338-
)
1363+
for func in _module_function_glob(target, object_path):
1364+
_logger.debug(
1365+
"wrap background-task %s"
1366+
% ((target, func, application, name, group),)
1367+
)
1368+
newrelic.api.background_task.wrap_background_task(
1369+
target, func, application, name, group
1370+
)
13391371
except Exception:
13401372
_raise_instrumentation_error("background-task", locals())
13411373

@@ -1394,10 +1426,10 @@ def _process_background_task_configuration():
13941426

13951427
def _database_trace_import_hook(object_path, sql):
13961428
def _instrument(target):
1397-
_logger.debug("wrap database-trace %s" % ((target, object_path, sql),))
1398-
13991429
try:
1400-
newrelic.api.database_trace.wrap_database_trace(target, object_path, sql)
1430+
for func in _module_function_glob(target, object_path):
1431+
_logger.debug("wrap database-trace %s" % ((target, func, sql),))
1432+
newrelic.api.database_trace.wrap_database_trace(target, func, sql)
14011433
except Exception:
14021434
_raise_instrumentation_error("database-trace", locals())
14031435

@@ -1444,14 +1476,14 @@ def _process_database_trace_configuration():
14441476

14451477
def _external_trace_import_hook(object_path, library, url, method):
14461478
def _instrument(target):
1447-
_logger.debug(
1448-
"wrap external-trace %s" % ((target, object_path, library, url, method),)
1449-
)
1450-
14511479
try:
1452-
newrelic.api.external_trace.wrap_external_trace(
1453-
target, object_path, library, url, method
1454-
)
1480+
for func in _module_function_glob(target, object_path):
1481+
_logger.debug(
1482+
"wrap external-trace %s" % ((target, func, library, url, method),)
1483+
)
1484+
newrelic.api.external_trace.wrap_external_trace(
1485+
target, func, library, url, method
1486+
)
14551487
except Exception:
14561488
_raise_instrumentation_error("external-trace", locals())
14571489

@@ -1512,15 +1544,15 @@ def _function_trace_import_hook(
15121544
object_path, name, group, label, params, terminal, rollup
15131545
):
15141546
def _instrument(target):
1515-
_logger.debug(
1516-
"wrap function-trace %s"
1517-
% ((target, object_path, name, group, label, params, terminal, rollup),)
1518-
)
1519-
15201547
try:
1521-
newrelic.api.function_trace.wrap_function_trace(
1522-
target, object_path, name, group, label, params, terminal, rollup
1523-
)
1548+
for func in _module_function_glob(target, object_path):
1549+
_logger.debug(
1550+
"wrap function-trace %s"
1551+
% ((target, func, name, group, label, params, terminal, rollup),)
1552+
)
1553+
newrelic.api.function_trace.wrap_function_trace(
1554+
target, func, name, group, label, params, terminal, rollup
1555+
)
15241556
except Exception:
15251557
_raise_instrumentation_error("function-trace", locals())
15261558

@@ -1588,12 +1620,12 @@ def _process_function_trace_configuration():
15881620

15891621
def _generator_trace_import_hook(object_path, name, group):
15901622
def _instrument(target):
1591-
_logger.debug("wrap generator-trace %s" % ((target, object_path, name, group),))
1592-
15931623
try:
1594-
newrelic.api.generator_trace.wrap_generator_trace(
1595-
target, object_path, name, group
1596-
)
1624+
for func in _module_function_glob(target, object_path):
1625+
_logger.debug("wrap generator-trace %s" % ((target, func, name, group),))
1626+
newrelic.api.generator_trace.wrap_generator_trace(
1627+
target, func, name, group
1628+
)
15971629
except Exception:
15981630
_raise_instrumentation_error("generator-trace", locals())
15991631

@@ -1648,14 +1680,14 @@ def _process_generator_trace_configuration():
16481680

16491681
def _profile_trace_import_hook(object_path, name, group, depth):
16501682
def _instrument(target):
1651-
_logger.debug(
1652-
"wrap profile-trace %s" % ((target, object_path, name, group, depth),)
1653-
)
1654-
16551683
try:
1656-
newrelic.api.profile_trace.wrap_profile_trace(
1657-
target, object_path, name, group, depth=depth
1658-
)
1684+
for func in _module_function_glob(target, object_path):
1685+
_logger.debug(
1686+
"wrap profile-trace %s" % ((target, func, name, group, depth),)
1687+
)
1688+
newrelic.api.profile_trace.wrap_profile_trace(
1689+
target, func, name, group, depth=depth
1690+
)
16591691
except Exception:
16601692
_raise_instrumentation_error("profile-trace", locals())
16611693

@@ -1714,12 +1746,12 @@ def _process_profile_trace_configuration():
17141746

17151747
def _memcache_trace_import_hook(object_path, command):
17161748
def _instrument(target):
1717-
_logger.debug("wrap memcache-trace %s" % ((target, object_path, command),))
1718-
17191749
try:
1720-
newrelic.api.memcache_trace.wrap_memcache_trace(
1721-
target, object_path, command
1722-
)
1750+
for func in _module_function_glob(target, object_path):
1751+
_logger.debug("wrap memcache-trace %s" % ((target, func, command),))
1752+
newrelic.api.memcache_trace.wrap_memcache_trace(
1753+
target, func, command
1754+
)
17231755
except Exception:
17241756
_raise_instrumentation_error("memcache-trace", locals())
17251757

@@ -1768,14 +1800,14 @@ def _process_memcache_trace_configuration():
17681800

17691801
def _transaction_name_import_hook(object_path, name, group, priority):
17701802
def _instrument(target):
1771-
_logger.debug(
1772-
"wrap transaction-name %s" % ((target, object_path, name, group, priority),)
1773-
)
1774-
17751803
try:
1776-
newrelic.api.transaction_name.wrap_transaction_name(
1777-
target, object_path, name, group, priority
1778-
)
1804+
for func in _module_function_glob(target, object_path):
1805+
_logger.debug(
1806+
"wrap transaction-name %s" % ((target, func, name, group, priority),)
1807+
)
1808+
newrelic.api.transaction_name.wrap_transaction_name(
1809+
target, func, name, group, priority
1810+
)
17791811
except Exception:
17801812
_raise_instrumentation_error("transaction-name", locals())
17811813

@@ -1837,12 +1869,12 @@ def _process_transaction_name_configuration():
18371869

18381870
def _error_trace_import_hook(object_path, ignore, expected):
18391871
def _instrument(target):
1840-
_logger.debug("wrap error-trace %s" % ((target, object_path, ignore, expected),))
1841-
18421872
try:
1843-
newrelic.api.error_trace.wrap_error_trace(
1844-
target, object_path, ignore, expected, None
1845-
)
1873+
for func in _module_function_glob(target, object_path):
1874+
_logger.debug("wrap error-trace %s" % ((target, func, ignore, expected),))
1875+
newrelic.api.error_trace.wrap_error_trace(
1876+
target, func, ignore, expected, None
1877+
)
18461878
except Exception:
18471879
_raise_instrumentation_error("error-trace", locals())
18481880

@@ -2009,15 +2041,15 @@ def _setup_data_source():
20092041

20102042
def _function_profile_import_hook(object_path, filename, delay, checkpoint):
20112043
def _instrument(target):
2012-
_logger.debug(
2013-
"wrap function-profile %s"
2014-
% ((target, object_path, filename, delay, checkpoint),)
2015-
)
2016-
20172044
try:
2018-
newrelic.api.function_profile.wrap_function_profile(
2019-
target, object_path, filename, delay, checkpoint
2020-
)
2045+
for func in _module_function_glob(target, object_path):
2046+
_logger.debug(
2047+
"wrap function-profile %s"
2048+
% ((target, func, filename, delay, checkpoint),)
2049+
)
2050+
newrelic.api.function_profile.wrap_function_profile(
2051+
target, func, filename, delay, checkpoint
2052+
)
20212053
except Exception:
20222054
_raise_instrumentation_error("function-profile", locals())
20232055

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
def run():
2+
pass
3+
4+
class A(object):
5+
def run():
6+
pass
7+
8+
class B(object):
9+
def run():
10+
pass

tests/agent_unittests/test_import_hook.py

+20
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import newrelic.packages.six as six
1919
import pytest
2020

21+
from newrelic.config import _module_function_glob
22+
2123
# a dummy hook just to be able to register hooks for modules
2224
def hook(*args, **kwargs):
2325
pass
@@ -59,3 +61,21 @@ def test_import_hook_finder(monkeypatch):
5961
# Finding a module that exists, and is registered, finds that module.
6062
module = finder.find_module("newrelic.api")
6163
assert module is not None
64+
65+
66+
@pytest.mark.parametrize("input,expected", [
67+
("*", {"run", "A.run", "B.run"}),
68+
("NotFound.*", set()),
69+
("r*", {"run"}),
70+
("*.run", {"A.run", "B.run"}),
71+
("A.*", {"A.run"}),
72+
("[A,B].run", {"A.run", "B.run"}),
73+
("B.r?n", {"B.run"}),
74+
("*.RUN", set()), # Check for case insensitivity issues
75+
])
76+
def test_module_function_globbing(input, expected):
77+
"""This asserts the behavior of filename style globbing on modules."""
78+
import _test_import_hook as module
79+
80+
result = set(_module_function_glob(module, input))
81+
assert result == expected, (result, expected)

0 commit comments

Comments
 (0)