diff --git a/.github/actions/setup-python-matrix/action.yml b/.github/actions/setup-python-matrix/action.yml index 3654f7eb2..bcb5cbc78 100644 --- a/.github/actions/setup-python-matrix/action.yml +++ b/.github/actions/setup-python-matrix/action.yml @@ -47,4 +47,4 @@ runs: shell: bash run: | python3.10 -m pip install -U pip - python3.10 -m pip install -U wheel setuptools tox virtualenv!=20.0.24 + python3.10 -m pip install -U wheel setuptools 'tox<4' virtualenv!=20.0.24 diff --git a/newrelic/core/code_level_metrics.py b/newrelic/core/code_level_metrics.py index 652715eab..ba00d93af 100644 --- a/newrelic/core/code_level_metrics.py +++ b/newrelic/core/code_level_metrics.py @@ -89,7 +89,7 @@ def extract_code_from_callable(func): # Use inspect to get file and line number file_path = inspect.getsourcefile(func) line_number = inspect.getsourcelines(func)[1] - except TypeError: + except Exception: pass # Split function path to extract class name diff --git a/tests/agent_features/_test_code_level_metrics.py b/tests/agent_features/_test_code_level_metrics.py index 90529320d..bbe3363f4 100644 --- a/tests/agent_features/_test_code_level_metrics.py +++ b/tests/agent_features/_test_code_level_metrics.py @@ -13,11 +13,12 @@ # limitations under the License. import functools + def exercise_function(): return -class ExerciseClass(): +class ExerciseClass(object): def exercise_method(self): return @@ -30,12 +31,46 @@ def exercise_class_method(cls): return -class ExerciseClassCallable(): +class ExerciseClassCallable(object): def __call__(self): return + +def exercise_method(self): + return + + +@staticmethod +def exercise_static_method(): + return + + +@classmethod +def exercise_class_method(cls): + return + + +def __call__(self): + return + + +type_dict = { + "exercise_method": exercise_method, + "exercise_static_method": exercise_static_method, + "exercise_class_method": exercise_class_method, + "exercise_lambda": lambda: None, +} +callable_type_dict = type_dict.copy() +callable_type_dict["__call__"] = __call__ + +ExerciseTypeConstructor = type("ExerciseTypeConstructor", (object,), type_dict) +ExerciseTypeConstructorCallable = type("ExerciseTypeConstructorCallable", (object,), callable_type_dict) + + CLASS_INSTANCE = ExerciseClass() CLASS_INSTANCE_CALLABLE = ExerciseClassCallable() +TYPE_CONSTRUCTOR_CLASS_INSTANCE = ExerciseTypeConstructor() +TYPE_CONSTRUCTOR_CALLABLE_CLASS_INSTANCE = ExerciseTypeConstructorCallable() -exercise_lambda = lambda: None +exercise_lambda = lambda: None # noqa: E731 exercise_partial = functools.partial(exercise_function) diff --git a/tests/agent_features/test_code_level_metrics.py b/tests/agent_features/test_code_level_metrics.py index 1d2bd6c3a..a7aeaa39a 100644 --- a/tests/agent_features/test_code_level_metrics.py +++ b/tests/agent_features/test_code_level_metrics.py @@ -12,25 +12,40 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys import sqlite3 -import newrelic.packages.six as six -import pytest +import sys -from testing_support.fixtures import override_application_settings, dt_enabled +import pytest +from _test_code_level_metrics import ( + CLASS_INSTANCE, + CLASS_INSTANCE_CALLABLE, + TYPE_CONSTRUCTOR_CALLABLE_CLASS_INSTANCE, + TYPE_CONSTRUCTOR_CLASS_INSTANCE, + ExerciseClass, + ExerciseClassCallable, + ExerciseTypeConstructor, + ExerciseTypeConstructorCallable, +) +from _test_code_level_metrics import __file__ as FILE_PATH +from _test_code_level_metrics import ( + exercise_function, + exercise_lambda, + exercise_partial, +) +from testing_support.fixtures import dt_enabled, override_application_settings from testing_support.validators.validate_span_events import validate_span_events +import newrelic.packages.six as six from newrelic.api.background_task import background_task -from newrelic.api.function_trace import FunctionTrace, FunctionTraceWrapper - -from _test_code_level_metrics import exercise_function, CLASS_INSTANCE, CLASS_INSTANCE_CALLABLE, exercise_lambda, exercise_partial, ExerciseClass, ExerciseClassCallable, __file__ as FILE_PATH - +from newrelic.api.function_trace import FunctionTrace is_pypy = hasattr(sys, "pypy_version_info") NAMESPACE = "_test_code_level_metrics" CLASS_NAMESPACE = ".".join((NAMESPACE, "ExerciseClass")) CALLABLE_CLASS_NAMESPACE = ".".join((NAMESPACE, "ExerciseClassCallable")) +TYPE_CONSTRUCTOR_NAMESPACE = ".".join((NAMESPACE, "ExerciseTypeConstructor")) +TYPE_CONSTRUCTOR_CALLABLE_NAMESPACE = ".".join((NAMESPACE, "ExerciseTypeConstructorCallable")) FUZZY_NAMESPACE = CLASS_NAMESPACE if six.PY3 else NAMESPACE if FILE_PATH.endswith(".pyc"): FILE_PATH = FILE_PATH[:-1] @@ -39,115 +54,166 @@ BUILTIN_ATTRS = {"code.filepath": "", "code.lineno": None} if not is_pypy else {} + def merge_dicts(A, B): d = {} d.update(A) d.update(B) return d -@pytest.mark.parametrize( - "func,args,agents", - ( - ( # Function - exercise_function, - (), - { - "code.filepath": FILE_PATH, - "code.function": "exercise_function", - "code.lineno": 16, - "code.namespace": NAMESPACE, - }, - ), - ( # Method - CLASS_INSTANCE.exercise_method, - (), - { - "code.filepath": FILE_PATH, - "code.function": "exercise_method", - "code.lineno": 21, - "code.namespace": CLASS_NAMESPACE, - }, - ), - ( # Static Method - CLASS_INSTANCE.exercise_static_method, - (), - { - "code.filepath": FILE_PATH, - "code.function": "exercise_static_method", - "code.lineno": 24, - "code.namespace": FUZZY_NAMESPACE, - }, - ), - ( # Class Method - ExerciseClass.exercise_class_method, - (), - { - "code.filepath": FILE_PATH, - "code.function": "exercise_class_method", - "code.lineno": 28, - "code.namespace": CLASS_NAMESPACE, - }, - ), - ( # Callable object - CLASS_INSTANCE_CALLABLE, - (), - { - "code.filepath": FILE_PATH, - "code.function": "__call__", - "code.lineno": 34, - "code.namespace": CALLABLE_CLASS_NAMESPACE, - }, - ), - ( # Lambda - exercise_lambda, - (), - { - "code.filepath": FILE_PATH, - "code.function": "", - "code.lineno": 40, - "code.namespace": NAMESPACE, - }, - ), - ( # Functools Partials - exercise_partial, - (), + +@pytest.fixture +def extract(): + def _extract(obj): + with FunctionTrace("_test", source=obj): + pass + + return _extract + + +_TEST_BASIC_CALLABLES = { + "function": ( + exercise_function, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_function", + "code.lineno": 17, + "code.namespace": NAMESPACE, + }, + ), + "lambda": ( + exercise_lambda, + (), + { + "code.filepath": FILE_PATH, + "code.function": "", + "code.lineno": 75, + "code.namespace": NAMESPACE, + }, + ), + "partial": ( + exercise_partial, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_function", + "code.lineno": 17, + "code.namespace": NAMESPACE, + }, + ), + "builtin_function": ( + max, + (1, 2), + merge_dicts( { - "code.filepath": FILE_PATH, - "code.function": "exercise_function", - "code.lineno": 16, - "code.namespace": NAMESPACE, - }, - ), - ( # Top Level Builtin - max, - (1, 2), - merge_dicts({ "code.function": "max", "code.namespace": "builtins" if six.PY3 else "__builtin__", - }, BUILTIN_ATTRS), + }, + BUILTIN_ATTRS, ), - ( # Module Level Builtin - sqlite3.connect, - (":memory:",), - merge_dicts({ + ), + "builtin_module_function": ( + sqlite3.connect, + (":memory:",), + merge_dicts( + { "code.function": "connect", "code.namespace": "_sqlite3", - }, BUILTIN_ATTRS), + }, + BUILTIN_ATTRS, ), - ( # Builtin Method - SQLITE_CONNECTION.__enter__, - (), - merge_dicts({ + ), +} + + +@pytest.mark.parametrize( + "func,args,agents", + [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_BASIC_CALLABLES)], +) +def test_code_level_metrics_basic_callables(func, args, agents, extract): + @override_application_settings( + { + "code_level_metrics.enabled": True, + } + ) + @dt_enabled + @validate_span_events( + count=1, + exact_agents=agents, + ) + @background_task() + def _test(): + extract(func) + + _test() + + +_TEST_METHODS = { + "method": ( + CLASS_INSTANCE.exercise_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_method", + "code.lineno": 22, + "code.namespace": CLASS_NAMESPACE, + }, + ), + "static_method": ( + CLASS_INSTANCE.exercise_static_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_static_method", + "code.lineno": 25, + "code.namespace": FUZZY_NAMESPACE, + }, + ), + "class_method": ( + ExerciseClass.exercise_class_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_class_method", + "code.lineno": 29, + "code.namespace": CLASS_NAMESPACE, + }, + ), + "call_method": ( + CLASS_INSTANCE_CALLABLE, + (), + { + "code.filepath": FILE_PATH, + "code.function": "__call__", + "code.lineno": 35, + "code.namespace": CALLABLE_CLASS_NAMESPACE, + }, + ), + "builtin_method": ( + SQLITE_CONNECTION.__enter__, + (), + merge_dicts( + { "code.function": "__enter__", "code.namespace": "sqlite3.Connection" if not is_pypy else "_sqlite3.Connection", - }, BUILTIN_ATTRS), + }, + BUILTIN_ATTRS, ), ), +} + + +@pytest.mark.parametrize( + "func,args,agents", + [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_METHODS)], ) -def test_code_level_metrics_callables(func, args, agents): - @override_application_settings({ - "code_level_metrics.enabled": True, - }) +def test_code_level_metrics_methods(func, args, agents, extract): + @override_application_settings( + { + "code_level_metrics.enabled": True, + } + ) @dt_enabled @validate_span_events( count=1, @@ -155,47 +221,145 @@ def test_code_level_metrics_callables(func, args, agents): ) @background_task() def _test(): - FunctionTraceWrapper(func)(*args) + extract(func) _test() +_TEST_TYPE_CONSTRUCTOR_METHODS = { + "method": ( + TYPE_CONSTRUCTOR_CLASS_INSTANCE.exercise_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_method", + "code.lineno": 39, + "code.namespace": TYPE_CONSTRUCTOR_NAMESPACE, + }, + ), + "static_method": ( + TYPE_CONSTRUCTOR_CLASS_INSTANCE.exercise_static_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_static_method", + "code.lineno": 43, + "code.namespace": NAMESPACE, + }, + ), + "class_method": ( + ExerciseTypeConstructor.exercise_class_method, + (), + { + "code.filepath": FILE_PATH, + "code.function": "exercise_class_method", + "code.lineno": 48, + "code.namespace": TYPE_CONSTRUCTOR_NAMESPACE, + }, + ), + "lambda_method": ( + ExerciseTypeConstructor.exercise_lambda, + (), + { + "code.filepath": FILE_PATH, + "code.function": "", + "code.lineno": 61, + # Lambdas behave strangely in type constructors on Python 2 and use the class namespace. + "code.namespace": NAMESPACE if six.PY3 else TYPE_CONSTRUCTOR_NAMESPACE, + }, + ), + "call_method": ( + TYPE_CONSTRUCTOR_CALLABLE_CLASS_INSTANCE, + (), + { + "code.filepath": FILE_PATH, + "code.function": "__call__", + "code.lineno": 53, + "code.namespace": TYPE_CONSTRUCTOR_CALLABLE_NAMESPACE, + }, + ), +} + + @pytest.mark.parametrize( - "obj,agents", - ( - ( # Class with __call__ - ExerciseClassCallable, - { - "code.filepath": FILE_PATH, - "code.function": "ExerciseClassCallable", - "code.lineno": 33, - "code.namespace":NAMESPACE, - }, - ), - ( # Class without __call__ - ExerciseClass, - { - "code.filepath": FILE_PATH, - "code.function": "ExerciseClass", - "code.lineno": 20, - "code.namespace": NAMESPACE, - }, - ), - ( # Non-callable Object instance - CLASS_INSTANCE, - { - "code.filepath": FILE_PATH, - "code.function": "ExerciseClass", - "code.lineno": 20, - "code.namespace": NAMESPACE, - }, - ), + "func,args,agents", + [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_TYPE_CONSTRUCTOR_METHODS)], +) +def test_code_level_metrics_type_constructor_methods(func, args, agents, extract): + @override_application_settings( + { + "code_level_metrics.enabled": True, + } + ) + @dt_enabled + @validate_span_events( + count=1, + exact_agents=agents, + ) + @background_task() + def _test(): + extract(func) + + _test() + + +_TEST_OBJECTS = { + "class": ( + ExerciseClass, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseClass", + "code.lineno": 21, + "code.namespace": NAMESPACE, + }, + ), + "callable_class": ( + ExerciseClassCallable, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseClassCallable", + "code.lineno": 34, + "code.namespace": NAMESPACE, + }, + ), + "type_constructor_class": ( + ExerciseTypeConstructor, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseTypeConstructor", + "code.namespace": NAMESPACE, + }, + ), + "type_constructor_class_callable_class": ( + ExerciseTypeConstructorCallable, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseTypeConstructorCallable", + "code.namespace": NAMESPACE, + }, ), + "non_callable_object": ( + CLASS_INSTANCE, + { + "code.filepath": FILE_PATH, + "code.function": "ExerciseClass", + "code.lineno": 21, + "code.namespace": NAMESPACE, + }, + ), +} + + +@pytest.mark.parametrize( + "obj,agents", + [pytest.param(*args, id=id_) for id_, args in six.iteritems(_TEST_OBJECTS)], ) -def test_code_level_metrics_objects(obj, agents): - @override_application_settings({ - "code_level_metrics.enabled": True, - }) +def test_code_level_metrics_objects(obj, agents, extract): + @override_application_settings( + { + "code_level_metrics.enabled": True, + } + ) @dt_enabled @validate_span_events( count=1, @@ -203,7 +367,6 @@ def test_code_level_metrics_objects(obj, agents): ) @background_task() def _test(): - with FunctionTrace("_test", source=obj): - pass - - _test() \ No newline at end of file + extract(obj) + + _test()