Skip to content

Commit 49fc491

Browse files
fix(tracer): add name sanitization for X-Ray subsegments (#4005)
* Adding sanitization method * Addressing Ruben's feedback * Addressing Ruben's feedback * Wording
1 parent 1072e96 commit 49fc491

File tree

5 files changed

+51
-23
lines changed

5 files changed

+51
-23
lines changed

aws_lambda_powertools/shared/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
XRAY_SDK_CORE_MODULE: str = "aws_xray_sdk.core"
77
XRAY_TRACE_ID_ENV: str = "_X_AMZN_TRACE_ID"
88
MIDDLEWARE_FACTORY_TRACE_ENV: str = "POWERTOOLS_TRACE_MIDDLEWARES"
9+
INVALID_XRAY_NAME_CHARACTERS = r"[?;*()!$~^<>]"
910

1011
# Logger constants
1112
LOGGER_LOG_SAMPLING_RATE: str = "POWERTOOLS_LOGGER_SAMPLE_RATE"

aws_lambda_powertools/shared/functions.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import itertools
55
import logging
66
import os
7+
import re
78
import warnings
89
from binascii import Error as BinAsciiError
910
from pathlib import Path
@@ -275,13 +276,10 @@ def abs_lambda_path(relative_path: str = "") -> str:
275276
If the path is empty, it will return the current working directory.
276277
"""
277278
# Retrieve the LAMBDA_TASK_ROOT environment variable or default to an empty string
278-
current_working_directory = os.environ.get("LAMBDA_TASK_ROOT", "")
279+
current_working_directory = os.environ.get("LAMBDA_TASK_ROOT", "") or str(Path.cwd())
279280

280-
# If LAMBDA_TASK_ROOT is not set, use the current working directory
281-
if not current_working_directory:
282-
current_working_directory = str(Path.cwd())
281+
return str(Path(current_working_directory, relative_path))
283282

284-
# Combine the current working directory and the relative path to get the absolute path
285-
absolute_path = str(Path(current_working_directory, relative_path))
286283

287-
return absolute_path
284+
def sanitize_xray_segment_name(name: str) -> str:
285+
return re.sub(constants.INVALID_XRAY_NAME_CHARACTERS, "", name)

aws_lambda_powertools/tracing/tracer.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
from typing import Any, Callable, Dict, List, Optional, Sequence, Union, cast, overload
99

1010
from aws_lambda_powertools.shared import constants
11-
from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice
11+
from aws_lambda_powertools.shared.functions import (
12+
resolve_env_var_choice,
13+
resolve_truthy_env_var_choice,
14+
sanitize_xray_segment_name,
15+
)
1216
from aws_lambda_powertools.shared.lazy_import import LazyLoader
1317
from aws_lambda_powertools.shared.types import AnyCallableT
1418
from aws_lambda_powertools.tracing.base import BaseProvider, BaseSegment
@@ -520,7 +524,8 @@ async def async_tasks():
520524
)
521525

522526
# Example: app.ClassA.get_all # noqa ERA001
523-
method_name = f"{method.__module__}.{method.__qualname__}"
527+
# Valid characters can be found at http://docs.aws.amazon.com/xray/latest/devguide/xray-api-segmentdocuments.html
528+
method_name = sanitize_xray_segment_name(f"{method.__module__}.{method.__qualname__}")
524529

525530
capture_response = resolve_truthy_env_var_choice(
526531
env=os.getenv(constants.TRACER_CAPTURE_RESPONSE_ENV, "true"),

tests/unit/test_shared_functions.py

+25
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
resolve_env_var_choice,
1616
resolve_max_age,
1717
resolve_truthy_env_var_choice,
18+
sanitize_xray_segment_name,
1819
strtobool,
1920
)
2021
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
@@ -175,3 +176,27 @@ def test_abs_lambda_path_w_filename_envvar(default_lambda_path):
175176
os.environ["LAMBDA_TASK_ROOT"] = default_lambda_path
176177
# Then path = env + relative_path
177178
assert abs_lambda_path(relative_path="cert/pub.cert") == str(Path(os.environ["LAMBDA_TASK_ROOT"], relative_path))
179+
180+
181+
def test_sanitize_xray_segment_name():
182+
# GIVEN a name with invalid characters
183+
invalid_name = "app?;*.lambda_function.(<locals>).get_todos!$~^<>"
184+
185+
# WHEN we sanitize this name by removing invalid characters
186+
sanitized_name = sanitize_xray_segment_name(invalid_name)
187+
188+
# THEN the sanitized name should not contain invalid characters
189+
expected_name = "app.lambda_function.locals.get_todos"
190+
assert sanitized_name == expected_name
191+
192+
193+
def test_sanitize_xray_segment_name_with_no_special_characters():
194+
# GIVEN a name without any invalid characters
195+
valid_name = "app#lambda_function"
196+
197+
# WHEN we sanitize this name
198+
sanitized_name = sanitize_xray_segment_name(valid_name)
199+
200+
# THEN the sanitized name remains the same as the original name
201+
expected_name = valid_name
202+
assert sanitized_name == expected_name

tests/unit/test_tracing.py

+13-14
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ def greeting(name, message):
127127
# and use service name as a metadata namespace
128128
assert in_subsegment_mock.in_subsegment.call_count == 1
129129
assert in_subsegment_mock.in_subsegment.call_args == mocker.call(
130-
name=f"## {MODULE_PREFIX}.test_tracer_method.<locals>.greeting",
130+
name=f"## {MODULE_PREFIX}.test_tracer_method.locals.greeting",
131131
)
132132
assert in_subsegment_mock.put_metadata.call_args == mocker.call(
133-
key=f"{MODULE_PREFIX}.test_tracer_method.<locals>.greeting response",
133+
key=f"{MODULE_PREFIX}.test_tracer_method.locals.greeting response",
134134
value=dummy_response,
135135
namespace="booking",
136136
)
@@ -261,8 +261,7 @@ def greeting(name, message):
261261
# and their service name as the namespace
262262
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
263263
assert (
264-
put_metadata_mock_args["key"]
265-
== f"{MODULE_PREFIX}.test_tracer_method_exception_metadata.<locals>.greeting error"
264+
put_metadata_mock_args["key"] == f"{MODULE_PREFIX}.test_tracer_method_exception_metadata.locals.greeting error"
266265
)
267266
assert put_metadata_mock_args["namespace"] == "booking"
268267

@@ -316,20 +315,20 @@ async def greeting(name, message):
316315
# THEN we should add metadata for each response like we would for a sync decorated method
317316
assert in_subsegment_mock.in_subsegment.call_count == 2
318317
assert in_subsegment_greeting_call_args == mocker.call(
319-
name=f"## {MODULE_PREFIX}.test_tracer_method_nested_async.<locals>.greeting",
318+
name=f"## {MODULE_PREFIX}.test_tracer_method_nested_async.locals.greeting",
320319
)
321320
assert in_subsegment_greeting2_call_args == mocker.call(
322-
name=f"## {MODULE_PREFIX}.test_tracer_method_nested_async.<locals>.greeting_2",
321+
name=f"## {MODULE_PREFIX}.test_tracer_method_nested_async.locals.greeting_2",
323322
)
324323

325324
assert in_subsegment_mock.put_metadata.call_count == 2
326325
assert put_metadata_greeting2_call_args == mocker.call(
327-
key=f"{MODULE_PREFIX}.test_tracer_method_nested_async.<locals>.greeting_2 response",
326+
key=f"{MODULE_PREFIX}.test_tracer_method_nested_async.locals.greeting_2 response",
328327
value=dummy_response,
329328
namespace="booking",
330329
)
331330
assert put_metadata_greeting_call_args == mocker.call(
332-
key=f"{MODULE_PREFIX}.test_tracer_method_nested_async.<locals>.greeting response",
331+
key=f"{MODULE_PREFIX}.test_tracer_method_nested_async.locals.greeting response",
333332
value=dummy_response,
334333
namespace="booking",
335334
)
@@ -375,7 +374,7 @@ async def greeting(name, message):
375374
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
376375
assert (
377376
put_metadata_mock_args["key"]
378-
== f"{MODULE_PREFIX}.test_tracer_method_exception_metadata_async.<locals>.greeting error"
377+
== f"{MODULE_PREFIX}.test_tracer_method_exception_metadata_async.locals.greeting error"
379378
)
380379
assert put_metadata_mock_args["namespace"] == "booking"
381380

@@ -409,7 +408,7 @@ def handler(event, context):
409408
assert in_subsegment_mock.in_subsegment.call_count == 2
410409
assert handler_trace == mocker.call(name="## handler")
411410
assert yield_function_trace == mocker.call(
412-
name=f"## {MODULE_PREFIX}.test_tracer_yield_from_context_manager.<locals>.yield_with_capture",
411+
name=f"## {MODULE_PREFIX}.test_tracer_yield_from_context_manager.locals.yield_with_capture",
413412
)
414413
assert "test result" in result
415414

@@ -436,7 +435,7 @@ def yield_with_capture():
436435
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
437436
assert (
438437
put_metadata_mock_args["key"]
439-
== f"{MODULE_PREFIX}.test_tracer_yield_from_context_manager_exception_metadata.<locals>.yield_with_capture error" # noqa E501
438+
== f"{MODULE_PREFIX}.test_tracer_yield_from_context_manager_exception_metadata.locals.yield_with_capture error" # noqa E501
440439
)
441440
assert isinstance(put_metadata_mock_args["value"], ValueError)
442441
assert put_metadata_mock_args["namespace"] == "booking"
@@ -480,7 +479,7 @@ def handler(event, context):
480479
assert in_subsegment_mock.in_subsegment.call_count == 2
481480
assert handler_trace == mocker.call(name="## handler")
482481
assert yield_function_trace == mocker.call(
483-
name=f"## {MODULE_PREFIX}.test_tracer_yield_from_nested_context_manager.<locals>.yield_with_capture",
482+
name=f"## {MODULE_PREFIX}.test_tracer_yield_from_nested_context_manager.locals.yield_with_capture",
484483
)
485484
assert "test result" in result
486485

@@ -512,7 +511,7 @@ def handler(event, context):
512511
assert in_subsegment_mock.in_subsegment.call_count == 2
513512
assert handler_trace == mocker.call(name="## handler")
514513
assert generator_fn_trace == mocker.call(
515-
name=f"## {MODULE_PREFIX}.test_tracer_yield_from_generator.<locals>.generator_fn",
514+
name=f"## {MODULE_PREFIX}.test_tracer_yield_from_generator.locals.generator_fn",
516515
)
517516
assert "test result" in result
518517

@@ -538,7 +537,7 @@ def generator_fn():
538537
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
539538
assert (
540539
put_metadata_mock_args["key"]
541-
== f"{MODULE_PREFIX}.test_tracer_yield_from_generator_exception_metadata.<locals>.generator_fn error"
540+
== f"{MODULE_PREFIX}.test_tracer_yield_from_generator_exception_metadata.locals.generator_fn error"
542541
)
543542
assert put_metadata_mock_args["namespace"] == "booking"
544543
assert isinstance(put_metadata_mock_args["value"], ValueError)

0 commit comments

Comments
 (0)