Skip to content

Commit 98867e3

Browse files
leandrodamascenaheitorlessarubenfonseca
authored andcommitted
feat(tracer): support methods with the same name (ABCs) by including fully qualified name in v2 (aws-powertools#1486)
Co-authored-by: Heitor Lessa <[email protected]> Co-authored-by: Ruben Fonseca <[email protected]>
1 parent 8c0dd4c commit 98867e3

File tree

5 files changed

+120
-19
lines changed

5 files changed

+120
-19
lines changed

Diff for: aws_lambda_powertools/tracing/tracer.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,8 @@ def capture_method(
354354
"""Decorator to create subsegment for arbitrary functions
355355
356356
It also captures both response and exceptions as metadata
357-
and creates a subsegment named `## <method_name>`
357+
and creates a subsegment named `## <method_module.method_qualifiedname>`
358+
# see here: [Qualified name for classes and functions](https://peps.python.org/pep-3155/)
358359
359360
When running [async functions concurrently](https://docs.python.org/3/library/asyncio-task.html#id6),
360361
methods may impact each others subsegment, and can trigger
@@ -508,7 +509,8 @@ async def async_tasks():
508509
functools.partial(self.capture_method, capture_response=capture_response, capture_error=capture_error),
509510
)
510511

511-
method_name = f"{method.__name__}"
512+
# Example: app.ClassA.get_all # noqa E800
513+
method_name = f"{method.__module__}.{method.__qualname__}"
512514

513515
capture_response = resolve_truthy_env_var_choice(
514516
env=os.getenv(constants.TRACER_CAPTURE_RESPONSE_ENV, "true"), choice=capture_response

Diff for: docs/media/tracer_utility_showcase.png

-30.8 KB
Loading

Diff for: tests/e2e/tracer/handlers/same_function_name.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from abc import ABC, abstractmethod
2+
from uuid import uuid4
3+
4+
from aws_lambda_powertools import Tracer
5+
from aws_lambda_powertools.utilities.typing import LambdaContext
6+
7+
tracer = Tracer()
8+
9+
10+
class MainAbstractClass(ABC):
11+
@abstractmethod
12+
def get_all(self):
13+
raise NotImplementedError
14+
15+
16+
class Comments(MainAbstractClass):
17+
@tracer.capture_method
18+
def get_all(self):
19+
return [{"id": f"{uuid4()}", "completed": False} for _ in range(5)]
20+
21+
22+
class Todos(MainAbstractClass):
23+
@tracer.capture_method
24+
def get_all(self):
25+
return [{"id": f"{uuid4()}", "completed": False} for _ in range(5)]
26+
27+
28+
def lambda_handler(event: dict, context: LambdaContext):
29+
30+
todos = Todos()
31+
comments = Comments()
32+
33+
return {"todos": todos.get_all(), "comments": comments.get_all()}

Diff for: tests/e2e/tracer/test_tracer.py

+38-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ def basic_handler_fn(infrastructure: dict) -> str:
1515
return infrastructure.get("BasicHandler", "")
1616

1717

18+
@pytest.fixture
19+
def same_function_name_fn(infrastructure: dict) -> str:
20+
return infrastructure.get("SameFunctionName", "")
21+
22+
23+
@pytest.fixture
24+
def same_function_name_arn(infrastructure: dict) -> str:
25+
return infrastructure.get("SameFunctionNameArn", "")
26+
27+
1828
@pytest.fixture
1929
def async_fn_arn(infrastructure: dict) -> str:
2030
return infrastructure.get("AsyncCaptureArn", "")
@@ -31,9 +41,9 @@ def test_lambda_handler_trace_is_visible(basic_handler_fn_arn: str, basic_handle
3141
handler_subsegment = f"## {handler_name}"
3242
handler_metadata_key = f"{handler_name} response"
3343

34-
method_name = basic_handler.get_todos.__name__
44+
method_name = f"basic_handler.{basic_handler.get_todos.__name__}"
3545
method_subsegment = f"## {method_name}"
36-
handler_metadata_key = f"{method_name} response"
46+
method_metadata_key = f"{method_name} response"
3747

3848
trace_query = data_builder.build_trace_default_query(function_name=basic_handler_fn)
3949

@@ -46,14 +56,38 @@ def test_lambda_handler_trace_is_visible(basic_handler_fn_arn: str, basic_handle
4656

4757
assert len(trace.get_annotation(key="ColdStart", value=True)) == 1
4858
assert len(trace.get_metadata(key=handler_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 2
49-
assert len(trace.get_metadata(key=handler_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 2
59+
assert len(trace.get_metadata(key=method_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 2
5060
assert len(trace.get_subsegment(name=handler_subsegment)) == 2
5161
assert len(trace.get_subsegment(name=method_subsegment)) == 2
5262

5363

64+
def test_lambda_handler_trace_multiple_functions_same_name(same_function_name_arn: str, same_function_name_fn: str):
65+
# GIVEN
66+
method_name_todos = "same_function_name.Todos.get_all"
67+
method_subsegment_todos = f"## {method_name_todos}"
68+
method_metadata_key_todos = f"{method_name_todos} response"
69+
70+
method_name_comments = "same_function_name.Comments.get_all"
71+
method_subsegment_comments = f"## {method_name_comments}"
72+
method_metadata_key_comments = f"{method_name_comments} response"
73+
74+
trace_query = data_builder.build_trace_default_query(function_name=same_function_name_fn)
75+
76+
# WHEN
77+
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=same_function_name_arn)
78+
79+
# THEN
80+
trace = data_fetcher.get_traces(start_date=execution_time, filter_expression=trace_query)
81+
82+
assert len(trace.get_metadata(key=method_metadata_key_todos, namespace=TracerStack.SERVICE_NAME)) == 1
83+
assert len(trace.get_metadata(key=method_metadata_key_comments, namespace=TracerStack.SERVICE_NAME)) == 1
84+
assert len(trace.get_subsegment(name=method_subsegment_todos)) == 1
85+
assert len(trace.get_subsegment(name=method_subsegment_comments)) == 1
86+
87+
5488
def test_async_trace_is_visible(async_fn_arn: str, async_fn: str):
5589
# GIVEN
56-
async_fn_name = async_capture.async_get_users.__name__
90+
async_fn_name = f"async_capture.{async_capture.async_get_users.__name__}"
5791
async_fn_name_subsegment = f"## {async_fn_name}"
5892
async_fn_name_metadata_key = f"{async_fn_name} response"
5993

Diff for: tests/unit/test_tracing.py

+45-13
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
# Maintenance: This should move to Functional tests and use Fake over mocks.
1212

13+
MODULE_PREFIX = "unit.test_tracing"
14+
1315

1416
@pytest.fixture
1517
def dummy_response():
@@ -125,9 +127,13 @@ def greeting(name, message):
125127
# and add its response as trace metadata
126128
# and use service name as a metadata namespace
127129
assert in_subsegment_mock.in_subsegment.call_count == 1
128-
assert in_subsegment_mock.in_subsegment.call_args == mocker.call(name="## greeting")
130+
assert in_subsegment_mock.in_subsegment.call_args == mocker.call(
131+
name=f"## {MODULE_PREFIX}.test_tracer_method.<locals>.greeting"
132+
)
129133
assert in_subsegment_mock.put_metadata.call_args == mocker.call(
130-
key="greeting response", value=dummy_response, namespace="booking"
134+
key=f"{MODULE_PREFIX}.test_tracer_method.<locals>.greeting response",
135+
value=dummy_response,
136+
namespace="booking",
131137
)
132138

133139

@@ -253,7 +259,10 @@ def greeting(name, message):
253259
# THEN we should add the exception using method name as key plus error
254260
# and their service name as the namespace
255261
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
256-
assert put_metadata_mock_args["key"] == "greeting error"
262+
assert (
263+
put_metadata_mock_args["key"]
264+
== f"{MODULE_PREFIX}.test_tracer_method_exception_metadata.<locals>.greeting error"
265+
)
257266
assert put_metadata_mock_args["namespace"] == "booking"
258267

259268

@@ -305,15 +314,23 @@ async def greeting(name, message):
305314

306315
# THEN we should add metadata for each response like we would for a sync decorated method
307316
assert in_subsegment_mock.in_subsegment.call_count == 2
308-
assert in_subsegment_greeting_call_args == mocker.call(name="## greeting")
309-
assert in_subsegment_greeting2_call_args == mocker.call(name="## greeting_2")
317+
assert in_subsegment_greeting_call_args == mocker.call(
318+
name=f"## {MODULE_PREFIX}.test_tracer_method_nested_async.<locals>.greeting"
319+
)
320+
assert in_subsegment_greeting2_call_args == mocker.call(
321+
name=f"## {MODULE_PREFIX}.test_tracer_method_nested_async.<locals>.greeting_2"
322+
)
310323

311324
assert in_subsegment_mock.put_metadata.call_count == 2
312325
assert put_metadata_greeting2_call_args == mocker.call(
313-
key="greeting_2 response", value=dummy_response, namespace="booking"
326+
key=f"{MODULE_PREFIX}.test_tracer_method_nested_async.<locals>.greeting_2 response",
327+
value=dummy_response,
328+
namespace="booking",
314329
)
315330
assert put_metadata_greeting_call_args == mocker.call(
316-
key="greeting response", value=dummy_response, namespace="booking"
331+
key=f"{MODULE_PREFIX}.test_tracer_method_nested_async.<locals>.greeting response",
332+
value=dummy_response,
333+
namespace="booking",
317334
)
318335

319336

@@ -355,7 +372,10 @@ async def greeting(name, message):
355372
# THEN we should add the exception using method name as key plus error
356373
# and their service name as the namespace
357374
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
358-
assert put_metadata_mock_args["key"] == "greeting error"
375+
assert (
376+
put_metadata_mock_args["key"]
377+
== f"{MODULE_PREFIX}.test_tracer_method_exception_metadata_async.<locals>.greeting error"
378+
)
359379
assert put_metadata_mock_args["namespace"] == "booking"
360380

361381

@@ -387,7 +407,9 @@ def handler(event, context):
387407
assert "test result" in in_subsegment_mock.put_metadata.call_args[1]["value"]
388408
assert in_subsegment_mock.in_subsegment.call_count == 2
389409
assert handler_trace == mocker.call(name="## handler")
390-
assert yield_function_trace == mocker.call(name="## yield_with_capture")
410+
assert yield_function_trace == mocker.call(
411+
name=f"## {MODULE_PREFIX}.test_tracer_yield_from_context_manager.<locals>.yield_with_capture"
412+
)
391413
assert "test result" in result
392414

393415

@@ -411,7 +433,10 @@ def yield_with_capture():
411433
# THEN we should add the exception using method name as key plus error
412434
# and their service name as the namespace
413435
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
414-
assert put_metadata_mock_args["key"] == "yield_with_capture error"
436+
assert (
437+
put_metadata_mock_args["key"]
438+
== f"{MODULE_PREFIX}.test_tracer_yield_from_context_manager_exception_metadata.<locals>.yield_with_capture error" # noqa E501
439+
)
415440
assert isinstance(put_metadata_mock_args["value"], ValueError)
416441
assert put_metadata_mock_args["namespace"] == "booking"
417442

@@ -453,7 +478,9 @@ def handler(event, context):
453478
assert "test result" in in_subsegment_mock.put_metadata.call_args[1]["value"]
454479
assert in_subsegment_mock.in_subsegment.call_count == 2
455480
assert handler_trace == mocker.call(name="## handler")
456-
assert yield_function_trace == mocker.call(name="## yield_with_capture")
481+
assert yield_function_trace == mocker.call(
482+
name=f"## {MODULE_PREFIX}.test_tracer_yield_from_nested_context_manager.<locals>.yield_with_capture"
483+
)
457484
assert "test result" in result
458485

459486

@@ -483,7 +510,9 @@ def handler(event, context):
483510
assert "test result" in in_subsegment_mock.put_metadata.call_args[1]["value"]
484511
assert in_subsegment_mock.in_subsegment.call_count == 2
485512
assert handler_trace == mocker.call(name="## handler")
486-
assert generator_fn_trace == mocker.call(name="## generator_fn")
513+
assert generator_fn_trace == mocker.call(
514+
name=f"## {MODULE_PREFIX}.test_tracer_yield_from_generator.<locals>.generator_fn"
515+
)
487516
assert "test result" in result
488517

489518

@@ -506,7 +535,10 @@ def generator_fn():
506535
# THEN we should add the exception using method name as key plus error
507536
# and their service name as the namespace
508537
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
509-
assert put_metadata_mock_args["key"] == "generator_fn error"
538+
assert (
539+
put_metadata_mock_args["key"]
540+
== f"{MODULE_PREFIX}.test_tracer_yield_from_generator_exception_metadata.<locals>.generator_fn error"
541+
)
510542
assert put_metadata_mock_args["namespace"] == "booking"
511543
assert isinstance(put_metadata_mock_args["value"], ValueError)
512544
assert str(put_metadata_mock_args["value"]) == "test"

0 commit comments

Comments
 (0)