Skip to content

Commit 886b009

Browse files
committed
Merge remote-tracking branch 'upstream/main' into logs-mainline
2 parents 7dbcb7e + 12fabd6 commit 886b009

18 files changed

+4617
-347
lines changed

.github/workflows/release-lambda.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99
aws_region:
1010
description: 'Deploy to aws regions'
1111
required: true
12-
default: 'us-east-1, us-east-2, us-west-1, us-west-2, ap-south-1, ap-northeast-3, ap-northeast-2, ap-southeast-1, ap-southeast-2, ap-northeast-1, ca-central-1, eu-central-1, eu-west-1, eu-west-2, eu-west-3, eu-north-1, sa-east-1, af-south-1, ap-east-1, ap-south-2, ap-southeast-3, ap-southeast-4, eu-central-2, eu-south-1, eu-south-2, il-central-1, me-central-1, me-south-1'
12+
default: 'us-east-1, us-east-2, us-west-1, us-west-2, ap-south-1, ap-northeast-3, ap-northeast-2, ap-southeast-1, ap-southeast-2, ap-northeast-1, ca-central-1, eu-central-1, eu-west-1, eu-west-2, eu-west-3, eu-north-1, sa-east-1, af-south-1, ap-east-1, ap-south-2, ap-southeast-3, ap-southeast-4, eu-central-2, eu-south-1, eu-south-2, il-central-1, me-central-1, me-south-1, ap-southeast-5, ap-southeast-7, mx-central-1, ca-west-1, cn-north-1, cn-northwest-1'
1313

1414
env:
1515
COMMERCIAL_REGIONS: us-east-1, us-east-2, us-west-1, us-west-2, ap-south-1, ap-northeast-3, ap-northeast-2, ap-southeast-1, ap-southeast-2, ap-northeast-1, ca-central-1, eu-central-1, eu-west-1, eu-west-2, eu-west-3, eu-north-1, sa-east-1, ap-southeast-5, ap-southeast-7, mx-central-1, ca-west-1, cn-north-1, cn-northwest-1

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,32 @@ def is_installed(req: str) -> bool:
3535
def is_agent_observability_enabled() -> bool:
3636
"""Is the Agentic AI monitoring flag set to true?"""
3737
return os.environ.get(AGENT_OBSERVABILITY_ENABLED, "false").lower() == "true"
38+
39+
40+
def get_aws_region() -> str:
41+
"""Get AWS region using botocore session.
42+
43+
botocore automatically checks in the following priority order:
44+
1. AWS_REGION environment variable
45+
2. AWS_DEFAULT_REGION environment variable
46+
3. AWS CLI config file (~/.aws/config)
47+
4. EC2 instance metadata service
48+
49+
Returns:
50+
The AWS region if found, None otherwise.
51+
"""
52+
if is_installed("botocore"):
53+
try:
54+
from botocore import session # pylint: disable=import-outside-toplevel
55+
56+
botocore_session = session.Session()
57+
if botocore_session.region_name:
58+
return botocore_session.region_name
59+
except (ImportError, AttributeError):
60+
# botocore failed to determine region
61+
pass
62+
63+
_logger.warning(
64+
"AWS region not found. Please set AWS_REGION environment variable or configure AWS CLI with 'aws configure'."
65+
)
66+
return None

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

Lines changed: 143 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import logging
55
import os
66
import re
7-
from typing import ClassVar, Dict, List, Optional, Type, Union
7+
from logging import NOTSET, Logger, getLogger
8+
from typing import ClassVar, Dict, List, NamedTuple, Optional, Type, Union
89

910
from importlib_metadata import version
1011
from typing_extensions import override
1112

