Skip to content

Commit b6fb638

Browse files
wangzleijj22ee
andauthored
Support export OTel metrics to be EMF in starndard output (#437)
*Issue #, if available:* Currently, OpenTelemetry (OTel) metrics users in AWS Lambda must export metrics via synchronous calls. This often results in increased function duration or lost metric data, due to how Lambda handles execution lifecycle — particularly during the [freeze phase](https://serverlessfolks.com/lambda-code-execution-freezethaw#heading-freeze). *Description of changes:* This PR introduces a new ConsoleEmfExporter, which exports OTel metrics to standard output using the CloudWatch Embedded Metric Format (EMF). In the Lambda environment, logs written to standard output are automatically forwarded to CloudWatch by Lambda's built-in logging agent. This makes ConsoleEmfExporter a simple and efficient way to export OTel metrics in Lambda, avoiding the overhead and reliability issues associated with synchronous metric export. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Jonathan Lee <[email protected]>
1 parent 9b4905f commit b6fb638

File tree

9 files changed

+1584
-687
lines changed

9 files changed

+1584
-687
lines changed

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

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,14 @@ class OtlpLogHeaderSetting(NamedTuple):
122122
log_group: Optional[str]
123123
log_stream: Optional[str]
124124
namespace: Optional[str]
125-
is_valid: bool
125+
126+
def is_valid(self) -> bool:
127+
"""Check if the log header setting is valid by ensuring both log_group and log_stream are present."""
128+
return self.log_group is not None and self.log_stream is not None
129+
130+
131+
# Singleton cache for OtlpLogHeaderSetting
132+
_otlp_log_header_setting_cache: Optional[OtlpLogHeaderSetting] = None
126133

127134

128135
class AwsOpenTelemetryConfigurator(_OTelSDKConfigurator):
@@ -440,7 +447,7 @@ def _customize_logs_exporter(log_exporter: LogExporter) -> LogExporter:
440447

441448
if isinstance(log_exporter, OTLPLogExporter):
442449

443-
if _validate_and_fetch_logs_header().is_valid:
450+
if _fetch_logs_header().is_valid():
444451
endpoint, region = _extract_endpoint_and_region_from_otlp_endpoint(logs_endpoint)
445452
# Setting default compression mode to Gzip as this is the behavior in upstream's
446453
# collector otlp http exporter:
@@ -627,18 +634,23 @@ def _extract_endpoint_and_region_from_otlp_endpoint(endpoint: str):
627634
return endpoint, region
628635

629636

630-
def _validate_and_fetch_logs_header() -> OtlpLogHeaderSetting:
631-
"""Checks if x-aws-log-group and x-aws-log-stream are present in the headers in order to send logs to
632-
AWS OTLP Logs endpoint."""
637+
def _fetch_logs_header() -> OtlpLogHeaderSetting:
638+
"""Returns the OTLP log header setting as a singleton instance."""
639+
global _otlp_log_header_setting_cache # pylint: disable=global-statement
640+
641+
if _otlp_log_header_setting_cache is not None:
642+
return _otlp_log_header_setting_cache
633643

634644
logs_headers = os.environ.get(OTEL_EXPORTER_OTLP_LOGS_HEADERS)
635645

636646
if not logs_headers:
637-
_logger.warning(
638-
"Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS "
639-
"to include x-aws-log-group and x-aws-log-stream"
640-
)
641-
return OtlpLogHeaderSetting(None, None, None, False)
647+
if not _is_lambda_environment():
648+
_logger.warning(
649+
"Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS "
650+
"to include x-aws-log-group and x-aws-log-stream"
651+
)
652+
_otlp_log_header_setting_cache = OtlpLogHeaderSetting(None, None, None)
653+
return _otlp_log_header_setting_cache
642654

643655
log_group = None
644656
log_stream = None
@@ -656,9 +668,14 @@ def _validate_and_fetch_logs_header() -> OtlpLogHeaderSetting:
656668
elif key == AWS_EMF_METRICS_NAMESPACE and value:
657669
namespace = value
658670

659-
is_valid = log_group is not None and log_stream is not None
671+
_otlp_log_header_setting_cache = OtlpLogHeaderSetting(log_group, log_stream, namespace)
672+
return _otlp_log_header_setting_cache
673+
660674

661-
return OtlpLogHeaderSetting(log_group, log_stream, namespace, is_valid)
675+
def _clear_logs_header_cache():
676+
"""Clear the singleton cache for OtlpLogHeaderSetting. Used primarily for testing."""
677+
global _otlp_log_header_setting_cache # pylint: disable=global-statement
678+
_otlp_log_header_setting_cache = None
662679

663680

664681
def _get_metric_export_interval():
@@ -773,8 +790,25 @@ def _check_emf_exporter_enabled() -> bool:
773790

774791

775792
def _create_emf_exporter():
776-
"""Create and configure the CloudWatch EMF exporter."""
793+
"""
794+
Create the appropriate EMF exporter based on the environment and configuration.
795+
796+
Returns:
797+
ConsoleEmfExporter for Lambda without log headers log group and stream
798+
AwsCloudWatchEmfExporter for other cases (when conditions are met)
799+
None if CloudWatch exporter cannot be created
800+
"""
777801
try:
802+
log_header_setting = _fetch_logs_header()
803+
804+
# Lambda without valid logs http headers - use Console EMF exporter
805+
if _is_lambda_environment() and not log_header_setting.is_valid():
806+
# pylint: disable=import-outside-toplevel
807+
from amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter import ConsoleEmfExporter
808+
809+
return ConsoleEmfExporter(namespace=log_header_setting.namespace)
810+
811+
# For non-Lambda environment or Lambda with valid headers - use CloudWatch EMF exporter
778812
session = get_aws_session()
779813
# Check if botocore is available before importing the EMF exporter
780814
if not session:
@@ -786,9 +820,7 @@ def _create_emf_exporter():
786820
AwsCloudWatchEmfExporter,
787821
)
788822

789-
log_header_setting = _validate_and_fetch_logs_header()
790-
791-
if not log_header_setting.is_valid:
823+
if not log_header_setting.is_valid():
792824
return None
793825

794826
return AwsCloudWatchEmfExporter(

0 commit comments

Comments
 (0)