11# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22# SPDX-License-Identifier: Apache-2.0
33# Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
4+
5+ # pylint: disable=too-many-lines
6+
47import logging
58import os
69import re
710from logging import Logger , getLogger
11+ from pathlib import Path
812from typing import ClassVar , Dict , List , NamedTuple , Optional , Type , Union
913
14+ import yaml
1015from importlib_metadata import version
1116from typing_extensions import override
1217
2530from amazon .opentelemetry .distro .aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
2631from amazon .opentelemetry .distro .exporter .console .logs .compact_console_log_exporter import CompactConsoleLogExporter
2732from amazon .opentelemetry .distro .otlp_udp_exporter import OTLPUdpSpanExporter
33+ from amazon .opentelemetry .distro .sampler ._aws_xray_adaptive_sampling_config import (
34+ _AnomalyCaptureLimit ,
35+ _AnomalyConditions ,
36+ _AWSXRayAdaptiveSamplingConfig ,
37+ _UsageType ,
38+ )
2839from amazon .opentelemetry .distro .sampler .aws_xray_remote_sampler import AwsXRayRemoteSampler
2940from amazon .opentelemetry .distro .scope_based_exporter import ScopeBasedPeriodicExportingMetricReader
3041from amazon .opentelemetry .distro .scope_based_filtering_view import ScopeBasedRetainingView
96107OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"
97108OTEL_EXPORTER_OTLP_LOGS_HEADERS = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"
98109OTEL_AWS_ENHANCED_CODE_ATTRIBUTES = "OTEL_AWS_EXPERIMENTAL_CODE_ATTRIBUTES"
110+ AWS_XRAY_ADAPTIVE_SAMPLING_CONFIG = "AWS_XRAY_ADAPTIVE_SAMPLING_CONFIG"
99111
100112XRAY_SERVICE = "xray"
101113LOGS_SERIVCE = "logs"
@@ -243,7 +255,8 @@ def _init_tracing(
243255 sampler : Sampler = None ,
244256 resource : Resource = None ,
245257):
246- sampler = _customize_sampler (sampler )
258+ original_sampler = sampler
259+ sampler = _customize_sampler (original_sampler )
247260
248261 trace_provider : TracerProvider = TracerProvider (
249262 id_generator = id_generator ,
@@ -254,12 +267,12 @@ def _init_tracing(
254267 for _ , exporter_class in exporters .items ():
255268 exporter_args : Dict [str , any ] = {}
256269 span_exporter : SpanExporter = exporter_class (** exporter_args )
257- span_exporter = _customize_span_exporter (span_exporter , resource )
270+ span_exporter = _customize_span_exporter (span_exporter , resource , original_sampler )
258271 trace_provider .add_span_processor (
259272 BatchSpanProcessor (span_exporter = span_exporter , max_export_batch_size = _span_export_batch_size ())
260273 )
261274
262- _customize_span_processors (trace_provider , resource )
275+ _customize_span_processors (trace_provider , resource , original_sampler )
263276
264277 set_tracer_provider (trace_provider )
265278
@@ -397,12 +410,25 @@ def _custom_import_sampler(sampler_name: str, resource: Resource) -> Sampler:
397410
398411
399412def _customize_sampler (sampler : Sampler ) -> Sampler :
413+ if isinstance (sampler , AwsXRayRemoteSampler ):
414+ config = os .environ .get (AWS_XRAY_ADAPTIVE_SAMPLING_CONFIG )
415+ parsed_config = None
416+
417+ try :
418+ parsed_config = _parse_config_string (config )
419+ # pylint: disable=broad-exception-caught
420+ except Exception as error :
421+ _logger .warning ("Failed to parse adaptive sampling configuration: %s" , str (error ))
422+
423+ if parsed_config is not None :
424+ sampler .set_adaptive_sampling_config (parsed_config )
425+
400426 if not _is_application_signals_enabled ():
401427 return sampler
402428 return AlwaysRecordSampler (sampler )
403429
404430
405- def _customize_span_exporter (span_exporter : SpanExporter , resource : Resource ) -> SpanExporter :
431+ def _customize_span_exporter (span_exporter : SpanExporter , resource : Resource , sampler : Sampler = None ) -> SpanExporter :
406432 traces_endpoint = os .environ .get (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT )
407433 if _is_lambda_environment ():
408434 # Override OTLP http default endpoint to UDP
@@ -425,7 +451,10 @@ def _customize_span_exporter(span_exporter: SpanExporter, resource: Resource) ->
425451 if not _is_application_signals_enabled ():
426452 return span_exporter
427453
428- return AwsMetricAttributesSpanExporterBuilder (span_exporter , resource ).build ()
454+ span_exporter = AwsMetricAttributesSpanExporterBuilder (span_exporter , resource ).build ()
455+ if sampler is not None and isinstance (sampler , AwsXRayRemoteSampler ):
456+ sampler .set_span_exporter (span_exporter )
457+ return span_exporter
429458
430459
431460def _customize_log_record_processor (logger_provider : LoggerProvider , log_exporter : Optional [LogExporter ]) -> None :
@@ -472,7 +501,7 @@ def _customize_logs_exporter(log_exporter: LogExporter) -> LogExporter:
472501 return log_exporter
473502
474503
475- def _customize_span_processors (provider : TracerProvider , resource : Resource ) -> None :
504+ def _customize_span_processors (provider : TracerProvider , resource : Resource , sampler : Sampler ) -> None :
476505
477506 if is_enhanced_code_attributes () is True :
478507 # pylint: disable=import-outside-toplevel
@@ -518,7 +547,7 @@ def session_id_predicate(baggage_key: str) -> bool:
518547 )
519548 meter_provider : MeterProvider = MeterProvider (resource = resource , metric_readers = [periodic_exporting_metric_reader ])
520549 # Construct and set application signals metrics processor
521- provider .add_span_processor (AwsSpanMetricsProcessorBuilder (meter_provider , resource ).build ())
550+ provider .add_span_processor (AwsSpanMetricsProcessorBuilder (meter_provider , resource ).set_sampler ( sampler ). build ())
522551
523552 return
524553
@@ -898,3 +927,84 @@ def _create_aws_otlp_exporter(endpoint: str, service: str, region: str):
898927 except Exception as errors :
899928 _logger .error ("Failed to create AWS OTLP exporter: %s" , errors )
900929 return None
930+
931+
932+ # pylint: disable=too-many-return-statements,too-many-branches
933+ def _parse_config_string (config : str ) -> Optional [_AWSXRayAdaptiveSamplingConfig ]:
934+ if config is None :
935+ return None
936+
937+ # Check if the config is a file path and the file exists
938+ path = Path (config )
939+ if path .exists ():
940+ try :
941+ config = path .read_text (encoding = "utf-8" )
942+ except IOError as err :
943+ _logger .warning ("Failed to read adaptive sampling configuration file: %s" , err )
944+ return None
945+ elif config .endswith (".yml" ) or config .endswith (".yaml" ):
946+ _logger .warning ("Adaptive sampling configuration file must be a YAML file" )
947+ return None
948+ else :
949+ _logger .debug ("Adaptive sampling configuration is not a file path, assuming it's a YAML string" )
950+
951+ # Parse YAML config
952+ config_map = None
953+ try :
954+ config_map = yaml .safe_load (config )
955+ # pylint: disable=broad-exception-caught
956+ except Exception as exception :
957+ _logger .warning ("Adaptive sampling configuration must be a valid YAML mapping: %s" , exception )
958+ if not isinstance (config_map , dict ):
959+ _logger .warning ("Adaptive sampling configuration must be a valid YAML mapping" )
960+ return None
961+
962+ # Ensure only relevant data is in the YAML configuration
963+ for key in config_map :
964+ if key not in ["version" , "anomalyConditions" , "anomalyCaptureLimit" ]:
965+ _logger .warning ("Invalid key in adaptive sampling configuration: %s" , key )
966+ return None
967+
968+ version_obj = config_map .get ("version" )
969+ if version_obj is None :
970+ _logger .warning ("Missing required 'version' field in adaptive sampling configuration" )
971+ return None
972+
973+ try :
974+ config_version = float (version_obj )
975+ if config_version < 1.0 or config_version >= 2.0 :
976+ _logger .warning (
977+ "Incompatible adaptive sampling config version: %s. "
978+ "This version of the AWS X-Ray remote sampler only supports version 1.X." ,
979+ config_version ,
980+ )
981+ return None
982+
983+ # Parse anomaly conditions
984+ anomaly_conditions = None
985+ if "anomalyConditions" in config_map :
986+ anomaly_conditions = [
987+ _AnomalyConditions (
988+ error_code_regex = cond .get ("errorCodeRegex" ),
989+ operations = cond .get ("operations" ),
990+ high_latency_ms = cond .get ("highLatencyMs" ),
991+ usage = _UsageType (cond ["usage" ]) if "usage" in cond else None ,
992+ )
993+ for cond in config_map ["anomalyConditions" ]
994+ ]
995+
996+ # Parse anomaly capture limit
997+ anomaly_capture_limit = None
998+ if "anomalyCaptureLimit" in config_map :
999+ anomaly_capture_limit_data = config_map ["anomalyCaptureLimit" ]
1000+ anomaly_capture_limit = _AnomalyCaptureLimit (
1001+ anomaly_traces_per_second = anomaly_capture_limit_data ["anomalyTracesPerSecond" ]
1002+ )
1003+
1004+ return _AWSXRayAdaptiveSamplingConfig (
1005+ version = config_version , anomaly_conditions = anomaly_conditions , anomaly_capture_limit = anomaly_capture_limit
1006+ )
1007+ except ValueError as err :
1008+ _logger .warning ("Failed to load AWS X-Ray adaptive sampling configuration: %s" , err )
1009+
1010+ return None
0 commit comments