1213
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE
1314
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
14-
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled
15+
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled, is_installed
1516
from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler
1617
from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import (
1718
AttributePropagatingSpanProcessorBuilder,
@@ -104,11 +105,27 @@
104105

105106
AWS_OTLP_LOGS_GROUP_HEADER = "x-aws-log-group"
106107
AWS_OTLP_LOGS_STREAM_HEADER = "x-aws-log-stream"
108+
AWS_EMF_METRICS_NAMESPACE = "x-aws-metric-namespace"
107109

108110
# UDP package size is not larger than 64KB
109111
LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10
110112

111-
_logger: logging.Logger = logging.getLogger(__name__)
113+
OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER"
114+
OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER"
115+
OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER"
116+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
117+
OTEL_TRACES_SAMPLER = "OTEL_TRACES_SAMPLER"
118+
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"
119+
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED = "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED"
120+
121+
_logger: Logger = getLogger(__name__)
122+
123+
124+
class OtlpLogHeaderSetting(NamedTuple):
125+
log_group: Optional[str]
126+
log_stream: Optional[str]
127+
namespace: Optional[str]
128+
is_valid: bool
112129

113130

114131
class AwsOpenTelemetryConfigurator(_OTelSDKConfigurator):
@@ -138,7 +155,12 @@ def _configure(self, **kwargs):
138155
# The OpenTelemetry Authors code
139156
# Long term, we wish to contribute this to upstream to improve initialization customizability and reduce dependency on
140157
# internal logic.
141-
def _initialize_components(setup_logging_handler: Optional[bool] = None):
158+
def _initialize_components():
159+
# Remove 'awsemf' from OTEL_METRICS_EXPORTER if present to prevent validation errors
160+
# from _import_exporters in OTel dependencies which would try to load exporters
161+
# We will contribute emf exporter to upstream for supporting OTel metrics in SDK
162+
is_emf_enabled = _check_emf_exporter_enabled()
163+
142164
trace_exporters, metric_exporters, log_exporters = _import_exporters(
143165
_get_exporter_names("traces"),
144166
_get_exporter_names("metrics"),
@@ -174,13 +196,11 @@ def _initialize_components(setup_logging_handler: Optional[bool] = None):
174196
sampler=sampler,
175197
resource=resource,
176198
)
177-
_init_metrics(metric_exporters, resource)
178199

179-
if setup_logging_handler is None:
180-
setup_logging_handler = (
181-
os.getenv(_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false").strip().lower() == "true"
182-
)
183-
_init_logging(log_exporters, resource, setup_logging_handler)
200+
_init_metrics(metric_exporters, resource, is_emf_enabled)
201+
logging_enabled = os.getenv(_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false")
202+
if logging_enabled.strip().lower() == "true":
203+
_init_logging(log_exporters, resource)
184204

185205

186206
def _init_logging(
@@ -238,6 +258,7 @@ def _init_tracing(
238258
def _init_metrics(
239259
exporters_or_readers: Dict[str, Union[Type[MetricExporter], Type[MetricReader]]],
240260
resource: Resource = None,
261+
is_emf_enabled: bool = False,
241262
):
242263
metric_readers = []
243264
views = []
@@ -250,7 +271,7 @@ def _init_metrics(
250271
else:
251272
metric_readers.append(PeriodicExportingMetricReader(exporter_or_reader_class(**exporter_args)))
252273

253-
_customize_metric_exporters(metric_readers, views)
274+
_customize_metric_exporters(metric_readers, views, is_emf_enabled)
254275

255276
provider = MeterProvider(resource=resource, metric_readers=metric_readers, views=views)
256277
set_meter_provider(provider)
@@ -276,6 +297,17 @@ def _export_unsampled_span_for_lambda(trace_provider: TracerProvider, resource:
276297
)
277298

278299

300+
def _export_unsampled_span_for_agent_observability(trace_provider: TracerProvider, resource: Resource = None):
301+
if not is_agent_observability_enabled():
302+
return
303+
304+
traces_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
305+
306+
span_exporter = OTLPAwsSpanExporter(endpoint=traces_endpoint, logger_provider=get_logger_provider())
307+
308+
trace_provider.add_span_processor(BatchUnsampledSpanProcessor(span_exporter=span_exporter))
309+
310+
279311
def _is_defer_to_workers_enabled():
280312
return os.environ.get(OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED_CONFIG, "false").strip().lower() == "true"
281313

@@ -407,7 +439,7 @@ def _customize_logs_exporter(log_exporter: LogExporter) -> LogExporter:
407439
if _is_aws_otlp_endpoint(logs_endpoint, "logs"):
408440
_logger.info("Detected using AWS OTLP Logs Endpoint.")
409441

410-
if isinstance(log_exporter, OTLPLogExporter) and _validate_logs_headers():
442+
if isinstance(log_exporter, OTLPLogExporter) and _validate_and_fetch_logs_header().is_valid:
411443
# Setting default compression mode to Gzip as this is the behavior in upstream's
412444
# collector otlp http exporter:
413445
# https://github.com/open-telemetry/opentelemetry-collector/tree/main/exporter/otlphttpexporter
@@ -426,9 +458,14 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
426458
if _is_lambda_environment():
427459
provider.add_span_processor(AwsLambdaSpanProcessor())
428460

461+
# We always send 100% spans to Genesis platform for agent observability because
462+
# AI applications typically have low throughput traffic patterns and require
463+
# comprehensive monitoring to catch subtle failure modes like hallucinations
464+
# and quality degradation that sampling could miss.
429465
# Add session.id baggage attribute to span attributes to support AI Agent use cases
430466
# enabling session ID tracking in spans.
431467
if is_agent_observability_enabled():
468+
_export_unsampled_span_for_agent_observability(provider, resource)
432469

433470
def session_id_predicate(baggage_key: str) -> bool:
434471
return baggage_key == "session.id"
@@ -460,7 +497,9 @@ def session_id_predicate(baggage_key: str) -> bool:
460497
return
461498

462499

463-
def _customize_metric_exporters(metric_readers: List[MetricReader], views: List[View]) -> None:
500+
def _customize_metric_exporters(
501+
metric_readers: List[MetricReader], views: List[View], is_emf_enabled: bool = False
502+
) -> None:
464503
if _is_application_signals_runtime_enabled():
465504
_get_runtime_metric_views(views, 0 == len(metric_readers))
466505

@@ -472,6 +511,11 @@ def _customize_metric_exporters(metric_readers: List[MetricReader], views: List[
472511
)
473512
metric_readers.append(scope_based_periodic_exporting_metric_reader)
474513

514+
if is_emf_enabled:
515+
emf_exporter = create_emf_exporter()
516+
if emf_exporter:
517+
metric_readers.append(PeriodicExportingMetricReader(emf_exporter))
518+
475519

476520
def _get_runtime_metric_views(views: List[View], retain_runtime_only: bool) -> None:
477521
runtime_metrics_scope_name = SYSTEM_METRICS_INSTRUMENTATION_SCOPE_NAME
@@ -561,7 +605,7 @@ def _is_aws_otlp_endpoint(otlp_endpoint: Optional[str] = None, service: str = "x
561605
return bool(re.match(pattern, otlp_endpoint.lower()))
562606

563607

564-
def _validate_logs_headers() -> bool:
608+
def _validate_and_fetch_logs_header() -> OtlpLogHeaderSetting:
565609
"""Checks if x-aws-log-group and x-aws-log-stream are present in the headers in order to send logs to
566610
AWS OTLP Logs endpoint."""
567611

@@ -572,26 +616,36 @@ def _validate_logs_headers() -> bool:
572616
"Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS "
573617
"to include x-aws-log-group and x-aws-log-stream"
574618
)
575-
return False
619+
return OtlpLogHeaderSetting(None, None, None, False)
576620

621+
log_group = None
622+
log_stream = None
623+
namespace = None
577624
filtered_log_headers_count = 0
578625

579626
for pair in logs_headers.split(","):
580627
if "=" in pair:
581628
split = pair.split("=", 1)
582629
key = split[0]
583630
value = split[1]
584-
if key in (AWS_OTLP_LOGS_GROUP_HEADER, AWS_OTLP_LOGS_STREAM_HEADER) and value:
631+
if key == AWS_OTLP_LOGS_GROUP_HEADER and value:
632+
log_group = value
585633
filtered_log_headers_count += 1
634+
elif key == AWS_OTLP_LOGS_STREAM_HEADER and value:
635+
log_stream = value
636+
filtered_log_headers_count += 1
637+
elif key == AWS_EMF_METRICS_NAMESPACE and value:
638+
namespace = value
639+
640+
is_valid = filtered_log_headers_count == 2 and log_group is not None and log_stream is not None
586641

587-
if filtered_log_headers_count != 2:
642+
if not is_valid:
588643
_logger.warning(
589644
"Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS "
590645
"to have values for x-aws-log-group and x-aws-log-stream"
591646
)
592-
return False
593647

594-
return True
648+
return OtlpLogHeaderSetting(log_group, log_stream, namespace, is_valid)
595649

596650

597651
def _get_metric_export_interval():
@@ -662,3 +716,73 @@ def create_exporter(self):
662716
)
663717

664718
raise RuntimeError(f"Unsupported AWS Application Signals export protocol: {protocol} ")
719+
720+
721+
def _check_emf_exporter_enabled() -> bool:
722+
"""
723+
Checks if OTEL_METRICS_EXPORTER contains "awsemf", removes it if present,
724+
and updates the environment variable.
725+
726+
Remove 'awsemf' from OTEL_METRICS_EXPORTER if present to prevent validation errors
727+
from _import_exporters in OTel dependencies which would try to load exporters
728+
We will contribute emf exporter to upstream for supporting OTel metrics in SDK
729+
730+
Returns:
731+
bool: True if "awsemf" was found and removed, False otherwise.
732+
"""
733+
# Get the current exporter value
734+
exporter_value = os.environ.get("OTEL_METRICS_EXPORTER", "")
735+
736+
# Check if it's empty
737+
if not exporter_value:
738+
return False
739+
740+
# Split by comma and convert to list
741+
exporters = [exp.strip() for exp in exporter_value.split(",")]
742+
743+
# Check if awsemf is in the list
744+
if "awsemf" not in exporters:
745+
return False
746+
747+
# Remove awsemf from the list
748+
exporters.remove("awsemf")
749+
750+
# Join the remaining exporters and update the environment variable
751+
new_value = ",".join(exporters) if exporters else ""
752+
753+
# Set the new value (or unset if empty)
754+
if new_value:
755+
os.environ["OTEL_METRICS_EXPORTER"] = new_value
756+
elif "OTEL_METRICS_EXPORTER" in os.environ:
757+
del os.environ["OTEL_METRICS_EXPORTER"]
758+
759+
return True
760+
761+
762+
def create_emf_exporter():
763+
"""Create and configure the CloudWatch EMF exporter."""
764+
try:
765+
# Check if botocore is available before importing the EMF exporter
766+
if not is_installed("botocore"):
767+
_logger.warning("botocore is not installed. EMF exporter requires botocore")
768+
return None
769+
770+
# pylint: disable=import-outside-toplevel
771+
from amazon.opentelemetry.distro.exporter.aws.metrics.aws_cloudwatch_emf_exporter import (
772+
AwsCloudWatchEmfExporter,
773+
)
774+
775+
log_header_setting = _validate_and_fetch_logs_header()
776+
777+
if not log_header_setting.is_valid:
778+
return None
779+
780+
return AwsCloudWatchEmfExporter(
781+
namespace=log_header_setting.namespace,
782+
log_group_name=log_header_setting.log_group,
783+
log_stream_name=log_header_setting.log_stream,
784+
)
785+
# pylint: disable=broad-exception-caught
786+
except Exception as errors:
787+
_logger.error("Failed to create EMF exporter: %s", errors)
788+
return None

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44
import sys
55
from logging import Logger, getLogger
66

7+
from amazon.opentelemetry.distro._utils import get_aws_region, is_agent_observability_enabled
8+
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import (
9+
APPLICATION_SIGNALS_ENABLED_CONFIG,
10+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
11+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
12+
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
13+
OTEL_LOGS_EXPORTER,
14+
OTEL_METRICS_EXPORTER,
15+
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
16+
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED,
17+
OTEL_TRACES_EXPORTER,
18+
OTEL_TRACES_SAMPLER,
19+
)
720
from amazon.opentelemetry.distro.patches._instrumentation_patch import apply_instrumentation_patches
821
from opentelemetry.distro import OpenTelemetryDistro
922
from opentelemetry.environment_variables import OTEL_PROPAGATORS, OTEL_PYTHON_ID_GENERATOR
@@ -65,5 +78,44 @@ def _configure(self, **kwargs):
6578
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, "base2_exponential_bucket_histogram"
6679
)
6780

81+
if is_agent_observability_enabled():
82+
# "otlp" is already native OTel default, but we set them here to be explicit
83+
# about intended configuration for agent observability
84+
os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp")
85+
os.environ.setdefault(OTEL_LOGS_EXPORTER, "otlp")
86+
os.environ.setdefault(OTEL_METRICS_EXPORTER, "awsemf")
87+
88+
# Set GenAI capture content default
89+
os.environ.setdefault(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "true")
90+
91+
# Set OTLP endpoints with AWS region if not already set
92+
region = get_aws_region()
93+
if region:
94+
os.environ.setdefault(
95+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, f"https://xray.{region}.amazonaws.com/v1/traces"
96+
)
97+
os.environ.setdefault(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, f"https://logs.{region}.amazonaws.com/v1/logs")
98+
else:
99+
_logger.warning(
100+
"AWS region could not be determined. OTLP endpoints will not be automatically configured. "
101+
"Please set AWS_REGION environment variable or configure OTLP endpoints manually."
102+
)
103+
104+
# Set sampler default
105+
os.environ.setdefault(OTEL_TRACES_SAMPLER, "parentbased_always_on")
106+
107+
# Set disabled instrumentations default
108+
os.environ.setdefault(
109+
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
110+
"http,sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector,"
111+
"botocore,boto3,urllib3,requests,starlette",
112+
)
113+
114+
# Set logging auto instrumentation default
115+
os.environ.setdefault(OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "true")
116+
117+
# Disable AWS Application Signals by default
118+
os.environ.setdefault(APPLICATION_SIGNALS_ENABLED_CONFIG, "false")
119+
68120
if kwargs.get("apply_patches", True):
69121
apply_instrumentation_patches()

0 commit comments

Comments
 (0)