Skip to content

Commit 5ec6d96

Browse files
authored
Merge branch 'main' into release_safety
2 parents 62a7795 + abe6c6b commit 5ec6d96

File tree

8 files changed

+460
-41
lines changed

8 files changed

+460
-41
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ RUN sed -i "/opentelemetry-exporter-otlp-proto-grpc/d" ./aws-opentelemetry-distr
2121
RUN mkdir workspace && pip install --target workspace ./aws-opentelemetry-distro
2222

2323
# Stage 2: Build the cp-utility binary
24-
FROM public.ecr.aws/docker/library/rust:1.82 as builder
24+
FROM public.ecr.aws/docker/library/rust:1.87 as builder
2525

2626
WORKDIR /usr/src/cp-utility
2727
COPY ./tools/cp-utility .

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:

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4+
import os
45
import sys
56
from logging import Logger, getLogger
67

78
import pkg_resources
89

910
_logger: Logger = getLogger(__name__)
1011

12+
AGENT_OBSERVABILITY_ENABLED = "AGENT_OBSERVABILITY_ENABLED"
13+
1114

1215
def is_installed(req: str) -> bool:
1316
"""Is the given required package installed?"""
@@ -21,3 +24,8 @@ def is_installed(req: str) -> bool:
2124
_logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc)
2225
return False
2326
return True
27+
28+
29+
def is_agent_observability_enabled() -> bool:
30+
"""Is the Agentic AI monitoring flag set to true?"""
31+
return os.environ.get(AGENT_OBSERVABILITY_ENABLED, "false").lower() == "true"

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE
1313
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
14+
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled
1415
from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler
1516
from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import (
1617
AttributePropagatingSpanProcessorBuilder,
@@ -27,7 +28,7 @@
2728
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
2829
from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader
2930
from amazon.opentelemetry.distro.scope_based_filtering_view import ScopeBasedRetainingView
30-
from opentelemetry._logs import set_logger_provider
31+
from opentelemetry._logs import get_logger_provider, set_logger_provider
3132
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
3233
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter
3334
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
@@ -359,7 +360,15 @@ def _customize_span_exporter(span_exporter: SpanExporter, resource: Resource) ->
359360
_logger.info("Detected using AWS OTLP Traces Endpoint.")
360361

361362
if isinstance(span_exporter, OTLPSpanExporter):
362-
span_exporter = OTLPAwsSpanExporter(endpoint=traces_endpoint)
363+
if is_agent_observability_enabled():
364+
# Span exporter needs an instance of logger provider in ai agent
365+
# observability case because we need to split input/output prompts
366+
# from span attributes and send them to the logs pipeline per
367+
# the new Gen AI semantic convention from OTel
368+
# ref: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/
369+
span_exporter = OTLPAwsSpanExporter(endpoint=traces_endpoint, logger_provider=get_logger_provider())
370+
else:
371+
span_exporter = OTLPAwsSpanExporter(endpoint=traces_endpoint)
363372

364373
else:
365374
_logger.warning(

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/traces/otlp_aws_span_exporter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from amazon.opentelemetry.distro.exporter.otlp.aws.common.aws_auth_session import AwsAuthSession
77
from opentelemetry.exporter.otlp.proto.http import Compression
88
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
9+
from opentelemetry.sdk._logs import LoggerProvider
910

1011

1112
class OTLPAwsSpanExporter(OTLPSpanExporter):
@@ -18,8 +19,10 @@ def __init__(
1819
headers: Optional[Dict[str, str]] = None,
1920
timeout: Optional[int] = None,
2021
compression: Optional[Compression] = None,
22+
logger_provider: Optional[LoggerProvider] = None,
2123
):
2224
self._aws_region = None
25+
self._logger_provider = logger_provider
2326

2427
if endpoint:
2528
self._aws_region = endpoint.split(".")[1]

0 commit comments

Comments
 (0)