diff --git a/ddtrace/_trace/tracer.py b/ddtrace/_trace/tracer.py index 12cd4d56197..7af6711fb83 100644 --- a/ddtrace/_trace/tracer.py +++ b/ddtrace/_trace/tracer.py @@ -67,6 +67,23 @@ AnyCallable = TypeVar("AnyCallable", bound=Callable) +def _default_wrap_span_name(f: Callable) -> str: + qualname_parts = f.__qualname__.split(".") + + # Functions defined in local scopes should not include in the name. + if len(qualname_parts) <= 1 or qualname_parts[-2] == "": + return f"{f.__module__}.{f.__name__}" + + try: + local_scope_index = len(qualname_parts) - 1 - qualname_parts[::-1].index("") + except ValueError: + scoped_qualname = f.__qualname__ + else: + scoped_qualname = ".".join(qualname_parts[local_scope_index + 1 :]) + + return f"{f.__module__}.{scoped_qualname}" + + def _default_span_processors_factory( profiling_span_processor: EndpointCallCounterProcessor, ) -> list[SpanProcessor]: @@ -812,8 +829,7 @@ def execute(): """ def wrap_decorator(f: AnyCallable) -> AnyCallable: - # FIXME[matt] include the class name for methods. - span_name = name if name else "%s.%s" % (f.__module__, f.__name__) + span_name = name if name else _default_wrap_span_name(f) # detect if the the given function is a coroutine and/or a generator # to use the right decorator; this initial check ensures that the diff --git a/releasenotes/notes/tracing-wrap-default-name-class-scope-9f2b8e3a1c5d4f67.yaml b/releasenotes/notes/tracing-wrap-default-name-class-scope-9f2b8e3a1c5d4f67.yaml new file mode 100644 index 00000000000..6956c1c395f --- /dev/null +++ b/releasenotes/notes/tracing-wrap-default-name-class-scope-9f2b8e3a1c5d4f67.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + tracing: The default name for spans generated by the ``@wrap`` decorator now + includes the class name for methods. diff --git a/tests/tracer/test_tracer.py b/tests/tracer/test_tracer.py index 08739274ad0..85545d80e83 100644 --- a/tests/tracer/test_tracer.py +++ b/tests/tracer/test_tracer.py @@ -225,9 +225,20 @@ def i(cls): self.assertEqual(f.i(), 3) self.assert_span_count(3) - self.spans[0].assert_matches(name="tests.tracer.test_tracer.s") - self.spans[1].assert_matches(name="tests.tracer.test_tracer.c") - self.spans[2].assert_matches(name="tests.tracer.test_tracer.i") + self.spans[0].assert_matches(name="tests.tracer.test_tracer.Foo.s") + self.spans[1].assert_matches(name="tests.tracer.test_tracer.Foo.c") + self.spans[2].assert_matches(name="tests.tracer.test_tracer.Foo.i") + + def test_tracer_wrap_local_function_no_locals_in_name(self): + @self.tracer.wrap() + def local_func(): + pass + + local_func() + + self.assert_span_count(1) + self.spans[0].assert_matches(name="tests.tracer.test_tracer.local_func") + assert "" not in self.spans[0].name def test_tracer_wrap_factory(self): def wrap_executor(tracer, fn, args, kwargs, span_name=None, service=None, resource=None, span_type=None):