Skip to content

Commit abe6c6b

Browse files
authored
Lambda Support Refactor + Enhance Unit Test Coverage for AWS Resources (#385)
## What does this pull request do? Refactors changes made for AWS Lambda support to make the more maintainable and easier to reason about. These changes also improve our test coverage to assert these new Lambda behaviors as well as existing behaviors for other AWS Resources (S3, DDB, SNS, etc.). By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 8644e08 commit abe6c6b

File tree

2 files changed

+379
-38
lines changed

2 files changed

+379
-38
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
_NORMALIZED_LAMBDA_SERVICE_NAME: str = "AWS::Lambda"
104104
_DB_CONNECTION_STRING_TYPE: str = "DB::Connection"
105105

106+
# Constants for Lambda operations
107+
_LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT: str = "LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT"
108+
_LAMBDA_INVOKE_OPERATION: str = "Invoke"
109+
106110
# Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
107111
_GRAPHQL: str = "graphql"
108112

@@ -145,6 +149,7 @@ def _generate_dependency_metric_attributes(span: ReadableSpan, resource: Resourc
145149
_set_egress_operation(span, attributes)
146150
_set_remote_service_and_operation(span, attributes)
147151
_set_remote_type_and_identifier(span, attributes)
152+
_set_remote_environment(span, attributes)
148153
_set_remote_db_user(span, attributes)
149154
_set_span_kind_for_dependency(span, attributes)
150155
return attributes
@@ -317,7 +322,17 @@ def _normalize_remote_service_name(span: ReadableSpan, service_name: str) -> str
317322
"Secrets Manager": _NORMALIZED_SECRETSMANAGER_SERVICE_NAME,
318323
"SNS": _NORMALIZED_SNS_SERVICE_NAME,
319324
"SFN": _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME,
325+
"Lambda": _NORMALIZED_LAMBDA_SERVICE_NAME,
320326
}
327+
328+
# Special handling for Lambda invoke operations
329+
if _is_lambda_invoke_operation(span):
330+
lambda_function_name = span.attributes.get(AWS_LAMBDA_FUNCTION_NAME)
331+
# If Lambda name is not present, use UnknownRemoteService
332+
# This is intentional - we want to clearly indicate when the Lambda function name
333+
# is missing rather than falling back to a generic service name
334+
return lambda_function_name if lambda_function_name else UNKNOWN_REMOTE_SERVICE
335+
321336
return aws_sdk_service_mapping.get(service_name, "AWS::" + service_name)
322337
return service_name
323338

@@ -450,22 +465,9 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
450465
)[-1]
451466
cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_STEPFUNCTIONS_ACTIVITY_ARN))
452467
elif is_key_present(span, AWS_LAMBDA_FUNCTION_NAME):
453-
# Handling downstream Lambda as a service vs. an AWS resource:
454-
# - If the method call is "Invoke", we treat downstream Lambda as a service.
455-
# - Otherwise, we treat it as an AWS resource.
456-
#
457-
# This addresses a Lambda topology issue in Application Signals.
458-
# More context in PR: https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
459-
#
460-
# NOTE: The env var LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT was introduced as part of this fix.
461-
# It is optional and allows users to override the default value if needed.
462-
if span.attributes.get(_RPC_METHOD) == "Invoke":
463-
attributes[AWS_REMOTE_SERVICE] = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME))
464-
465-
attributes[AWS_REMOTE_ENVIRONMENT] = (
466-
f'lambda:{os.environ.get("LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT", "default")}'
467-
)
468-
else:
468+
# For non-Invoke Lambda operations, treat Lambda as a resource,
469+
# see normalize_remote_service_name for more information.
470+
if not _is_lambda_invoke_operation(span):
469471
remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::Function"
470472
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_NAME))
471473
cloudformation_primary_identifier = _escape_delimiters(span.attributes.get(AWS_LAMBDA_FUNCTION_ARN))
@@ -491,6 +493,32 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
491493
attributes[AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER] = cloudformation_primary_identifier
492494

493495

496+
def _set_remote_environment(span: ReadableSpan, attributes: BoundedAttributes) -> None:
497+
"""
498+
Remote environment is used to identify the environment of downstream services. Currently only
499+
set to "lambda:default" for Lambda Invoke operations when aws-api system is detected.
500+
"""
501+
# We want to treat downstream Lambdas as a service rather than a resource because
502+
# Application Signals topology map gets disconnected due to conflicting Lambda Entity
503+
# definitions
504+
# Additional context can be found in
505+
# https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
506+
if _is_lambda_invoke_operation(span):
507+
remote_environment = os.environ.get(_LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT, "").strip()
508+
if not remote_environment:
509+
remote_environment = "default"
510+
attributes[AWS_REMOTE_ENVIRONMENT] = f"lambda:{remote_environment}"
511+
512+
513+
def _is_lambda_invoke_operation(span: ReadableSpan) -> bool:
514+
"""Check if the span represents a Lambda Invoke operation."""
515+
if not is_aws_sdk_span(span):
516+
return False
517+
518+
rpc_service = _get_remote_service(span, _RPC_SERVICE)
519+
return rpc_service == "Lambda" and span.attributes.get(_RPC_METHOD) == _LAMBDA_INVOKE_OPERATION
520+
521+
494522
def _get_db_connection(span: ReadableSpan) -> None:
495523
"""
496524
RemoteResourceIdentifier is populated with rule:

0 commit comments

Comments
 (0)