From 97e6eed939c9d8efafc9e578d97fc5cc3772c1c7 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 2 Mar 2026 12:15:53 -0800 Subject: [PATCH 01/21] Initial set of changes --- .../export/trace/_rate_limited_sampling.py | 21 ++- .../exporter/export/trace/_sampling.py | 8 + .../azure/monitor/opentelemetry/_configure.py | 28 ++-- .../opentelemetry/_utils/configurations.py | 145 +++++------------- 4 files changed, 76 insertions(+), 126 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py index 3061fcafaa03..efcc72d03e18 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import math +import os import threading import time from typing import Optional, Sequence @@ -16,6 +17,10 @@ from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes +from opentelemetry.sdk.environment_variables import ( + OTEL_TRACES_SAMPLER_ARG, +) + from azure.monitor.opentelemetry.exporter._constants import _SAMPLE_RATE_KEY from azure.monitor.opentelemetry.exporter.export.trace._utils import ( @@ -33,13 +38,13 @@ def __init__(self, effective_window_count: float, effective_window_nanoseconds: class RateLimitedSamplingPercentage: - def __init__(self, target_spans_per_second_limit: float, round_to_nearest: bool = True): - if target_spans_per_second_limit < 0.0: + def __init__(self, traces_per_second: float, round_to_nearest: bool = True): + if traces_per_second < 0.0: raise ValueError("Limit for sampled spans per second must be nonnegative!") # Hardcoded adaptation time of 0.1 seconds for adjusting to sudden changes in telemetry volumes adaptation_time_seconds = 0.1 self._inverse_adaptation_time_nanoseconds = 1e-9 / adaptation_time_seconds - self._target_spans_per_nanosecond_limit = 1e-9 * target_spans_per_second_limit + self._target_spans_per_nanosecond_limit = 1e-9 * traces_per_second initial_nano_time = int(time.time_ns()) self._state = _State(0.0, 0.0, initial_nano_time) self._lock = threading.Lock() @@ -82,9 +87,13 @@ def get(self) -> float: class RateLimitedSampler(Sampler): - def __init__(self, target_spans_per_second_limit: float): - self._sampling_percentage_generator = RateLimitedSamplingPercentage(target_spans_per_second_limit) - self._description = f"RateLimitedSampler{{{target_spans_per_second_limit}}}" + def __init__(self, traces_per_second: float = 5.0): + env_sampling_traces_per_second = os.environ.get(OTEL_TRACES_SAMPLER_ARG) + if env_sampling_traces_per_second is not None: + traces_per_second = float(env_sampling_traces_per_second) + + self._sampling_percentage_generator = RateLimitedSamplingPercentage(traces_per_second) + self._description = f"RateLimitedSampler{{{traces_per_second}}}" def should_sample( self, diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py index 9f676bf50fa9..29b89a683085 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import os from typing import Optional, Sequence from opentelemetry.context import Context @@ -13,6 +14,10 @@ from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes +from opentelemetry.sdk.environment_variables import ( + OTEL_TRACES_SAMPLER_ARG, +) + from azure.monitor.opentelemetry.exporter.export.trace._utils import _get_DJB2_sample_score from azure.monitor.opentelemetry.exporter._constants import _SAMPLE_RATE_KEY @@ -28,6 +33,9 @@ class ApplicationInsightsSampler(Sampler): # sampling_ratio must take a value in the range [0,1] def __init__(self, sampling_ratio: float = 1.0): + env_sampling_ratio = os.environ.get(OTEL_TRACES_SAMPLER_ARG) + if env_sampling_ratio is not None: + sampling_ratio = float(env_sampling_ratio) if not 0.0 <= sampling_ratio <= 1.0: raise ValueError("sampling_ratio must be in the range [0,1]") self._ratio = sampling_ratio diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py index 415b522509b5..2c973ee0e41d 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py @@ -43,6 +43,9 @@ ENABLE_TRACE_BASED_SAMPLING_ARG, SAMPLING_ARG, SAMPLER_TYPE, + RATE_LIMITED_SAMPLER, + FIXED_PERCENTAGE_SAMPLER, + SAMPLER_TYPE, ) from azure.monitor.opentelemetry._types import ConfigurationValue from azure.monitor.opentelemetry.exporter._quickpulse import ( # pylint: disable=import-error,no-name-in-module @@ -166,21 +169,20 @@ def configure_azure_monitor(**kwargs) -> None: # pylint: disable=C4758 def _setup_tracing(configurations: Dict[str, ConfigurationValue]): resource: Resource = configurations[RESOURCE_ARG] # type: ignore enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG] - if SAMPLING_ARG in configurations: - sampler_arg = configurations[SAMPLING_ARG] + if SAMPLER_TYPE in configurations: sampler_type = configurations[SAMPLER_TYPE] - sampler = _get_sampler_from_name(sampler_type, sampler_arg) - tracer_provider = TracerProvider(sampler=sampler, resource=resource) - elif SAMPLING_RATIO_ARG in configurations: - sampling_ratio = configurations[SAMPLING_RATIO_ARG] - tracer_provider = TracerProvider( - sampler=ApplicationInsightsSampler(sampling_ratio=cast(float, sampling_ratio)), resource=resource - ) - else: - traces_per_second = configurations[SAMPLING_TRACES_PER_SECOND_ARG] - tracer_provider = TracerProvider( - sampler=RateLimitedSampler(target_spans_per_second_limit=cast(float, traces_per_second)), resource=resource + if sampler_type == RATE_LIMITED_SAMPLER: + tracer_provider = TracerProvider( + sampler=RateLimitedSampler(), resource=resource ) + elif sampler_type == FIXED_PERCENTAGE_SAMPLER: + tracer_provider = TracerProvider( + sampler=ApplicationInsightsSampler(), resource=resource + ) + else: + tracer_provider = TracerProvider( + sampler = _get_sampler_from_name(SAMPLER_TYPE), resource=resource) + for span_processor in configurations[SPAN_PROCESSORS_ARG]: # type: ignore tracer_provider.add_span_processor(span_processor) # type: ignore diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py index f323ddffcb01..d988ac7f869c 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py @@ -183,114 +183,14 @@ def _default_resource(configurations): # pylint: disable=too-many-statements,too-many-branches def _default_sampling_ratio(configurations): - default_value = 1.0 default_value_for_rate_limited_sampler = 5.0 - sampler_type = environ.get(OTEL_TRACES_SAMPLER) - sampler_arg = environ.get(OTEL_TRACES_SAMPLER_ARG) - - # Handle rate-limited sampler - if sampler_type == RATE_LIMITED_SAMPLER: - try: - sampler_value = float(sampler_arg) if sampler_arg is not None else default_value_for_rate_limited_sampler - if sampler_value < 0.0: - _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a non-negative number.") - sampler_value = default_value_for_rate_limited_sampler - else: - _logger.info("Using rate limited sampler: %s traces per second", sampler_value) - configurations[SAMPLING_TRACES_PER_SECOND_ARG] = sampler_value - except ValueError as e: - _logger.error( # pylint: disable=C - _INVALID_TRACES_PER_SECOND_MESSAGE, - OTEL_TRACES_SAMPLER_ARG, - default_value_for_rate_limited_sampler, - e, - ) - configurations[SAMPLING_TRACES_PER_SECOND_ARG] = default_value_for_rate_limited_sampler + SAMPLER_TYPE = environ.get(OTEL_TRACES_SAMPLER) - # Handle fixed percentage sampler - elif sampler_type in (FIXED_PERCENTAGE_SAMPLER, "microsoft.fixed.percentage"): # to support older string - try: - sampler_value = float(sampler_arg) if sampler_arg is not None else default_value - if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1.") - sampler_value = default_value - else: - _logger.info("Using sampling ratio: %s", sampler_value) - configurations[SAMPLING_RATIO_ARG] = sampler_value - except ValueError as e: - _logger.error( # pylint: disable=C - _INVALID_FLOAT_MESSAGE, - OTEL_TRACES_SAMPLER_ARG, - default_value, - e, - ) - configurations[SAMPLING_RATIO_ARG] = default_value - - # Handle always_on sampler - elif sampler_type == ALWAYS_ON_SAMPLER: - configurations[SAMPLING_ARG] = 1.0 - configurations[SAMPLER_TYPE] = ALWAYS_ON_SAMPLER - - # Handle always_off sampler - elif sampler_type == ALWAYS_OFF_SAMPLER: - configurations[SAMPLING_ARG] = 0.0 - configurations[SAMPLER_TYPE] = ALWAYS_OFF_SAMPLER - - # Handle trace_id_ratio sampler - elif sampler_type == TRACE_ID_RATIO_SAMPLER: - try: - sampler_value = float(sampler_arg) if sampler_arg is not None else default_value - if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1.") - sampler_value = default_value - else: - _logger.info("Using sampling value: %s", sampler_value) - configurations[SAMPLING_ARG] = sampler_value - except ValueError as e: - _logger.error( # pylint: disable=C - _INVALID_FLOAT_MESSAGE, - OTEL_TRACES_SAMPLER_ARG, - default_value, - e, - ) - configurations[SAMPLING_ARG] = default_value - configurations[SAMPLER_TYPE] = TRACE_ID_RATIO_SAMPLER - - # Handle parentbased_always_on sampler - elif sampler_type == PARENT_BASED_ALWAYS_ON_SAMPLER: - configurations[SAMPLING_ARG] = 1.0 - configurations[SAMPLER_TYPE] = PARENT_BASED_ALWAYS_ON_SAMPLER - - # Handle parentbased_always_off sampler - elif sampler_type == PARENT_BASED_ALWAYS_OFF_SAMPLER: - configurations[SAMPLING_ARG] = 0.0 - configurations[SAMPLER_TYPE] = PARENT_BASED_ALWAYS_OFF_SAMPLER - - # Handle parentbased_trace_id_ratio sampler - elif sampler_type == PARENT_BASED_TRACE_ID_RATIO_SAMPLER: - try: - sampler_value = float(sampler_arg) if sampler_arg is not None else default_value - if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1.") - sampler_value = default_value - else: - _logger.info("Using sampling value: %s", sampler_value) - configurations[SAMPLING_ARG] = sampler_value - except ValueError as e: - _logger.error( # pylint: disable=C - _INVALID_FLOAT_MESSAGE, - OTEL_TRACES_SAMPLER_ARG, - default_value, - e, - ) - configurations[SAMPLING_ARG] = default_value - configurations[SAMPLER_TYPE] = PARENT_BASED_TRACE_ID_RATIO_SAMPLER - - # Handle all other cases (no sampler type specified or unsupported sampler type) - else: + if SAMPLER_TYPE is None or SAMPLER_TYPE not in SUPPORTED_OTEL_SAMPLERS: + # Handle all other cases (no sampler type specified or unsupported sampler type) if configurations.get(SAMPLING_TRACES_PER_SECOND_ARG) is None: configurations[SAMPLING_TRACES_PER_SECOND_ARG] = default_value_for_rate_limited_sampler - if sampler_type is not None: + if SAMPLER_TYPE is not None: _logger.error( # pylint: disable=C "Invalid argument for the sampler to be used for tracing. " "Supported values are %s. Defaulting to %s: %s", @@ -378,17 +278,48 @@ def _default_enable_trace_based_sampling(configurations): configurations.setdefault(ENABLE_TRACE_BASED_SAMPLING_ARG, False) -def _get_sampler_from_name(sampler_type, sampler_arg): +def _get_sampler_from_name(sampler_type): + default_value = 1.0 + sampler_arg = environ.get(OTEL_TRACES_SAMPLER_ARG) + sampler_value = float(sampler_arg) if sampler_arg is not None else default_value if sampler_type == ALWAYS_ON_SAMPLER: return ALWAYS_ON if sampler_type == ALWAYS_OFF_SAMPLER: return ALWAYS_OFF if sampler_type == TRACE_ID_RATIO_SAMPLER: - ratio = float(sampler_arg) if sampler_arg is not None else 1.0 + try: + sampler_value = float(sampler_arg) if sampler_arg is not None else default_value + if sampler_value < 0.0 or sampler_value > 1.0: + _logger.error("Invalid argument value for TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1.") + sampler_value = default_value + else: + _logger.info("Using sampling value: %s", sampler_value) + except ValueError as e: + _logger.error( # pylint: disable=C + _INVALID_FLOAT_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + default_value, + e, + ) + ratio = float(sampler_value) if sampler_value is not None else default_value return TraceIdRatioBased(ratio) if sampler_type == PARENT_BASED_ALWAYS_OFF_SAMPLER: return ParentBased(ALWAYS_OFF) if sampler_type == PARENT_BASED_TRACE_ID_RATIO_SAMPLER: - ratio = float(sampler_arg) if sampler_arg is not None else 1.0 + try: + sampler_value = float(sampler_arg) if sampler_arg is not None else default_value + if sampler_value < 0.0 or sampler_value > 1.0: + _logger.error("Invalid argument value for PARENT_BASED_TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1.") + sampler_value = default_value + else: + _logger.info("Using sampling value: %s", sampler_value) + except ValueError as e: + _logger.error( # pylint: disable=C + _INVALID_FLOAT_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + default_value, + e, + ) + ratio = float(sampler_value) if sampler_value is not None else default_value return ParentBased(TraceIdRatioBased(ratio)) return ParentBased(ALWAYS_ON) From 8bb7adce382faff17f17070d7eb8dbe857e4948b Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 4 Mar 2026 10:03:18 -0800 Subject: [PATCH 02/21] Fixing logic for the samplers --- .../export/trace/_rate_limited_sampling.py | 22 +- .../exporter/export/trace/_sampling.py | 35 ++- .../tests/trace/test_sampling.py | 41 +++- .../azure/monitor/opentelemetry/_configure.py | 24 +- .../azure/monitor/opentelemetry/_constants.py | 1 + .../opentelemetry/_utils/configurations.py | 48 ++-- .../tests/test_configure.py | 12 +- .../tests/utils/test_configurations.py | 206 ++---------------- 8 files changed, 156 insertions(+), 233 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py index efcc72d03e18..1b2c5ba9f8af 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py @@ -6,6 +6,7 @@ import threading import time from typing import Optional, Sequence +from logging import getLogger from opentelemetry.context import Context from opentelemetry.trace import Link, SpanKind, format_trace_id from opentelemetry.sdk.trace.sampling import ( @@ -29,6 +30,9 @@ parent_context_sampling, ) +_INVALID_TRACES_PER_SECOND_MESSAGE = "Value of %s must be a positive number for traces per second. Defaulting to %s: %s" + +_logger = getLogger(__name__) class _State: def __init__(self, effective_window_count: float, effective_window_nanoseconds: float, last_nano_time: int): @@ -88,9 +92,21 @@ def get(self) -> float: class RateLimitedSampler(Sampler): def __init__(self, traces_per_second: float = 5.0): - env_sampling_traces_per_second = os.environ.get(OTEL_TRACES_SAMPLER_ARG) - if env_sampling_traces_per_second is not None: - traces_per_second = float(env_sampling_traces_per_second) + sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) + try: + sampler_value = float(sampling_arg) if sampling_arg is not None else traces_per_second + if sampler_value < 0.0: + _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a non-negative number.") + else: + _logger.info("Using rate limited sampler: %s traces per second", sampler_value) + traces_per_second = sampler_value + except ValueError as e: + _logger.error( # pylint: disable=C + _INVALID_TRACES_PER_SECOND_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + traces_per_second, + e, + ) self._sampling_percentage_generator = RateLimitedSamplingPercentage(traces_per_second) self._description = f"RateLimitedSampler{{{traces_per_second}}}" diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py index 29b89a683085..ba14568639e6 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import os from typing import Optional, Sequence +from logging import getLogger from opentelemetry.context import Context from opentelemetry.trace import Link, SpanKind, format_trace_id @@ -22,6 +23,9 @@ from azure.monitor.opentelemetry.exporter._constants import _SAMPLE_RATE_KEY +_INVALID_FLOAT_MESSAGE = "Value of %s must be a float. Defaulting to %s: %s" + +_logger = getLogger(__name__) # Sampler is responsible for the following: # Implements same trace id hashing algorithm so that traces are sampled the same across multiple nodes (via AI SDKS) @@ -32,12 +36,31 @@ class ApplicationInsightsSampler(Sampler): """Sampler that implements the same probability sampling algorithm as the ApplicationInsights SDKs.""" # sampling_ratio must take a value in the range [0,1] - def __init__(self, sampling_ratio: float = 1.0): - env_sampling_ratio = os.environ.get(OTEL_TRACES_SAMPLER_ARG) - if env_sampling_ratio is not None: - sampling_ratio = float(env_sampling_ratio) - if not 0.0 <= sampling_ratio <= 1.0: - raise ValueError("sampling_ratio must be in the range [0,1]") + def __init__(self, sampling_ratio: Optional[float] = None): + default_value = 1.0 + if sampling_ratio is not None: + if sampling_ratio < 0.0 or sampling_ratio > 1.0: + _logger.error("Sampling ratio must be in the range [0.0, 1.0]. Defaulting to %s.", default_value) + sampling_ratio = default_value + else: + sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) + try: + sampler_value = float(sampling_arg) if sampling_arg is not None else default_value + if sampler_value < 0.0 or sampler_value > 1.0: + _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1. Defaulting to %s.", default_value) + sampling_ratio = default_value + else: + _logger.info("Using sampling ratio: %s", sampler_value) + sampling_ratio = sampler_value + except ValueError as e: + _logger.error( # pylint: disable=C + _INVALID_FLOAT_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + default_value, + e, + ) + sampling_ratio = default_value + self._ratio = sampling_ratio self._sample_rate = sampling_ratio * 100 diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py index 5d9d8b580ee6..1ae0cc028e62 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py @@ -22,8 +22,45 @@ def test_constructor_ratio(self): self.assertEqual(sampler._sample_rate, 75) def test_invalid_ratio(self): - self.assertRaises(ValueError, lambda: ApplicationInsightsSampler(1.01)) - self.assertRaises(ValueError, lambda: ApplicationInsightsSampler(-0.01)) + # Invalid explicit ratio logs an error and defaults to 1.0 instead of raising + sampler = ApplicationInsightsSampler(1.01) + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) + sampler = ApplicationInsightsSampler(-0.01) + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) + + def test_user_passed_value_through_distro(self): + sampler = ApplicationInsightsSampler(sampling_ratio=0.5) + self.assertEqual(sampler._ratio, 0.5) + self.assertEqual(sampler._sample_rate, 50.0) + + def test_constructor_sampler_arg(self): + with mock.patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "0.5"}): + sampler = ApplicationInsightsSampler() + self.assertEqual(sampler._ratio, 0.5) + self.assertEqual(sampler._sample_rate, 50.0) + + def test_constructor_explicit_ratio_ignores_sampler_arg(self): + # Explicit ratio passed from distro takes priority over env var + with mock.patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "0.3"}): + sampler = ApplicationInsightsSampler(0.75) + self.assertEqual(sampler._ratio, 0.75) + self.assertEqual(sampler._sample_rate, 75.0) + + def test_constructor_sampler_arg_invalid_range(self): + # Invalid env var with no explicit ratio falls back to 1.0 + with mock.patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "1.5"}): + sampler = ApplicationInsightsSampler() + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) + + def test_constructor_sampler_arg_invalid_float(self): + # Non-numeric env var with no explicit ratio falls back to 1.0 + with mock.patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "not_a_number"}): + sampler = ApplicationInsightsSampler() + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) @mock.patch("azure.monitor.opentelemetry.exporter.export.trace._sampling._get_DJB2_sample_score") def test_should_sample(self, score_mock): diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py index 2c973ee0e41d..e93b07acbf52 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py @@ -169,20 +169,16 @@ def configure_azure_monitor(**kwargs) -> None: # pylint: disable=C4758 def _setup_tracing(configurations: Dict[str, ConfigurationValue]): resource: Resource = configurations[RESOURCE_ARG] # type: ignore enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG] - if SAMPLER_TYPE in configurations: - sampler_type = configurations[SAMPLER_TYPE] - if sampler_type == RATE_LIMITED_SAMPLER: - tracer_provider = TracerProvider( - sampler=RateLimitedSampler(), resource=resource - ) - elif sampler_type == FIXED_PERCENTAGE_SAMPLER: - tracer_provider = TracerProvider( - sampler=ApplicationInsightsSampler(), resource=resource - ) - else: - tracer_provider = TracerProvider( - sampler = _get_sampler_from_name(SAMPLER_TYPE), resource=resource) - + sampler_type = configurations.get(SAMPLER_TYPE, RATE_LIMITED_SAMPLER) + if sampler_type == RATE_LIMITED_SAMPLER: + traces_per_second = configurations.get(SAMPLING_TRACES_PER_SECOND_ARG) + sampler = RateLimitedSampler() if traces_per_second is None else RateLimitedSampler(traces_per_second=traces_per_second) + elif sampler_type == FIXED_PERCENTAGE_SAMPLER: + sampling_ratio = configurations.get(SAMPLING_RATIO_ARG) + sampler = ApplicationInsightsSampler() if sampling_ratio is None else ApplicationInsightsSampler(sampling_ratio=sampling_ratio) + else: + sampler = _get_sampler_from_name(sampler_type) + tracer_provider = TracerProvider(sampler=sampler, resource=resource) for span_processor in configurations[SPAN_PROCESSORS_ARG]: # type: ignore tracer_provider.add_span_processor(span_processor) # type: ignore diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py index 2823f173894a..c274ff3e5762 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py @@ -41,6 +41,7 @@ PARENT_BASED_ALWAYS_OFF_SAMPLER = "parentbased_always_off" PARENT_BASED_TRACE_ID_RATIO_SAMPLER = "parentbased_trace_id_ratio" SUPPORTED_OTEL_SAMPLERS = ( + "microsoft.fixed.percentage", # Ensures backward compatibility with old value RATE_LIMITED_SAMPLER, FIXED_PERCENTAGE_SAMPLER, ALWAYS_ON_SAMPLER, diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py index d988ac7f869c..b447634ea7c1 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py @@ -100,7 +100,7 @@ def _get_configurations(**kwargs) -> Dict[str, ConfigurationValue]: _default_logger_name(configurations) _default_logging_formatter(configurations) _default_resource(configurations) - _default_sampling_ratio(configurations) + _default_sampler(configurations) _default_instrumentation_options(configurations) _default_span_processors(configurations) _default_log_record_processors(configurations) @@ -181,23 +181,29 @@ def _default_resource(configurations): configurations[RESOURCE_ARG] = Resource.create(configurations[RESOURCE_ARG].attributes) -# pylint: disable=too-many-statements,too-many-branches -def _default_sampling_ratio(configurations): - default_value_for_rate_limited_sampler = 5.0 - SAMPLER_TYPE = environ.get(OTEL_TRACES_SAMPLER) - - if SAMPLER_TYPE is None or SAMPLER_TYPE not in SUPPORTED_OTEL_SAMPLERS: - # Handle all other cases (no sampler type specified or unsupported sampler type) - if configurations.get(SAMPLING_TRACES_PER_SECOND_ARG) is None: - configurations[SAMPLING_TRACES_PER_SECOND_ARG] = default_value_for_rate_limited_sampler - if SAMPLER_TYPE is not None: - _logger.error( # pylint: disable=C - "Invalid argument for the sampler to be used for tracing. " - "Supported values are %s. Defaulting to %s: %s", - SUPPORTED_OTEL_SAMPLERS, - RATE_LIMITED_SAMPLER, - configurations[SAMPLING_TRACES_PER_SECOND_ARG], - ) +def _default_sampler(configurations): + sampler = (environ.get(OTEL_TRACES_SAMPLER) or "").strip() or None + if sampler in SUPPORTED_OTEL_SAMPLERS: + configurations[SAMPLER_TYPE] = sampler + elif sampler is not None: + _logger.error( # pylint: disable=C + "Invalid value: %s, provided for the sampler to be used for tracing. " + "Supported values are %s. Defaulting to %s: %s", + sampler, SUPPORTED_OTEL_SAMPLERS, RATE_LIMITED_SAMPLER, "5.0 traces per second", + ) + configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER + elif configurations.get("sampling_ratio") is not None: + configurations[SAMPLER_TYPE] = FIXED_PERCENTAGE_SAMPLER + configurations[SAMPLING_RATIO_ARG] = configurations["sampling_ratio"] + elif configurations.get("traces_per_second") is not None: + configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER + configurations[SAMPLING_TRACES_PER_SECOND_ARG] = configurations["traces_per_second"] + else: + _logger.info( # pylint: disable=C + "No sampler specified. Defaulting to %s: %s", RATE_LIMITED_SAMPLER, "5.0 traces per second", + ) + configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER + print(f"Sampler type set to: {configurations[SAMPLER_TYPE]}") def _default_instrumentation_options(configurations): @@ -288,9 +294,8 @@ def _get_sampler_from_name(sampler_type): return ALWAYS_OFF if sampler_type == TRACE_ID_RATIO_SAMPLER: try: - sampler_value = float(sampler_arg) if sampler_arg is not None else default_value if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid argument value for TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1.") + _logger.error("Invalid sampler argument for TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1.") sampler_value = default_value else: _logger.info("Using sampling value: %s", sampler_value) @@ -307,9 +312,8 @@ def _get_sampler_from_name(sampler_type): return ParentBased(ALWAYS_OFF) if sampler_type == PARENT_BASED_TRACE_ID_RATIO_SAMPLER: try: - sampler_value = float(sampler_arg) if sampler_arg is not None else default_value if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid argument value for PARENT_BASED_TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1.") + _logger.error("Invalid sampler argument for PARENT_BASED_TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1.") sampler_value = default_value else: _logger.info("Using sampling value: %s", sampler_value) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/test_configure.py b/sdk/monitor/azure-monitor-opentelemetry/tests/test_configure.py index 370394a4747d..12d9f7d5af1e 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/tests/test_configure.py +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/test_configure.py @@ -25,6 +25,13 @@ _setup_tracing, configure_azure_monitor, ) + +from azure.monitor.opentelemetry._constants import ( + SAMPLER_TYPE, + FIXED_PERCENTAGE_SAMPLER, + RATE_LIMITED_SAMPLER, +) + from azure.monitor.opentelemetry._diagnostics.diagnostic_logging import _DISTRO_DETECTS_ATTACH @@ -378,6 +385,7 @@ def test_setup_tracing( "azure.core.tracing.ext.opentelemetry_span": Mock(OpenTelemetrySpan=opentelemetry_span_mock), }, ): + configurations[SAMPLER_TYPE] = FIXED_PERCENTAGE_SAMPLER _setup_tracing(configurations) sampler_mock.assert_called_once_with(sampling_ratio=0.5) tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) @@ -451,8 +459,9 @@ def test_setup_tracing_rate_limited_sampler( "azure.core.tracing.ext.opentelemetry_span": Mock(OpenTelemetrySpan=opentelemetry_span_mock), }, ): + configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER _setup_tracing(configurations) - sampler_mock.assert_called_once_with(target_spans_per_second_limit=2.0) + sampler_mock.assert_called_once_with(traces_per_second=2.0) tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) set_tracer_provider_mock.assert_called_once_with(tp_init_mock) trace_exporter_mock.assert_called_once_with(**configurations) @@ -524,6 +533,7 @@ def test_setup_tracing_perf_counters_disabled( "azure.core.tracing.ext.opentelemetry_span": Mock(OpenTelemetrySpan=opentelemetry_span_mock), }, ): + configurations[SAMPLER_TYPE] = FIXED_PERCENTAGE_SAMPLER _setup_tracing(configurations) sampler_mock.assert_called_once_with(sampling_ratio=0.5) tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py index 81caeeda6faf..042ec1f7f166 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py @@ -19,7 +19,6 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, - OTEL_TRACES_SAMPLER_ARG, OTEL_TRACES_SAMPLER, ) from opentelemetry.instrumentation.environment_variables import ( @@ -48,6 +47,7 @@ PARENT_BASED_ALWAYS_ON_SAMPLER, PARENT_BASED_TRACE_ID_RATIO_SAMPLER, METRIC_READERS_ARG, + SAMPLER_TYPE, ) from azure.monitor.opentelemetry._version import VERSION @@ -98,6 +98,7 @@ def test_get_configurations(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_MERGED_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "azure_app_service,azure_vm") resource_create_mock.assert_called_once_with(TEST_CUSTOM_RESOURCE.attributes) + self.assertEqual(configurations["sampler_type"], "microsoft.fixed_percentage") self.assertEqual(configurations["sampling_ratio"], 0.5) self.assertEqual(configurations["credential"], "test_credential") self.assertEqual( @@ -150,7 +151,7 @@ def test_get_configurations_defaults(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "azure_app_service,azure_vm") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["traces_per_second"], 5.0) + self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") self.assertTrue("credential" not in configurations) self.assertTrue("storage_directory" not in configurations) self.assertEqual(configurations["enable_live_metrics"], True) @@ -166,7 +167,6 @@ def test_get_configurations_defaults(self, resource_create_mock): "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER_ARG: "0.5", OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", @@ -198,13 +198,12 @@ def test_get_configurations_env_vars(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["traces_per_second"], 5.0) + self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") @patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: FIXED_PERCENTAGE_SAMPLER, - OTEL_TRACES_SAMPLER_ARG: "Half", OTEL_TRACES_EXPORTER: "False", OTEL_LOGS_EXPORTER: "no", OTEL_METRICS_EXPORTER: "True", @@ -218,13 +217,12 @@ def test_get_configurations_env_vars_validation(self, resource_create_mock): self.assertEqual(configurations["disable_logging"], False) self.assertEqual(configurations["disable_metrics"], False) self.assertEqual(configurations["disable_tracing"], False) - self.assertEqual(configurations["sampling_ratio"], 1.0) + self.assertEqual(configurations["sampler_type"], "microsoft.fixed_percentage") @patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "microsoft.fixed.percentage", - OTEL_TRACES_SAMPLER_ARG: "10.45", OTEL_TRACES_EXPORTER: "False", OTEL_LOGS_EXPORTER: "no", OTEL_METRICS_EXPORTER: "True", @@ -238,7 +236,7 @@ def test_get_configurations_env_vars_validation_check_backward_compatibility(sel self.assertEqual(configurations["disable_logging"], False) self.assertEqual(configurations["disable_metrics"], False) self.assertEqual(configurations["disable_tracing"], False) - self.assertEqual(configurations["sampling_ratio"], 1.0) + self.assertEqual(configurations[SAMPLER_TYPE], "microsoft.fixed.percentage") @patch.dict( "os.environ", @@ -427,8 +425,7 @@ def test_get_configurations_logging_format_no_env_var(self): "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER: RATE_LIMITED_SAMPLER, - OTEL_TRACES_SAMPLER_ARG: "0.5", + OTEL_TRACES_SAMPLER: "always_offk", OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", @@ -437,7 +434,7 @@ def test_get_configurations_logging_format_no_env_var(self): clear=True, ) @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) - def test_get_configurations_env_vars_rate_limited(self, resource_create_mock): + def test_get_configurations_env_vars_always_off_incorrect_val(self, resource_create_mock): configurations = _get_configurations() self.assertTrue("connection_string" not in configurations) @@ -460,40 +457,12 @@ def test_get_configurations_env_vars_rate_limited(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["traces_per_second"], 0.5) - - @patch.dict("os.environ", {}, clear=True) - @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) - def test_get_configurations_rate_limited_sampler_param(self, resource_create_mock): - configurations = _get_configurations(traces_per_second=2.5) - - self.assertTrue("connection_string" not in configurations) - self.assertEqual(configurations["disable_logging"], False) - self.assertEqual(configurations["disable_metrics"], False) - self.assertEqual(configurations["disable_tracing"], False) - self.assertEqual( - configurations["instrumentation_options"], - { - "azure_sdk": {"enabled": True}, - "django": {"enabled": True}, - "fastapi": {"enabled": True}, - "flask": {"enabled": True}, - "psycopg2": {"enabled": True}, - "requests": {"enabled": True}, - "urllib": {"enabled": True}, - "urllib3": {"enabled": True}, - }, - ) - self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "azure_app_service,azure_vm") - resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["traces_per_second"], 2.5) + self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") @patch.dict( "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER_ARG: "34", OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", @@ -525,52 +494,13 @@ def test_get_configurations_env_vars_no_preference(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["traces_per_second"], 5.0) - - @patch.dict( - "os.environ", - { - OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER_ARG: "2 traces per second", - OTEL_TRACES_EXPORTER: "None", - OTEL_LOGS_EXPORTER: "none", - OTEL_METRICS_EXPORTER: "NONE", - OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "custom_resource_detector", - }, - clear=True, - ) - @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) - def test_get_configurations_env_vars_check_default(self, resource_create_mock): - configurations = _get_configurations() - - self.assertTrue("connection_string" not in configurations) - self.assertEqual(configurations["disable_logging"], True) - self.assertEqual(configurations["disable_metrics"], True) - self.assertEqual(configurations["disable_tracing"], True) - self.assertEqual( - configurations["instrumentation_options"], - { - "azure_sdk": {"enabled": False}, - "django": {"enabled": True}, - "fastapi": {"enabled": False}, - "flask": {"enabled": False}, - "psycopg2": {"enabled": True}, - "requests": {"enabled": False}, - "urllib": {"enabled": True}, - "urllib3": {"enabled": True}, - }, - ) - self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") - resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["traces_per_second"], 5.0) + self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") @patch.dict( "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", OTEL_TRACES_SAMPLER: FIXED_PERCENTAGE_SAMPLER, - OTEL_TRACES_SAMPLER_ARG: "0.9", OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", @@ -602,7 +532,7 @@ def test_get_configurations_env_vars_fixed_percentage(self, resource_create_mock self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["sampling_ratio"], 0.9) + self.assertEqual(configurations["sampler_type"], "microsoft.fixed_percentage") @patch.dict( "os.environ", @@ -640,7 +570,6 @@ def test_get_configurations_env_vars_always_on(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["sampling_arg"], 1.0) self.assertEqual(configurations["sampler_type"], "always_on") @patch.dict( @@ -657,7 +586,6 @@ def test_get_configurations_env_vars_always_off(self, resource_create_mock): self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 0.0) self.assertEqual(configurations["sampler_type"], "always_off") @patch.dict( @@ -665,25 +593,6 @@ def test_get_configurations_env_vars_always_off(self, resource_create_mock): { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", OTEL_TRACES_SAMPLER: TRACE_ID_RATIO_SAMPLER, - OTEL_TRACES_SAMPLER_ARG: "3.5", - }, - clear=True, - ) - @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) - def test_get_configurations_env_vars_trace_id_ratio_incorrect_value(self, resource_create_mock): - configurations = _get_configurations() - - self.assertTrue("connection_string" not in configurations) - self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 1.0) - self.assertEqual(configurations["sampler_type"], "trace_id_ratio") - - @patch.dict( - "os.environ", - { - OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER: TRACE_ID_RATIO_SAMPLER, - OTEL_TRACES_SAMPLER_ARG: ".75", }, clear=True, ) @@ -693,25 +602,6 @@ def test_get_configurations_env_vars_trace_id_ratio(self, resource_create_mock): self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 0.75) - self.assertEqual(configurations["sampler_type"], "trace_id_ratio") - - @patch.dict( - "os.environ", - { - OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER: TRACE_ID_RATIO_SAMPLER, - OTEL_TRACES_SAMPLER_ARG: "sampler", - }, - clear=True, - ) - @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) - def test_get_configurations_env_vars_trace_id_ratio_non_numeric_value(self, resource_create_mock): - configurations = _get_configurations() - - self.assertTrue("connection_string" not in configurations) - self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 1.0) self.assertEqual(configurations["sampler_type"], "trace_id_ratio") @patch.dict( @@ -728,7 +618,6 @@ def test_get_configurations_env_vars_parentbased_always_on(self, resource_create self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 1.0) self.assertEqual(configurations["sampler_type"], "parentbased_always_on") @patch.dict( @@ -745,7 +634,6 @@ def test_get_configurations_env_vars_parentbased_always_off(self, resource_creat self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 0.0) self.assertEqual(configurations["sampler_type"], "parentbased_always_off") @patch.dict( @@ -753,7 +641,6 @@ def test_get_configurations_env_vars_parentbased_always_off(self, resource_creat { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", OTEL_TRACES_SAMPLER: PARENT_BASED_TRACE_ID_RATIO_SAMPLER, - OTEL_TRACES_SAMPLER_ARG: "0.89", }, clear=True, ) @@ -763,27 +650,6 @@ def test_get_configurations_env_vars_parentbased_trace_id_ratio(self, resource_c self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 0.89) - self.assertEqual(configurations["sampler_type"], "parentbased_trace_id_ratio") - - @patch.dict( - "os.environ", - { - OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER: PARENT_BASED_TRACE_ID_RATIO_SAMPLER, - OTEL_TRACES_SAMPLER_ARG: "9.45", - }, - clear=True, - ) - @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) - def test_get_configurations_env_vars_parentbased_trace_id_ratio_with_out_of_bounds_value( - self, resource_create_mock - ): - configurations = _get_configurations() - - self.assertTrue("connection_string" not in configurations) - self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 1.0) self.assertEqual(configurations["sampler_type"], "parentbased_trace_id_ratio") @patch.dict( @@ -791,35 +657,9 @@ def test_get_configurations_env_vars_parentbased_trace_id_ratio_with_out_of_boun { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", OTEL_TRACES_SAMPLER: PARENT_BASED_TRACE_ID_RATIO_SAMPLER, - OTEL_TRACES_SAMPLER_ARG: "non-numeric-value", }, clear=True, ) - @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) - def test_get_configurations_env_vars_parentbased_trace_id_ratio_non_numeric_value(self, resource_create_mock): - configurations = _get_configurations() - - self.assertTrue("connection_string" not in configurations) - self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 1.0) - self.assertEqual(configurations["sampler_type"], "parentbased_trace_id_ratio") - - @patch.dict( - "os.environ", - { - OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER: PARENT_BASED_TRACE_ID_RATIO_SAMPLER, - }, - clear=True, - ) - @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) - def test_get_configurations_env_vars_parentbased_trace_id_ratio_no_sampler_argument(self, resource_create_mock): - configurations = _get_configurations() - - self.assertTrue("connection_string" not in configurations) - self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampling_arg"], 1.0) - self.assertEqual(configurations["sampler_type"], "parentbased_trace_id_ratio") @patch.dict( "os.environ", @@ -834,26 +674,24 @@ def test_get_configurations_env_vars_no_sampling_env_set(self, resource_create_m self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["traces_per_second"], 5.0) + self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") # Tests for the _get_sampler_from_name function def test_get_sampler_from_name_always_on_off(self): - self.assertIs(_get_sampler_from_name(ALWAYS_ON_SAMPLER, None), ALWAYS_ON) - self.assertIs(_get_sampler_from_name(ALWAYS_OFF_SAMPLER, None), ALWAYS_OFF) + self.assertIs(_get_sampler_from_name(ALWAYS_ON_SAMPLER), ALWAYS_ON) + self.assertIs(_get_sampler_from_name(ALWAYS_OFF_SAMPLER), ALWAYS_OFF) def test_get_sampler_from_name_trace_id_ratio(self): - sampler = _get_sampler_from_name(TRACE_ID_RATIO_SAMPLER, "0.3") + sampler = _get_sampler_from_name(TRACE_ID_RATIO_SAMPLER) self.assertIsInstance(sampler, TraceIdRatioBased) - self.assertEqual(sampler._rate, 0.3) def test_get_sampler_from_name_trace_id_ratio_defaults_to_one(self): - sampler = _get_sampler_from_name(TRACE_ID_RATIO_SAMPLER, None) + sampler = _get_sampler_from_name(TRACE_ID_RATIO_SAMPLER) self.assertIsInstance(sampler, TraceIdRatioBased) - self.assertEqual(sampler._rate, 1.0) def test_get_sampler_from_name_parent_based_fixed(self): - sampler_on = _get_sampler_from_name(PARENT_BASED_ALWAYS_ON_SAMPLER, None) - sampler_off = _get_sampler_from_name(PARENT_BASED_ALWAYS_OFF_SAMPLER, None) + sampler_on = _get_sampler_from_name(PARENT_BASED_ALWAYS_ON_SAMPLER) + sampler_off = _get_sampler_from_name(PARENT_BASED_ALWAYS_OFF_SAMPLER) self.assertIsInstance(sampler_on, ParentBased) self.assertIs(sampler_on._root, ALWAYS_ON) @@ -862,17 +700,15 @@ def test_get_sampler_from_name_parent_based_fixed(self): self.assertIs(sampler_off._root, ALWAYS_OFF) def test_get_sampler_from_name_parent_based_trace_id_ratio(self): - sampler = _get_sampler_from_name(PARENT_BASED_TRACE_ID_RATIO_SAMPLER, "0.25") + sampler = _get_sampler_from_name(PARENT_BASED_TRACE_ID_RATIO_SAMPLER) self.assertIsInstance(sampler, ParentBased) self.assertIsInstance(sampler._root, TraceIdRatioBased) - self.assertEqual(sampler._root._rate, 0.25) def test_get_sampler_from_name_parent_based_trace_id_ratio_defaults(self): - sampler = _get_sampler_from_name(PARENT_BASED_TRACE_ID_RATIO_SAMPLER, None) + sampler = _get_sampler_from_name(PARENT_BASED_TRACE_ID_RATIO_SAMPLER) self.assertIsInstance(sampler._root, TraceIdRatioBased) - self.assertEqual(sampler._root._rate, 1.0) def test_get_sampler_from_name_invalid_type_defaults_parentbased_always_on(self): - sampler = _get_sampler_from_name("not-a-sampler", None) + sampler = _get_sampler_from_name("not-a-sampler") self.assertIsInstance(sampler, ParentBased) self.assertIs(sampler._root, ALWAYS_ON) From d84c2741ba6c1fb68af5217120d6014e35a683e5 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 4 Mar 2026 13:20:48 -0800 Subject: [PATCH 03/21] Update rate limited sampler logic and rename default arg --- .../export/trace/_rate_limited_sampling.py | 40 ++++++++++++------- .../exporter/export/trace/_sampling.py | 16 ++++---- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py index 1b2c5ba9f8af..74142df24484 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py @@ -91,22 +91,32 @@ def get(self) -> float: class RateLimitedSampler(Sampler): - def __init__(self, traces_per_second: float = 5.0): - sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) - try: - sampler_value = float(sampling_arg) if sampling_arg is not None else traces_per_second - if sampler_value < 0.0: - _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a non-negative number.") + def __init__(self, traces_per_second: Optional[float] = None): + default_traces_per_second = 5.0 + if traces_per_second is not None: + if traces_per_second < 0.0: + _logger.error("Invalid value for traces per second. It should be a non-negative number.") + traces_per_second = default_traces_per_second else: - _logger.info("Using rate limited sampler: %s traces per second", sampler_value) - traces_per_second = sampler_value - except ValueError as e: - _logger.error( # pylint: disable=C - _INVALID_TRACES_PER_SECOND_MESSAGE, - OTEL_TRACES_SAMPLER_ARG, - traces_per_second, - e, - ) + _logger.info("Using rate limited sampler: %s traces per second", traces_per_second) + else: + sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) + try: + sampler_value = float(sampling_arg) if sampling_arg is not None else default_traces_per_second + if sampler_value < 0.0: + _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a non-negative number.") + traces_per_second = default_traces_per_second + else: + _logger.info("Using rate limited sampler: %s traces per second", sampler_value) + traces_per_second = sampler_value + except ValueError as e: + _logger.error( # pylint: disable=C + _INVALID_TRACES_PER_SECOND_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + default_traces_per_second, + e, + ) + traces_per_second = default_traces_per_second self._sampling_percentage_generator = RateLimitedSamplingPercentage(traces_per_second) self._description = f"RateLimitedSampler{{{traces_per_second}}}" diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py index ba14568639e6..390931acbd25 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py @@ -37,18 +37,18 @@ class ApplicationInsightsSampler(Sampler): # sampling_ratio must take a value in the range [0,1] def __init__(self, sampling_ratio: Optional[float] = None): - default_value = 1.0 + default_sampling_ratio = 1.0 if sampling_ratio is not None: if sampling_ratio < 0.0 or sampling_ratio > 1.0: - _logger.error("Sampling ratio must be in the range [0.0, 1.0]. Defaulting to %s.", default_value) - sampling_ratio = default_value + _logger.error("Sampling ratio must be in the range [0.0, 1.0]. Defaulting to %s.", default_sampling_ratio) + sampling_ratio = default_sampling_ratio else: sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) try: - sampler_value = float(sampling_arg) if sampling_arg is not None else default_value + sampler_value = float(sampling_arg) if sampling_arg is not None else default_sampling_ratio if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1. Defaulting to %s.", default_value) - sampling_ratio = default_value + _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1. Defaulting to %s.", default_sampling_ratio) + sampling_ratio = default_sampling_ratio else: _logger.info("Using sampling ratio: %s", sampler_value) sampling_ratio = sampler_value @@ -56,10 +56,10 @@ def __init__(self, sampling_ratio: Optional[float] = None): _logger.error( # pylint: disable=C _INVALID_FLOAT_MESSAGE, OTEL_TRACES_SAMPLER_ARG, - default_value, + default_sampling_ratio, e, ) - sampling_ratio = default_value + sampling_ratio = default_sampling_ratio self._ratio = sampling_ratio self._sample_rate = sampling_ratio * 100 From 0577bad26963a68cdf18a42169d1f9d530abaa6f Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 6 Mar 2026 08:22:53 -0800 Subject: [PATCH 04/21] Adding tests for RLS --- .../export/trace/_rate_limited_sampling.py | 6 ++-- .../tests/trace/test_rate_limited_sampling.py | 34 ++++++++++++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py index 74142df24484..571df15898dd 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py @@ -95,7 +95,7 @@ def __init__(self, traces_per_second: Optional[float] = None): default_traces_per_second = 5.0 if traces_per_second is not None: if traces_per_second < 0.0: - _logger.error("Invalid value for traces per second. It should be a non-negative number.") + _logger.error("Invalid value %s, for traces per second. It should be a non-negative number.", traces_per_second) traces_per_second = default_traces_per_second else: _logger.info("Using rate limited sampler: %s traces per second", traces_per_second) @@ -104,13 +104,13 @@ def __init__(self, traces_per_second: Optional[float] = None): try: sampler_value = float(sampling_arg) if sampling_arg is not None else default_traces_per_second if sampler_value < 0.0: - _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a non-negative number.") + _logger.error("Invalid value %s, for OTEL_TRACES_SAMPLER_ARG. It should be a non-negative number.", sampler_value) traces_per_second = default_traces_per_second else: _logger.info("Using rate limited sampler: %s traces per second", sampler_value) traces_per_second = sampler_value except ValueError as e: - _logger.error( # pylint: disable=C + _logger.error( # pylint: disable=C0301 _INVALID_TRACES_PER_SECOND_MESSAGE, OTEL_TRACES_SAMPLER_ARG, default_traces_per_second, diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py index 03133d065aef..e33707d251fa 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py @@ -172,11 +172,43 @@ def test_sampler_creation(self): sampler = RateLimitedSampler(target_rate) self.assertIsInstance(sampler, RateLimitedSampler) self.assertEqual(sampler.get_description(), f"RateLimitedSampler{{{target_rate}}}") + - # Test that negative target rates raise a ValueError + # Test that negative explicit traces per second logs error and defaults to 5.0 def test_negative_rate_raises_error(self): with self.assertRaises(ValueError): RateLimitedSampler(-1.0) + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + + # Test default traces per second when no argument and no env var set + def test_constructor_default_traces_per_second(self): + with patch.dict("os.environ", {}, clear=True): + sampler = RateLimitedSampler() + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + + # Test traces per second is read from OTEL_TRACES_SAMPLER_ARG when no explicit value passed + def test_constructor_traces_per_second_from_env_var(self): + with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "10.0"}): + sampler = RateLimitedSampler() + self.assertEqual(sampler.get_description(), "RateLimitedSampler{10.0}") + + # Test explicit traces per second takes precedence over OTEL_TRACES_SAMPLER_ARG + def test_constructor_explicit_ignores_env_var(self): + with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "10.0"}): + sampler = RateLimitedSampler(3.0) + self.assertEqual(sampler.get_description(), "RateLimitedSampler{3.0}") + + # Test env var with negative value logs error and defaults to 5.0 + def test_constructor_env_var_negative(self): + with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "-5.0"}): + sampler = RateLimitedSampler() + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + + # Test env var with invalid float value logs error and defaults to 5.0 + def test_constructor_env_var_invalid_float(self): + with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "not_a_number"}): + sampler = RateLimitedSampler() + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") # Test sampling behavior with zero target rate def test_zero_rate_sampling(self): From 44153bc2fdf858674f6a90e3ee424add79d9e9ae Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 9 Mar 2026 14:02:04 -0700 Subject: [PATCH 05/21] Improve logging and logic for argument --- .../export/trace/_rate_limited_sampling.py | 18 +++--- .../exporter/export/trace/_sampling.py | 14 +++-- .../tests/trace/test_rate_limited_sampling.py | 16 +++-- .../tests/trace/test_sampling.py | 6 ++ .../opentelemetry/_utils/configurations.py | 62 +++++++++---------- 5 files changed, 64 insertions(+), 52 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py index 571df15898dd..9bf775c45906 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py @@ -30,7 +30,7 @@ parent_context_sampling, ) -_INVALID_TRACES_PER_SECOND_MESSAGE = "Value of %s must be a positive number for traces per second. Defaulting to %s: %s" +_INVALID_TRACES_PER_SECOND_MESSAGE = "Value of %s must be a positive number for traces per second. Defaulting to %s." _logger = getLogger(__name__) @@ -94,11 +94,15 @@ class RateLimitedSampler(Sampler): def __init__(self, traces_per_second: Optional[float] = None): default_traces_per_second = 5.0 if traces_per_second is not None: - if traces_per_second < 0.0: - _logger.error("Invalid value %s, for traces per second. It should be a non-negative number.", traces_per_second) + try: + if traces_per_second < 0.0: + _logger.error("Invalid value %s, for traces per second. It should be a non-negative number.", traces_per_second) + traces_per_second = default_traces_per_second + else: + _logger.info("Using rate limited sampler: %s traces per second", traces_per_second) + except TypeError: + _logger.error("Invalid value %s, for traces per second. Defaulting to %s", traces_per_second, default_traces_per_second) traces_per_second = default_traces_per_second - else: - _logger.info("Using rate limited sampler: %s traces per second", traces_per_second) else: sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) try: @@ -109,15 +113,13 @@ def __init__(self, traces_per_second: Optional[float] = None): else: _logger.info("Using rate limited sampler: %s traces per second", sampler_value) traces_per_second = sampler_value - except ValueError as e: + except ValueError as e: # pylint: disable=unused-variable _logger.error( # pylint: disable=C0301 _INVALID_TRACES_PER_SECOND_MESSAGE, OTEL_TRACES_SAMPLER_ARG, default_traces_per_second, - e, ) traces_per_second = default_traces_per_second - self._sampling_percentage_generator = RateLimitedSamplingPercentage(traces_per_second) self._description = f"RateLimitedSampler{{{traces_per_second}}}" diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py index 390931acbd25..e3f917799363 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py @@ -23,7 +23,7 @@ from azure.monitor.opentelemetry.exporter._constants import _SAMPLE_RATE_KEY -_INVALID_FLOAT_MESSAGE = "Value of %s must be a float. Defaulting to %s: %s" +_INVALID_FLOAT_MESSAGE = "Value of %s must be a float. Defaulting to %s." _logger = getLogger(__name__) @@ -39,8 +39,12 @@ class ApplicationInsightsSampler(Sampler): def __init__(self, sampling_ratio: Optional[float] = None): default_sampling_ratio = 1.0 if sampling_ratio is not None: - if sampling_ratio < 0.0 or sampling_ratio > 1.0: - _logger.error("Sampling ratio must be in the range [0.0, 1.0]. Defaulting to %s.", default_sampling_ratio) + try: + if sampling_ratio < 0.0 or sampling_ratio > 1.0: + _logger.error("Sampling ratio must be in the range [0.0, 1.0]. Defaulting to %s.", default_sampling_ratio) + sampling_ratio = default_sampling_ratio + except TypeError: + _logger.error("Invalid value %s, for sampling ratio. Defaulting to %s.", sampling_ratio, default_sampling_ratio) sampling_ratio = default_sampling_ratio else: sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) @@ -52,15 +56,13 @@ def __init__(self, sampling_ratio: Optional[float] = None): else: _logger.info("Using sampling ratio: %s", sampler_value) sampling_ratio = sampler_value - except ValueError as e: + except ValueError as e: # pylint: disable=unused-variable _logger.error( # pylint: disable=C _INVALID_FLOAT_MESSAGE, OTEL_TRACES_SAMPLER_ARG, default_sampling_ratio, - e, ) sampling_ratio = default_sampling_ratio - self._ratio = sampling_ratio self._sample_rate = sampling_ratio * 100 diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py index e33707d251fa..5b06e116e19c 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py @@ -172,12 +172,15 @@ def test_sampler_creation(self): sampler = RateLimitedSampler(target_rate) self.assertIsInstance(sampler, RateLimitedSampler) self.assertEqual(sampler.get_description(), f"RateLimitedSampler{{{target_rate}}}") - # Test that negative explicit traces per second logs error and defaults to 5.0 def test_negative_rate_raises_error(self): - with self.assertRaises(ValueError): - RateLimitedSampler(-1.0) + sampler = RateLimitedSampler(-1.0) + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + + # Test that non-numeric explicit traces per second logs error and defaults to 5.0 + def test_invalid_type_rate_defaults(self): + sampler = RateLimitedSampler({}) # type: ignore self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") # Test default traces per second when no argument and no env var set @@ -193,10 +196,11 @@ def test_constructor_traces_per_second_from_env_var(self): self.assertEqual(sampler.get_description(), "RateLimitedSampler{10.0}") # Test explicit traces per second takes precedence over OTEL_TRACES_SAMPLER_ARG - def test_constructor_explicit_ignores_env_var(self): + def test_constructor_explicit_value_passed_through_distro(self): with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "10.0"}): - sampler = RateLimitedSampler(3.0) - self.assertEqual(sampler.get_description(), "RateLimitedSampler{3.0}") + traces_per_second = 2.0 + sampler = RateLimitedSampler(traces_per_second) + self.assertEqual(sampler.get_description(), f"RateLimitedSampler{{{traces_per_second}}}") # Test env var with negative value logs error and defaults to 5.0 def test_constructor_env_var_negative(self): diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py index 1ae0cc028e62..b386ddc88020 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py @@ -29,6 +29,12 @@ def test_invalid_ratio(self): sampler = ApplicationInsightsSampler(-0.01) self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0) + + def test_invalid_type_ratio_defaults(self): + # Non-numeric explicit ratio logs an error and defaults to 1.0 + sampler = ApplicationInsightsSampler({}) # type: ignore + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) def test_user_passed_value_through_distro(self): sampler = ApplicationInsightsSampler(sampling_ratio=0.5) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py index b447634ea7c1..a5db133c91d5 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py @@ -76,7 +76,7 @@ from azure.monitor.opentelemetry._version import VERSION -_INVALID_FLOAT_MESSAGE = "Value of %s must be a float. Defaulting to %s: %s" +_INVALID_FLOAT_MESSAGE = "Value of %s must be a float. Defaulting to %s." _INVALID_TRACES_PER_SECOND_MESSAGE = "Value of %s must be a positive number for traces per second. Defaulting to %s: %s" _SUPPORTED_RESOURCE_DETECTORS = ( _AZURE_APP_SERVICE_RESOURCE_DETECTOR_NAME, @@ -203,7 +203,6 @@ def _default_sampler(configurations): "No sampler specified. Defaulting to %s: %s", RATE_LIMITED_SAMPLER, "5.0 traces per second", ) configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER - print(f"Sampler type set to: {configurations[SAMPLER_TYPE]}") def _default_instrumentation_options(configurations): @@ -286,44 +285,43 @@ def _default_enable_trace_based_sampling(configurations): def _get_sampler_from_name(sampler_type): default_value = 1.0 - sampler_arg = environ.get(OTEL_TRACES_SAMPLER_ARG) - sampler_value = float(sampler_arg) if sampler_arg is not None else default_value + if sampler_type == PARENT_BASED_ALWAYS_OFF_SAMPLER: + return ParentBased(ALWAYS_OFF) + + if sampler_type == PARENT_BASED_ALWAYS_ON_SAMPLER: + return ParentBased(ALWAYS_ON) + if sampler_type == ALWAYS_ON_SAMPLER: return ALWAYS_ON + if sampler_type == ALWAYS_OFF_SAMPLER: return ALWAYS_OFF - if sampler_type == TRACE_ID_RATIO_SAMPLER: + + sampler_arg = environ.get(OTEL_TRACES_SAMPLER_ARG) + sampler_value = default_value + if sampler_arg is not None: try: - if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid sampler argument for TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1.") - sampler_value = default_value - else: - _logger.info("Using sampling value: %s", sampler_value) + sampler_value = float(sampler_arg) except ValueError as e: - _logger.error( # pylint: disable=C + _logger.error( _INVALID_FLOAT_MESSAGE, OTEL_TRACES_SAMPLER_ARG, default_value, - e, ) - ratio = float(sampler_value) if sampler_value is not None else default_value - return TraceIdRatioBased(ratio) - if sampler_type == PARENT_BASED_ALWAYS_OFF_SAMPLER: - return ParentBased(ALWAYS_OFF) + sampler_value = default_value + + if sampler_type == TRACE_ID_RATIO_SAMPLER: + if sampler_value < 0.0 or sampler_value > 1.0: + _logger.error("Invalid sampler argument for TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1. Defaulting to %s.", default_value) + sampler_value = default_value + else: + _logger.info("Using sampling value: %s", sampler_value) + return TraceIdRatioBased(sampler_value) + if sampler_type == PARENT_BASED_TRACE_ID_RATIO_SAMPLER: - try: - if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid sampler argument for PARENT_BASED_TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1.") - sampler_value = default_value - else: - _logger.info("Using sampling value: %s", sampler_value) - except ValueError as e: - _logger.error( # pylint: disable=C - _INVALID_FLOAT_MESSAGE, - OTEL_TRACES_SAMPLER_ARG, - default_value, - e, - ) - ratio = float(sampler_value) if sampler_value is not None else default_value - return ParentBased(TraceIdRatioBased(ratio)) - return ParentBased(ALWAYS_ON) + if sampler_value < 0.0 or sampler_value > 1.0: + _logger.error("Invalid sampler argument for PARENT_BASED_TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1. Defaulting to %s.", default_value) + sampler_value = default_value + else: + _logger.info("Using sampling value: %s", sampler_value) + return ParentBased(TraceIdRatioBased(sampler_value)) From 076851d42fc25ef15d104c3c499b606451de83ab Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 20 Mar 2026 16:00:04 -0700 Subject: [PATCH 06/21] Improve logging --- .../exporter/export/trace/_rate_limited_sampling.py | 11 ++++++----- .../opentelemetry/exporter/export/trace/_sampling.py | 4 ++-- .../azure/monitor/opentelemetry/_configure.py | 2 +- .../monitor/opentelemetry/_utils/configurations.py | 7 ++++--- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py index 9bf775c45906..6214a7c08e5b 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py @@ -30,7 +30,8 @@ parent_context_sampling, ) -_INVALID_TRACES_PER_SECOND_MESSAGE = "Value of %s must be a positive number for traces per second. Defaulting to %s." +_INVALID_TRACES_PER_SECOND_MESSAGE = "Invalid value '%s' for traces per second. Expected a float. Defaulting to %s." +_INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE = "Invalid value '%s' for traces per second. It should be a non-negative number. Defaulting to %s" _logger = getLogger(__name__) @@ -96,19 +97,19 @@ def __init__(self, traces_per_second: Optional[float] = None): if traces_per_second is not None: try: if traces_per_second < 0.0: - _logger.error("Invalid value %s, for traces per second. It should be a non-negative number.", traces_per_second) + _logger.error(_INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE, traces_per_second, default_traces_per_second) traces_per_second = default_traces_per_second else: _logger.info("Using rate limited sampler: %s traces per second", traces_per_second) except TypeError: - _logger.error("Invalid value %s, for traces per second. Defaulting to %s", traces_per_second, default_traces_per_second) + _logger.error(_INVALID_TRACES_PER_SECOND_MESSAGE, traces_per_second, default_traces_per_second) traces_per_second = default_traces_per_second else: sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) try: sampler_value = float(sampling_arg) if sampling_arg is not None else default_traces_per_second if sampler_value < 0.0: - _logger.error("Invalid value %s, for OTEL_TRACES_SAMPLER_ARG. It should be a non-negative number.", sampler_value) + _logger.error(_INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE, sampler_value, default_traces_per_second) traces_per_second = default_traces_per_second else: _logger.info("Using rate limited sampler: %s traces per second", sampler_value) @@ -116,7 +117,7 @@ def __init__(self, traces_per_second: Optional[float] = None): except ValueError as e: # pylint: disable=unused-variable _logger.error( # pylint: disable=C0301 _INVALID_TRACES_PER_SECOND_MESSAGE, - OTEL_TRACES_SAMPLER_ARG, + sampling_arg, default_traces_per_second, ) traces_per_second = default_traces_per_second diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py index e3f917799363..314cbfdb9287 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py @@ -44,14 +44,14 @@ def __init__(self, sampling_ratio: Optional[float] = None): _logger.error("Sampling ratio must be in the range [0.0, 1.0]. Defaulting to %s.", default_sampling_ratio) sampling_ratio = default_sampling_ratio except TypeError: - _logger.error("Invalid value %s, for sampling ratio. Defaulting to %s.", sampling_ratio, default_sampling_ratio) + _logger.error("Invalid value '%s' for sampling ratio. Defaulting to %s.", sampling_ratio, default_sampling_ratio) sampling_ratio = default_sampling_ratio else: sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) try: sampler_value = float(sampling_arg) if sampling_arg is not None else default_sampling_ratio if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1. Defaulting to %s.", default_sampling_ratio) + _logger.error("Invalid value '%s' for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1. Defaulting to %s.", sampling_arg, default_sampling_ratio) sampling_ratio = default_sampling_ratio else: _logger.info("Using sampling ratio: %s", sampler_value) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py index e93b07acbf52..7ab0c720b273 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py @@ -170,7 +170,7 @@ def _setup_tracing(configurations: Dict[str, ConfigurationValue]): resource: Resource = configurations[RESOURCE_ARG] # type: ignore enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG] sampler_type = configurations.get(SAMPLER_TYPE, RATE_LIMITED_SAMPLER) - if sampler_type == RATE_LIMITED_SAMPLER: + if sampler_type == RATE_LIMITED_SAMPLER or sampler_type == "microsoft.fixed.percentage": traces_per_second = configurations.get(SAMPLING_TRACES_PER_SECOND_ARG) sampler = RateLimitedSampler() if traces_per_second is None else RateLimitedSampler(traces_per_second=traces_per_second) elif sampler_type == FIXED_PERCENTAGE_SAMPLER: diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py index a5db133c91d5..df4058b065bd 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py @@ -186,8 +186,8 @@ def _default_sampler(configurations): if sampler in SUPPORTED_OTEL_SAMPLERS: configurations[SAMPLER_TYPE] = sampler elif sampler is not None: - _logger.error( # pylint: disable=C - "Invalid value: %s, provided for the sampler to be used for tracing. " + _logger.error( # pylint: disable=C0301 + "Invalid value '%s' for the sampler. " "Supported values are %s. Defaulting to %s: %s", sampler, SUPPORTED_OTEL_SAMPLERS, RATE_LIMITED_SAMPLER, "5.0 traces per second", ) @@ -199,7 +199,7 @@ def _default_sampler(configurations): configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER configurations[SAMPLING_TRACES_PER_SECOND_ARG] = configurations["traces_per_second"] else: - _logger.info( # pylint: disable=C + _logger.info( # pylint: disable=C0301 "No sampler specified. Defaulting to %s: %s", RATE_LIMITED_SAMPLER, "5.0 traces per second", ) configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER @@ -325,3 +325,4 @@ def _get_sampler_from_name(sampler_type): else: _logger.info("Using sampling value: %s", sampler_value) return ParentBased(TraceIdRatioBased(sampler_value)) + return ParentBased(ALWAYS_ON) \ No newline at end of file From 97eb1d01894a7bc2f72786fe0434b8d4423f9dd1 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Mar 2026 05:04:10 -0700 Subject: [PATCH 07/21] Fix format --- .../export/trace/_rate_limited_sampling.py | 13 +++++++++--- .../exporter/export/trace/_sampling.py | 20 ++++++++++++++++--- .../tests/trace/test_sampling.py | 2 +- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py index 6214a7c08e5b..aad78a00e728 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py @@ -31,10 +31,13 @@ ) _INVALID_TRACES_PER_SECOND_MESSAGE = "Invalid value '%s' for traces per second. Expected a float. Defaulting to %s." -_INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE = "Invalid value '%s' for traces per second. It should be a non-negative number. Defaulting to %s" +_INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE = ( + "Invalid value '%s' for traces per second. It should be a non-negative number. Defaulting to %s" +) _logger = getLogger(__name__) + class _State: def __init__(self, effective_window_count: float, effective_window_nanoseconds: float, last_nano_time: int): self.effective_window_count = effective_window_count @@ -97,7 +100,9 @@ def __init__(self, traces_per_second: Optional[float] = None): if traces_per_second is not None: try: if traces_per_second < 0.0: - _logger.error(_INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE, traces_per_second, default_traces_per_second) + _logger.error( + _INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE, traces_per_second, default_traces_per_second + ) traces_per_second = default_traces_per_second else: _logger.info("Using rate limited sampler: %s traces per second", traces_per_second) @@ -109,7 +114,9 @@ def __init__(self, traces_per_second: Optional[float] = None): try: sampler_value = float(sampling_arg) if sampling_arg is not None else default_traces_per_second if sampler_value < 0.0: - _logger.error(_INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE, sampler_value, default_traces_per_second) + _logger.error( + _INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE, sampler_value, default_traces_per_second + ) traces_per_second = default_traces_per_second else: _logger.info("Using rate limited sampler: %s traces per second", sampler_value) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py index 314cbfdb9287..0416a01f8e7d 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py @@ -27,6 +27,7 @@ _logger = getLogger(__name__) + # Sampler is responsible for the following: # Implements same trace id hashing algorithm so that traces are sampled the same across multiple nodes (via AI SDKS) # Adds item count to span attribute if span is sampled (needed for ingestion service) @@ -41,17 +42,30 @@ def __init__(self, sampling_ratio: Optional[float] = None): if sampling_ratio is not None: try: if sampling_ratio < 0.0 or sampling_ratio > 1.0: - _logger.error("Sampling ratio must be in the range [0.0, 1.0]. Defaulting to %s.", default_sampling_ratio) + _logger.error( + "Invalid value '%s' for sampling ratio. " + "Sampling ratio must be in the range [0.0, 1.0]. " + "Defaulting to %s.", + sampling_ratio, + default_sampling_ratio, + ) sampling_ratio = default_sampling_ratio except TypeError: - _logger.error("Invalid value '%s' for sampling ratio. Defaulting to %s.", sampling_ratio, default_sampling_ratio) + _logger.error( + "Invalid value '%s' for sampling ratio. Defaulting to %s.", sampling_ratio, default_sampling_ratio + ) sampling_ratio = default_sampling_ratio else: sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) try: sampler_value = float(sampling_arg) if sampling_arg is not None else default_sampling_ratio if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid value '%s' for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1. Defaulting to %s.", sampling_arg, default_sampling_ratio) + _logger.error( + "Invalid value '%s' for OTEL_TRACES_SAMPLER_ARG. " + "It should be a value between 0 and 1. Defaulting to %s.", + sampling_arg, + default_sampling_ratio, + ) sampling_ratio = default_sampling_ratio else: _logger.info("Using sampling ratio: %s", sampler_value) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py index b386ddc88020..581e4d25ee6c 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py @@ -35,7 +35,7 @@ def test_invalid_type_ratio_defaults(self): sampler = ApplicationInsightsSampler({}) # type: ignore self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0) - + def test_user_passed_value_through_distro(self): sampler = ApplicationInsightsSampler(sampling_ratio=0.5) self.assertEqual(sampler._ratio, 0.5) From 8cbbd7c48368bbe6756e32e80b16c3b18ed83f07 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Mar 2026 10:21:11 -0700 Subject: [PATCH 08/21] Fix format --- .../azure/monitor/opentelemetry/_configure.py | 12 +++++++-- .../azure/monitor/opentelemetry/_constants.py | 2 +- .../opentelemetry/_utils/configurations.py | 26 +++++++++++++------ .../tests/utils/test_configurations.py | 1 - 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py index 7ab0c720b273..05ef11b00cbe 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py @@ -172,10 +172,18 @@ def _setup_tracing(configurations: Dict[str, ConfigurationValue]): sampler_type = configurations.get(SAMPLER_TYPE, RATE_LIMITED_SAMPLER) if sampler_type == RATE_LIMITED_SAMPLER or sampler_type == "microsoft.fixed.percentage": traces_per_second = configurations.get(SAMPLING_TRACES_PER_SECOND_ARG) - sampler = RateLimitedSampler() if traces_per_second is None else RateLimitedSampler(traces_per_second=traces_per_second) + sampler = ( + RateLimitedSampler() + if traces_per_second is None + else RateLimitedSampler(traces_per_second=traces_per_second) + ) elif sampler_type == FIXED_PERCENTAGE_SAMPLER: sampling_ratio = configurations.get(SAMPLING_RATIO_ARG) - sampler = ApplicationInsightsSampler() if sampling_ratio is None else ApplicationInsightsSampler(sampling_ratio=sampling_ratio) + sampler = ( + ApplicationInsightsSampler() + if sampling_ratio is None + else ApplicationInsightsSampler(sampling_ratio=sampling_ratio) + ) else: sampler = _get_sampler_from_name(sampler_type) tracer_provider = TracerProvider(sampler=sampler, resource=resource) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py index c274ff3e5762..6d3ffffc3514 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py @@ -41,7 +41,7 @@ PARENT_BASED_ALWAYS_OFF_SAMPLER = "parentbased_always_off" PARENT_BASED_TRACE_ID_RATIO_SAMPLER = "parentbased_trace_id_ratio" SUPPORTED_OTEL_SAMPLERS = ( - "microsoft.fixed.percentage", # Ensures backward compatibility with old value + "microsoft.fixed.percentage", # Ensures backward compatibility with old value RATE_LIMITED_SAMPLER, FIXED_PERCENTAGE_SAMPLER, ALWAYS_ON_SAMPLER, diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py index df4058b065bd..aa6893584c2e 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py @@ -187,9 +187,11 @@ def _default_sampler(configurations): configurations[SAMPLER_TYPE] = sampler elif sampler is not None: _logger.error( # pylint: disable=C0301 - "Invalid value '%s' for the sampler. " - "Supported values are %s. Defaulting to %s: %s", - sampler, SUPPORTED_OTEL_SAMPLERS, RATE_LIMITED_SAMPLER, "5.0 traces per second", + "Invalid value '%s' for the sampler. " "Supported values are %s. Defaulting to %s: %s", + sampler, + SUPPORTED_OTEL_SAMPLERS, + RATE_LIMITED_SAMPLER, + "5.0 traces per second", ) configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER elif configurations.get("sampling_ratio") is not None: @@ -200,7 +202,9 @@ def _default_sampler(configurations): configurations[SAMPLING_TRACES_PER_SECOND_ARG] = configurations["traces_per_second"] else: _logger.info( # pylint: disable=C0301 - "No sampler specified. Defaulting to %s: %s", RATE_LIMITED_SAMPLER, "5.0 traces per second", + "No sampler specified. Defaulting to %s: %s", + RATE_LIMITED_SAMPLER, + "5.0 traces per second", ) configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER @@ -296,7 +300,7 @@ def _get_sampler_from_name(sampler_type): if sampler_type == ALWAYS_OFF_SAMPLER: return ALWAYS_OFF - + sampler_arg = environ.get(OTEL_TRACES_SAMPLER_ARG) sampler_value = default_value if sampler_arg is not None: @@ -312,7 +316,10 @@ def _get_sampler_from_name(sampler_type): if sampler_type == TRACE_ID_RATIO_SAMPLER: if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid sampler argument for TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1. Defaulting to %s.", default_value) + _logger.error( + "Invalid sampler argument for TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1. Defaulting to %s.", + default_value, + ) sampler_value = default_value else: _logger.info("Using sampling value: %s", sampler_value) @@ -320,9 +327,12 @@ def _get_sampler_from_name(sampler_type): if sampler_type == PARENT_BASED_TRACE_ID_RATIO_SAMPLER: if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error("Invalid sampler argument for PARENT_BASED_TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1. Defaulting to %s.", default_value) + _logger.error( + "Invalid sampler argument for PARENT_BASED_TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1. Defaulting to %s.", + default_value, + ) sampler_value = default_value else: _logger.info("Using sampling value: %s", sampler_value) return ParentBased(TraceIdRatioBased(sampler_value)) - return ParentBased(ALWAYS_ON) \ No newline at end of file + return ParentBased(ALWAYS_ON) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py index 042ec1f7f166..3f46891adb34 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py @@ -660,7 +660,6 @@ def test_get_configurations_env_vars_parentbased_trace_id_ratio(self, resource_c }, clear=True, ) - @patch.dict( "os.environ", { From e6f7bb00cb508d7f628ba2f58d18c7ec700df006 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Mar 2026 11:45:17 -0700 Subject: [PATCH 09/21] Fix lint --- .../azure/monitor/opentelemetry/_configure.py | 4 +- .../opentelemetry/_utils/configurations.py | 69 +++++++++---------- 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py index 05ef11b00cbe..26b59da12fef 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py @@ -41,11 +41,9 @@ METRIC_READERS_ARG, VIEWS_ARG, ENABLE_TRACE_BASED_SAMPLING_ARG, - SAMPLING_ARG, SAMPLER_TYPE, RATE_LIMITED_SAMPLER, FIXED_PERCENTAGE_SAMPLER, - SAMPLER_TYPE, ) from azure.monitor.opentelemetry._types import ConfigurationValue from azure.monitor.opentelemetry.exporter._quickpulse import ( # pylint: disable=import-error,no-name-in-module @@ -170,7 +168,7 @@ def _setup_tracing(configurations: Dict[str, ConfigurationValue]): resource: Resource = configurations[RESOURCE_ARG] # type: ignore enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG] sampler_type = configurations.get(SAMPLER_TYPE, RATE_LIMITED_SAMPLER) - if sampler_type == RATE_LIMITED_SAMPLER or sampler_type == "microsoft.fixed.percentage": + if sampler_type in (RATE_LIMITED_SAMPLER, 'microsoft.fixed.percentage'): traces_per_second = configurations.get(SAMPLING_TRACES_PER_SECOND_ARG) sampler = ( RateLimitedSampler() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py index aa6893584c2e..fd4e1f644c58 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py @@ -69,7 +69,6 @@ PARENT_BASED_ALWAYS_ON_SAMPLER, PARENT_BASED_ALWAYS_OFF_SAMPLER, PARENT_BASED_TRACE_ID_RATIO_SAMPLER, - SAMPLING_ARG, SAMPLER_TYPE, ) from azure.monitor.opentelemetry._types import ConfigurationValue @@ -187,7 +186,7 @@ def _default_sampler(configurations): configurations[SAMPLER_TYPE] = sampler elif sampler is not None: _logger.error( # pylint: disable=C0301 - "Invalid value '%s' for the sampler. " "Supported values are %s. Defaulting to %s: %s", + "Invalid value '%s' for the sampler. Supported values are %s. Defaulting to %s: %s", sampler, SUPPORTED_OTEL_SAMPLERS, RATE_LIMITED_SAMPLER, @@ -289,50 +288,44 @@ def _default_enable_trace_based_sampling(configurations): def _get_sampler_from_name(sampler_type): default_value = 1.0 - if sampler_type == PARENT_BASED_ALWAYS_OFF_SAMPLER: - return ParentBased(ALWAYS_OFF) - - if sampler_type == PARENT_BASED_ALWAYS_ON_SAMPLER: - return ParentBased(ALWAYS_ON) - - if sampler_type == ALWAYS_ON_SAMPLER: - return ALWAYS_ON + sampler = None - if sampler_type == ALWAYS_OFF_SAMPLER: - return ALWAYS_OFF - - sampler_arg = environ.get(OTEL_TRACES_SAMPLER_ARG) - sampler_value = default_value - if sampler_arg is not None: - try: - sampler_value = float(sampler_arg) - except ValueError as e: - _logger.error( - _INVALID_FLOAT_MESSAGE, - OTEL_TRACES_SAMPLER_ARG, - default_value, - ) - sampler_value = default_value + if sampler_type == PARENT_BASED_ALWAYS_OFF_SAMPLER: + sampler = ParentBased(ALWAYS_OFF) + elif sampler_type == PARENT_BASED_ALWAYS_ON_SAMPLER: + sampler = ParentBased(ALWAYS_ON) + elif sampler_type == ALWAYS_ON_SAMPLER: + sampler = ALWAYS_ON + elif sampler_type == ALWAYS_OFF_SAMPLER: + sampler = ALWAYS_OFF + elif sampler_type in (TRACE_ID_RATIO_SAMPLER, PARENT_BASED_TRACE_ID_RATIO_SAMPLER): + sampler_arg = environ.get(OTEL_TRACES_SAMPLER_ARG) + sampler_value = default_value + if sampler_arg is not None: + try: + sampler_value = float(sampler_arg) + except ValueError: + _logger.error( + _INVALID_FLOAT_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + default_value, + ) + sampler_value = default_value - if sampler_type == TRACE_ID_RATIO_SAMPLER: if sampler_value < 0.0 or sampler_value > 1.0: _logger.error( - "Invalid sampler argument for TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1. Defaulting to %s.", + "Invalid sampler argument for %s. " \ + "It should be a value between 0 and 1. Defaulting to %s.", + sampler_type, default_value, ) sampler_value = default_value else: _logger.info("Using sampling value: %s", sampler_value) - return TraceIdRatioBased(sampler_value) - if sampler_type == PARENT_BASED_TRACE_ID_RATIO_SAMPLER: - if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error( - "Invalid sampler argument for PARENT_BASED_TRACE_ID_RATIO_SAMPLER. It should be a value between 0 and 1. Defaulting to %s.", - default_value, - ) - sampler_value = default_value + if sampler_type == TRACE_ID_RATIO_SAMPLER: + sampler = TraceIdRatioBased(sampler_value) else: - _logger.info("Using sampling value: %s", sampler_value) - return ParentBased(TraceIdRatioBased(sampler_value)) - return ParentBased(ALWAYS_ON) + sampler = ParentBased(TraceIdRatioBased(sampler_value)) + + return sampler if sampler is not None else ParentBased(ALWAYS_ON) From 7be8d0190b08707c9a730255529f9fee07317e3c Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Mar 2026 14:51:21 -0700 Subject: [PATCH 10/21] Fix format and cspell --- .../azure/monitor/opentelemetry/_configure.py | 2 +- .../azure/monitor/opentelemetry/_utils/configurations.py | 3 +-- .../tests/utils/test_configurations.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py index 26b59da12fef..d3d1f4641b6f 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py @@ -168,7 +168,7 @@ def _setup_tracing(configurations: Dict[str, ConfigurationValue]): resource: Resource = configurations[RESOURCE_ARG] # type: ignore enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG] sampler_type = configurations.get(SAMPLER_TYPE, RATE_LIMITED_SAMPLER) - if sampler_type in (RATE_LIMITED_SAMPLER, 'microsoft.fixed.percentage'): + if sampler_type in (RATE_LIMITED_SAMPLER, "microsoft.fixed.percentage"): traces_per_second = configurations.get(SAMPLING_TRACES_PER_SECOND_ARG) sampler = ( RateLimitedSampler() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py index fd4e1f644c58..dfe7fd1f6b16 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py @@ -314,8 +314,7 @@ def _get_sampler_from_name(sampler_type): if sampler_value < 0.0 or sampler_value > 1.0: _logger.error( - "Invalid sampler argument for %s. " \ - "It should be a value between 0 and 1. Defaulting to %s.", + "Invalid sampler argument for %s. " "It should be a value between 0 and 1. Defaulting to %s.", sampler_type, default_value, ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py index 3f46891adb34..e06fb6cdbf7c 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py @@ -425,7 +425,7 @@ def test_get_configurations_logging_format_no_env_var(self): "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER: "always_offk", + OTEL_TRACES_SAMPLER: "always_offk", # cspell:disable OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", From 6a2388451741e5e5059f54ad6723e401c259977d Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Mar 2026 15:08:51 -0700 Subject: [PATCH 11/21] Fix cspell --- .../tests/utils/test_configurations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py index e06fb6cdbf7c..7dbf0b8efc04 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py @@ -425,7 +425,7 @@ def test_get_configurations_logging_format_no_env_var(self): "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER: "always_offk", # cspell:disable + OTEL_TRACES_SAMPLER: "always_offk", # cSpell:disable OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", From 7f7fa17d766d6efef9b699e32f6ac11ab126c14e Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Mar 2026 15:35:49 -0700 Subject: [PATCH 12/21] Disable specific spelling --- .../tests/utils/test_configurations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py index 7dbf0b8efc04..8eca1ecb466b 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py @@ -425,7 +425,7 @@ def test_get_configurations_logging_format_no_env_var(self): "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER: "always_offk", # cSpell:disable + OTEL_TRACES_SAMPLER: "always_offk", # cSpell: ignore offk OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", From 45c93b3c4d2e03136bc6138aaed885afa32761ee Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Mar 2026 16:17:26 -0700 Subject: [PATCH 13/21] Fix lint --- .../azure/monitor/opentelemetry/_utils/configurations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py index dfe7fd1f6b16..4e03494e6888 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py @@ -314,7 +314,7 @@ def _get_sampler_from_name(sampler_type): if sampler_value < 0.0 or sampler_value > 1.0: _logger.error( - "Invalid sampler argument for %s. " "It should be a value between 0 and 1. Defaulting to %s.", + "Invalid sampler argument for %s. It should be a value between 0 and 1. Defaulting to %s.", sampler_type, default_value, ) From c45dbea0a511688573a85f7563e274e277ec5f34 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 24 Mar 2026 09:25:07 -0700 Subject: [PATCH 14/21] Retrigger CI/CD pipeline From ba05f8bcf195ab7fefdc103829915026c4268d69 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 24 Mar 2026 09:33:41 -0700 Subject: [PATCH 15/21] Retrigger CI/CD pipeline From a9b1cea73d8ec1b958149bd2ded8c0ca466b032c Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 24 Mar 2026 11:31:31 -0700 Subject: [PATCH 16/21] Remove distro changes (moved to separate PR) --- .../azure/monitor/opentelemetry/_configure.py | 32 ++- .../azure/monitor/opentelemetry/_constants.py | 1 - .../opentelemetry/_utils/configurations.py | 200 +++++++++++------ .../tests/test_configure.py | 12 +- .../tests/utils/test_configurations.py | 207 ++++++++++++++++-- 5 files changed, 333 insertions(+), 119 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py index d3d1f4641b6f..415b522509b5 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py @@ -41,9 +41,8 @@ METRIC_READERS_ARG, VIEWS_ARG, ENABLE_TRACE_BASED_SAMPLING_ARG, + SAMPLING_ARG, SAMPLER_TYPE, - RATE_LIMITED_SAMPLER, - FIXED_PERCENTAGE_SAMPLER, ) from azure.monitor.opentelemetry._types import ConfigurationValue from azure.monitor.opentelemetry.exporter._quickpulse import ( # pylint: disable=import-error,no-name-in-module @@ -167,24 +166,21 @@ def configure_azure_monitor(**kwargs) -> None: # pylint: disable=C4758 def _setup_tracing(configurations: Dict[str, ConfigurationValue]): resource: Resource = configurations[RESOURCE_ARG] # type: ignore enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG] - sampler_type = configurations.get(SAMPLER_TYPE, RATE_LIMITED_SAMPLER) - if sampler_type in (RATE_LIMITED_SAMPLER, "microsoft.fixed.percentage"): - traces_per_second = configurations.get(SAMPLING_TRACES_PER_SECOND_ARG) - sampler = ( - RateLimitedSampler() - if traces_per_second is None - else RateLimitedSampler(traces_per_second=traces_per_second) - ) - elif sampler_type == FIXED_PERCENTAGE_SAMPLER: - sampling_ratio = configurations.get(SAMPLING_RATIO_ARG) - sampler = ( - ApplicationInsightsSampler() - if sampling_ratio is None - else ApplicationInsightsSampler(sampling_ratio=sampling_ratio) + if SAMPLING_ARG in configurations: + sampler_arg = configurations[SAMPLING_ARG] + sampler_type = configurations[SAMPLER_TYPE] + sampler = _get_sampler_from_name(sampler_type, sampler_arg) + tracer_provider = TracerProvider(sampler=sampler, resource=resource) + elif SAMPLING_RATIO_ARG in configurations: + sampling_ratio = configurations[SAMPLING_RATIO_ARG] + tracer_provider = TracerProvider( + sampler=ApplicationInsightsSampler(sampling_ratio=cast(float, sampling_ratio)), resource=resource ) else: - sampler = _get_sampler_from_name(sampler_type) - tracer_provider = TracerProvider(sampler=sampler, resource=resource) + traces_per_second = configurations[SAMPLING_TRACES_PER_SECOND_ARG] + tracer_provider = TracerProvider( + sampler=RateLimitedSampler(target_spans_per_second_limit=cast(float, traces_per_second)), resource=resource + ) for span_processor in configurations[SPAN_PROCESSORS_ARG]: # type: ignore tracer_provider.add_span_processor(span_processor) # type: ignore diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py index 6d3ffffc3514..2823f173894a 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py @@ -41,7 +41,6 @@ PARENT_BASED_ALWAYS_OFF_SAMPLER = "parentbased_always_off" PARENT_BASED_TRACE_ID_RATIO_SAMPLER = "parentbased_trace_id_ratio" SUPPORTED_OTEL_SAMPLERS = ( - "microsoft.fixed.percentage", # Ensures backward compatibility with old value RATE_LIMITED_SAMPLER, FIXED_PERCENTAGE_SAMPLER, ALWAYS_ON_SAMPLER, diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py index 4e03494e6888..f323ddffcb01 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py @@ -69,13 +69,14 @@ PARENT_BASED_ALWAYS_ON_SAMPLER, PARENT_BASED_ALWAYS_OFF_SAMPLER, PARENT_BASED_TRACE_ID_RATIO_SAMPLER, + SAMPLING_ARG, SAMPLER_TYPE, ) from azure.monitor.opentelemetry._types import ConfigurationValue from azure.monitor.opentelemetry._version import VERSION -_INVALID_FLOAT_MESSAGE = "Value of %s must be a float. Defaulting to %s." +_INVALID_FLOAT_MESSAGE = "Value of %s must be a float. Defaulting to %s: %s" _INVALID_TRACES_PER_SECOND_MESSAGE = "Value of %s must be a positive number for traces per second. Defaulting to %s: %s" _SUPPORTED_RESOURCE_DETECTORS = ( _AZURE_APP_SERVICE_RESOURCE_DETECTOR_NAME, @@ -99,7 +100,7 @@ def _get_configurations(**kwargs) -> Dict[str, ConfigurationValue]: _default_logger_name(configurations) _default_logging_formatter(configurations) _default_resource(configurations) - _default_sampler(configurations) + _default_sampling_ratio(configurations) _default_instrumentation_options(configurations) _default_span_processors(configurations) _default_log_record_processors(configurations) @@ -180,32 +181,123 @@ def _default_resource(configurations): configurations[RESOURCE_ARG] = Resource.create(configurations[RESOURCE_ARG].attributes) -def _default_sampler(configurations): - sampler = (environ.get(OTEL_TRACES_SAMPLER) or "").strip() or None - if sampler in SUPPORTED_OTEL_SAMPLERS: - configurations[SAMPLER_TYPE] = sampler - elif sampler is not None: - _logger.error( # pylint: disable=C0301 - "Invalid value '%s' for the sampler. Supported values are %s. Defaulting to %s: %s", - sampler, - SUPPORTED_OTEL_SAMPLERS, - RATE_LIMITED_SAMPLER, - "5.0 traces per second", - ) - configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER - elif configurations.get("sampling_ratio") is not None: - configurations[SAMPLER_TYPE] = FIXED_PERCENTAGE_SAMPLER - configurations[SAMPLING_RATIO_ARG] = configurations["sampling_ratio"] - elif configurations.get("traces_per_second") is not None: - configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER - configurations[SAMPLING_TRACES_PER_SECOND_ARG] = configurations["traces_per_second"] +# pylint: disable=too-many-statements,too-many-branches +def _default_sampling_ratio(configurations): + default_value = 1.0 + default_value_for_rate_limited_sampler = 5.0 + sampler_type = environ.get(OTEL_TRACES_SAMPLER) + sampler_arg = environ.get(OTEL_TRACES_SAMPLER_ARG) + + # Handle rate-limited sampler + if sampler_type == RATE_LIMITED_SAMPLER: + try: + sampler_value = float(sampler_arg) if sampler_arg is not None else default_value_for_rate_limited_sampler + if sampler_value < 0.0: + _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a non-negative number.") + sampler_value = default_value_for_rate_limited_sampler + else: + _logger.info("Using rate limited sampler: %s traces per second", sampler_value) + configurations[SAMPLING_TRACES_PER_SECOND_ARG] = sampler_value + except ValueError as e: + _logger.error( # pylint: disable=C + _INVALID_TRACES_PER_SECOND_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + default_value_for_rate_limited_sampler, + e, + ) + configurations[SAMPLING_TRACES_PER_SECOND_ARG] = default_value_for_rate_limited_sampler + + # Handle fixed percentage sampler + elif sampler_type in (FIXED_PERCENTAGE_SAMPLER, "microsoft.fixed.percentage"): # to support older string + try: + sampler_value = float(sampler_arg) if sampler_arg is not None else default_value + if sampler_value < 0.0 or sampler_value > 1.0: + _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1.") + sampler_value = default_value + else: + _logger.info("Using sampling ratio: %s", sampler_value) + configurations[SAMPLING_RATIO_ARG] = sampler_value + except ValueError as e: + _logger.error( # pylint: disable=C + _INVALID_FLOAT_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + default_value, + e, + ) + configurations[SAMPLING_RATIO_ARG] = default_value + + # Handle always_on sampler + elif sampler_type == ALWAYS_ON_SAMPLER: + configurations[SAMPLING_ARG] = 1.0 + configurations[SAMPLER_TYPE] = ALWAYS_ON_SAMPLER + + # Handle always_off sampler + elif sampler_type == ALWAYS_OFF_SAMPLER: + configurations[SAMPLING_ARG] = 0.0 + configurations[SAMPLER_TYPE] = ALWAYS_OFF_SAMPLER + + # Handle trace_id_ratio sampler + elif sampler_type == TRACE_ID_RATIO_SAMPLER: + try: + sampler_value = float(sampler_arg) if sampler_arg is not None else default_value + if sampler_value < 0.0 or sampler_value > 1.0: + _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1.") + sampler_value = default_value + else: + _logger.info("Using sampling value: %s", sampler_value) + configurations[SAMPLING_ARG] = sampler_value + except ValueError as e: + _logger.error( # pylint: disable=C + _INVALID_FLOAT_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + default_value, + e, + ) + configurations[SAMPLING_ARG] = default_value + configurations[SAMPLER_TYPE] = TRACE_ID_RATIO_SAMPLER + + # Handle parentbased_always_on sampler + elif sampler_type == PARENT_BASED_ALWAYS_ON_SAMPLER: + configurations[SAMPLING_ARG] = 1.0 + configurations[SAMPLER_TYPE] = PARENT_BASED_ALWAYS_ON_SAMPLER + + # Handle parentbased_always_off sampler + elif sampler_type == PARENT_BASED_ALWAYS_OFF_SAMPLER: + configurations[SAMPLING_ARG] = 0.0 + configurations[SAMPLER_TYPE] = PARENT_BASED_ALWAYS_OFF_SAMPLER + + # Handle parentbased_trace_id_ratio sampler + elif sampler_type == PARENT_BASED_TRACE_ID_RATIO_SAMPLER: + try: + sampler_value = float(sampler_arg) if sampler_arg is not None else default_value + if sampler_value < 0.0 or sampler_value > 1.0: + _logger.error("Invalid value for OTEL_TRACES_SAMPLER_ARG. It should be a value between 0 and 1.") + sampler_value = default_value + else: + _logger.info("Using sampling value: %s", sampler_value) + configurations[SAMPLING_ARG] = sampler_value + except ValueError as e: + _logger.error( # pylint: disable=C + _INVALID_FLOAT_MESSAGE, + OTEL_TRACES_SAMPLER_ARG, + default_value, + e, + ) + configurations[SAMPLING_ARG] = default_value + configurations[SAMPLER_TYPE] = PARENT_BASED_TRACE_ID_RATIO_SAMPLER + + # Handle all other cases (no sampler type specified or unsupported sampler type) else: - _logger.info( # pylint: disable=C0301 - "No sampler specified. Defaulting to %s: %s", - RATE_LIMITED_SAMPLER, - "5.0 traces per second", - ) - configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER + if configurations.get(SAMPLING_TRACES_PER_SECOND_ARG) is None: + configurations[SAMPLING_TRACES_PER_SECOND_ARG] = default_value_for_rate_limited_sampler + if sampler_type is not None: + _logger.error( # pylint: disable=C + "Invalid argument for the sampler to be used for tracing. " + "Supported values are %s. Defaulting to %s: %s", + SUPPORTED_OTEL_SAMPLERS, + RATE_LIMITED_SAMPLER, + configurations[SAMPLING_TRACES_PER_SECOND_ARG], + ) def _default_instrumentation_options(configurations): @@ -286,45 +378,17 @@ def _default_enable_trace_based_sampling(configurations): configurations.setdefault(ENABLE_TRACE_BASED_SAMPLING_ARG, False) -def _get_sampler_from_name(sampler_type): - default_value = 1.0 - sampler = None - +def _get_sampler_from_name(sampler_type, sampler_arg): + if sampler_type == ALWAYS_ON_SAMPLER: + return ALWAYS_ON + if sampler_type == ALWAYS_OFF_SAMPLER: + return ALWAYS_OFF + if sampler_type == TRACE_ID_RATIO_SAMPLER: + ratio = float(sampler_arg) if sampler_arg is not None else 1.0 + return TraceIdRatioBased(ratio) if sampler_type == PARENT_BASED_ALWAYS_OFF_SAMPLER: - sampler = ParentBased(ALWAYS_OFF) - elif sampler_type == PARENT_BASED_ALWAYS_ON_SAMPLER: - sampler = ParentBased(ALWAYS_ON) - elif sampler_type == ALWAYS_ON_SAMPLER: - sampler = ALWAYS_ON - elif sampler_type == ALWAYS_OFF_SAMPLER: - sampler = ALWAYS_OFF - elif sampler_type in (TRACE_ID_RATIO_SAMPLER, PARENT_BASED_TRACE_ID_RATIO_SAMPLER): - sampler_arg = environ.get(OTEL_TRACES_SAMPLER_ARG) - sampler_value = default_value - if sampler_arg is not None: - try: - sampler_value = float(sampler_arg) - except ValueError: - _logger.error( - _INVALID_FLOAT_MESSAGE, - OTEL_TRACES_SAMPLER_ARG, - default_value, - ) - sampler_value = default_value - - if sampler_value < 0.0 or sampler_value > 1.0: - _logger.error( - "Invalid sampler argument for %s. It should be a value between 0 and 1. Defaulting to %s.", - sampler_type, - default_value, - ) - sampler_value = default_value - else: - _logger.info("Using sampling value: %s", sampler_value) - - if sampler_type == TRACE_ID_RATIO_SAMPLER: - sampler = TraceIdRatioBased(sampler_value) - else: - sampler = ParentBased(TraceIdRatioBased(sampler_value)) - - return sampler if sampler is not None else ParentBased(ALWAYS_ON) + return ParentBased(ALWAYS_OFF) + if sampler_type == PARENT_BASED_TRACE_ID_RATIO_SAMPLER: + ratio = float(sampler_arg) if sampler_arg is not None else 1.0 + return ParentBased(TraceIdRatioBased(ratio)) + return ParentBased(ALWAYS_ON) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/test_configure.py b/sdk/monitor/azure-monitor-opentelemetry/tests/test_configure.py index 12d9f7d5af1e..370394a4747d 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/tests/test_configure.py +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/test_configure.py @@ -25,13 +25,6 @@ _setup_tracing, configure_azure_monitor, ) - -from azure.monitor.opentelemetry._constants import ( - SAMPLER_TYPE, - FIXED_PERCENTAGE_SAMPLER, - RATE_LIMITED_SAMPLER, -) - from azure.monitor.opentelemetry._diagnostics.diagnostic_logging import _DISTRO_DETECTS_ATTACH @@ -385,7 +378,6 @@ def test_setup_tracing( "azure.core.tracing.ext.opentelemetry_span": Mock(OpenTelemetrySpan=opentelemetry_span_mock), }, ): - configurations[SAMPLER_TYPE] = FIXED_PERCENTAGE_SAMPLER _setup_tracing(configurations) sampler_mock.assert_called_once_with(sampling_ratio=0.5) tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) @@ -459,9 +451,8 @@ def test_setup_tracing_rate_limited_sampler( "azure.core.tracing.ext.opentelemetry_span": Mock(OpenTelemetrySpan=opentelemetry_span_mock), }, ): - configurations[SAMPLER_TYPE] = RATE_LIMITED_SAMPLER _setup_tracing(configurations) - sampler_mock.assert_called_once_with(traces_per_second=2.0) + sampler_mock.assert_called_once_with(target_spans_per_second_limit=2.0) tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) set_tracer_provider_mock.assert_called_once_with(tp_init_mock) trace_exporter_mock.assert_called_once_with(**configurations) @@ -533,7 +524,6 @@ def test_setup_tracing_perf_counters_disabled( "azure.core.tracing.ext.opentelemetry_span": Mock(OpenTelemetrySpan=opentelemetry_span_mock), }, ): - configurations[SAMPLER_TYPE] = FIXED_PERCENTAGE_SAMPLER _setup_tracing(configurations) sampler_mock.assert_called_once_with(sampling_ratio=0.5) tp_mock.assert_called_once_with(sampler=sampler_init_mock, resource=TEST_RESOURCE) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py index 8eca1ecb466b..81caeeda6faf 100644 --- a/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py @@ -19,6 +19,7 @@ from opentelemetry.sdk.environment_variables import ( OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, + OTEL_TRACES_SAMPLER_ARG, OTEL_TRACES_SAMPLER, ) from opentelemetry.instrumentation.environment_variables import ( @@ -47,7 +48,6 @@ PARENT_BASED_ALWAYS_ON_SAMPLER, PARENT_BASED_TRACE_ID_RATIO_SAMPLER, METRIC_READERS_ARG, - SAMPLER_TYPE, ) from azure.monitor.opentelemetry._version import VERSION @@ -98,7 +98,6 @@ def test_get_configurations(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_MERGED_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "azure_app_service,azure_vm") resource_create_mock.assert_called_once_with(TEST_CUSTOM_RESOURCE.attributes) - self.assertEqual(configurations["sampler_type"], "microsoft.fixed_percentage") self.assertEqual(configurations["sampling_ratio"], 0.5) self.assertEqual(configurations["credential"], "test_credential") self.assertEqual( @@ -151,7 +150,7 @@ def test_get_configurations_defaults(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "azure_app_service,azure_vm") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") + self.assertEqual(configurations["traces_per_second"], 5.0) self.assertTrue("credential" not in configurations) self.assertTrue("storage_directory" not in configurations) self.assertEqual(configurations["enable_live_metrics"], True) @@ -167,6 +166,7 @@ def test_get_configurations_defaults(self, resource_create_mock): "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", + OTEL_TRACES_SAMPLER_ARG: "0.5", OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", @@ -198,12 +198,13 @@ def test_get_configurations_env_vars(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") + self.assertEqual(configurations["traces_per_second"], 5.0) @patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: FIXED_PERCENTAGE_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: "Half", OTEL_TRACES_EXPORTER: "False", OTEL_LOGS_EXPORTER: "no", OTEL_METRICS_EXPORTER: "True", @@ -217,12 +218,13 @@ def test_get_configurations_env_vars_validation(self, resource_create_mock): self.assertEqual(configurations["disable_logging"], False) self.assertEqual(configurations["disable_metrics"], False) self.assertEqual(configurations["disable_tracing"], False) - self.assertEqual(configurations["sampler_type"], "microsoft.fixed_percentage") + self.assertEqual(configurations["sampling_ratio"], 1.0) @patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "microsoft.fixed.percentage", + OTEL_TRACES_SAMPLER_ARG: "10.45", OTEL_TRACES_EXPORTER: "False", OTEL_LOGS_EXPORTER: "no", OTEL_METRICS_EXPORTER: "True", @@ -236,7 +238,7 @@ def test_get_configurations_env_vars_validation_check_backward_compatibility(sel self.assertEqual(configurations["disable_logging"], False) self.assertEqual(configurations["disable_metrics"], False) self.assertEqual(configurations["disable_tracing"], False) - self.assertEqual(configurations[SAMPLER_TYPE], "microsoft.fixed.percentage") + self.assertEqual(configurations["sampling_ratio"], 1.0) @patch.dict( "os.environ", @@ -425,7 +427,8 @@ def test_get_configurations_logging_format_no_env_var(self): "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", - OTEL_TRACES_SAMPLER: "always_offk", # cSpell: ignore offk + OTEL_TRACES_SAMPLER: RATE_LIMITED_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: "0.5", OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", @@ -434,7 +437,7 @@ def test_get_configurations_logging_format_no_env_var(self): clear=True, ) @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) - def test_get_configurations_env_vars_always_off_incorrect_val(self, resource_create_mock): + def test_get_configurations_env_vars_rate_limited(self, resource_create_mock): configurations = _get_configurations() self.assertTrue("connection_string" not in configurations) @@ -457,12 +460,40 @@ def test_get_configurations_env_vars_always_off_incorrect_val(self, resource_cre self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") + self.assertEqual(configurations["traces_per_second"], 0.5) + + @patch.dict("os.environ", {}, clear=True) + @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) + def test_get_configurations_rate_limited_sampler_param(self, resource_create_mock): + configurations = _get_configurations(traces_per_second=2.5) + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["disable_logging"], False) + self.assertEqual(configurations["disable_metrics"], False) + self.assertEqual(configurations["disable_tracing"], False) + self.assertEqual( + configurations["instrumentation_options"], + { + "azure_sdk": {"enabled": True}, + "django": {"enabled": True}, + "fastapi": {"enabled": True}, + "flask": {"enabled": True}, + "psycopg2": {"enabled": True}, + "requests": {"enabled": True}, + "urllib": {"enabled": True}, + "urllib3": {"enabled": True}, + }, + ) + self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "azure_app_service,azure_vm") + resource_create_mock.assert_called_once_with() + self.assertEqual(configurations["traces_per_second"], 2.5) @patch.dict( "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", + OTEL_TRACES_SAMPLER_ARG: "34", OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", @@ -494,13 +525,52 @@ def test_get_configurations_env_vars_no_preference(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") + self.assertEqual(configurations["traces_per_second"], 5.0) + + @patch.dict( + "os.environ", + { + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", + OTEL_TRACES_SAMPLER_ARG: "2 traces per second", + OTEL_TRACES_EXPORTER: "None", + OTEL_LOGS_EXPORTER: "none", + OTEL_METRICS_EXPORTER: "NONE", + OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "custom_resource_detector", + }, + clear=True, + ) + @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) + def test_get_configurations_env_vars_check_default(self, resource_create_mock): + configurations = _get_configurations() + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["disable_logging"], True) + self.assertEqual(configurations["disable_metrics"], True) + self.assertEqual(configurations["disable_tracing"], True) + self.assertEqual( + configurations["instrumentation_options"], + { + "azure_sdk": {"enabled": False}, + "django": {"enabled": True}, + "fastapi": {"enabled": False}, + "flask": {"enabled": False}, + "psycopg2": {"enabled": True}, + "requests": {"enabled": False}, + "urllib": {"enabled": True}, + "urllib3": {"enabled": True}, + }, + ) + self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") + resource_create_mock.assert_called_once_with() + self.assertEqual(configurations["traces_per_second"], 5.0) @patch.dict( "os.environ", { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", OTEL_TRACES_SAMPLER: FIXED_PERCENTAGE_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: "0.9", OTEL_TRACES_EXPORTER: "None", OTEL_LOGS_EXPORTER: "none", OTEL_METRICS_EXPORTER: "NONE", @@ -532,7 +602,7 @@ def test_get_configurations_env_vars_fixed_percentage(self, resource_create_mock self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() - self.assertEqual(configurations["sampler_type"], "microsoft.fixed_percentage") + self.assertEqual(configurations["sampling_ratio"], 0.9) @patch.dict( "os.environ", @@ -570,6 +640,7 @@ def test_get_configurations_env_vars_always_on(self, resource_create_mock): self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) self.assertEqual(environ[OTEL_EXPERIMENTAL_RESOURCE_DETECTORS], "custom_resource_detector") resource_create_mock.assert_called_once_with() + self.assertEqual(configurations["sampling_arg"], 1.0) self.assertEqual(configurations["sampler_type"], "always_on") @patch.dict( @@ -586,6 +657,7 @@ def test_get_configurations_env_vars_always_off(self, resource_create_mock): self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 0.0) self.assertEqual(configurations["sampler_type"], "always_off") @patch.dict( @@ -593,6 +665,25 @@ def test_get_configurations_env_vars_always_off(self, resource_create_mock): { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", OTEL_TRACES_SAMPLER: TRACE_ID_RATIO_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: "3.5", + }, + clear=True, + ) + @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) + def test_get_configurations_env_vars_trace_id_ratio_incorrect_value(self, resource_create_mock): + configurations = _get_configurations() + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 1.0) + self.assertEqual(configurations["sampler_type"], "trace_id_ratio") + + @patch.dict( + "os.environ", + { + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", + OTEL_TRACES_SAMPLER: TRACE_ID_RATIO_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: ".75", }, clear=True, ) @@ -602,6 +693,25 @@ def test_get_configurations_env_vars_trace_id_ratio(self, resource_create_mock): self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 0.75) + self.assertEqual(configurations["sampler_type"], "trace_id_ratio") + + @patch.dict( + "os.environ", + { + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", + OTEL_TRACES_SAMPLER: TRACE_ID_RATIO_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: "sampler", + }, + clear=True, + ) + @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) + def test_get_configurations_env_vars_trace_id_ratio_non_numeric_value(self, resource_create_mock): + configurations = _get_configurations() + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 1.0) self.assertEqual(configurations["sampler_type"], "trace_id_ratio") @patch.dict( @@ -618,6 +728,7 @@ def test_get_configurations_env_vars_parentbased_always_on(self, resource_create self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 1.0) self.assertEqual(configurations["sampler_type"], "parentbased_always_on") @patch.dict( @@ -634,6 +745,7 @@ def test_get_configurations_env_vars_parentbased_always_off(self, resource_creat self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 0.0) self.assertEqual(configurations["sampler_type"], "parentbased_always_off") @patch.dict( @@ -641,6 +753,7 @@ def test_get_configurations_env_vars_parentbased_always_off(self, resource_creat { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", OTEL_TRACES_SAMPLER: PARENT_BASED_TRACE_ID_RATIO_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: "0.89", }, clear=True, ) @@ -650,6 +763,7 @@ def test_get_configurations_env_vars_parentbased_trace_id_ratio(self, resource_c self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 0.89) self.assertEqual(configurations["sampler_type"], "parentbased_trace_id_ratio") @patch.dict( @@ -657,9 +771,56 @@ def test_get_configurations_env_vars_parentbased_trace_id_ratio(self, resource_c { OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", OTEL_TRACES_SAMPLER: PARENT_BASED_TRACE_ID_RATIO_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: "9.45", }, clear=True, ) + @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) + def test_get_configurations_env_vars_parentbased_trace_id_ratio_with_out_of_bounds_value( + self, resource_create_mock + ): + configurations = _get_configurations() + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 1.0) + self.assertEqual(configurations["sampler_type"], "parentbased_trace_id_ratio") + + @patch.dict( + "os.environ", + { + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", + OTEL_TRACES_SAMPLER: PARENT_BASED_TRACE_ID_RATIO_SAMPLER, + OTEL_TRACES_SAMPLER_ARG: "non-numeric-value", + }, + clear=True, + ) + @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) + def test_get_configurations_env_vars_parentbased_trace_id_ratio_non_numeric_value(self, resource_create_mock): + configurations = _get_configurations() + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 1.0) + self.assertEqual(configurations["sampler_type"], "parentbased_trace_id_ratio") + + @patch.dict( + "os.environ", + { + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask,requests,fastapi,azure_sdk", + OTEL_TRACES_SAMPLER: PARENT_BASED_TRACE_ID_RATIO_SAMPLER, + }, + clear=True, + ) + @patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE) + def test_get_configurations_env_vars_parentbased_trace_id_ratio_no_sampler_argument(self, resource_create_mock): + configurations = _get_configurations() + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) + self.assertEqual(configurations["sampling_arg"], 1.0) + self.assertEqual(configurations["sampler_type"], "parentbased_trace_id_ratio") + @patch.dict( "os.environ", { @@ -673,24 +834,26 @@ def test_get_configurations_env_vars_no_sampling_env_set(self, resource_create_m self.assertTrue("connection_string" not in configurations) self.assertEqual(configurations["resource"].attributes, TEST_DEFAULT_RESOURCE.attributes) - self.assertEqual(configurations["sampler_type"], "microsoft.rate_limited") + self.assertEqual(configurations["traces_per_second"], 5.0) # Tests for the _get_sampler_from_name function def test_get_sampler_from_name_always_on_off(self): - self.assertIs(_get_sampler_from_name(ALWAYS_ON_SAMPLER), ALWAYS_ON) - self.assertIs(_get_sampler_from_name(ALWAYS_OFF_SAMPLER), ALWAYS_OFF) + self.assertIs(_get_sampler_from_name(ALWAYS_ON_SAMPLER, None), ALWAYS_ON) + self.assertIs(_get_sampler_from_name(ALWAYS_OFF_SAMPLER, None), ALWAYS_OFF) def test_get_sampler_from_name_trace_id_ratio(self): - sampler = _get_sampler_from_name(TRACE_ID_RATIO_SAMPLER) + sampler = _get_sampler_from_name(TRACE_ID_RATIO_SAMPLER, "0.3") self.assertIsInstance(sampler, TraceIdRatioBased) + self.assertEqual(sampler._rate, 0.3) def test_get_sampler_from_name_trace_id_ratio_defaults_to_one(self): - sampler = _get_sampler_from_name(TRACE_ID_RATIO_SAMPLER) + sampler = _get_sampler_from_name(TRACE_ID_RATIO_SAMPLER, None) self.assertIsInstance(sampler, TraceIdRatioBased) + self.assertEqual(sampler._rate, 1.0) def test_get_sampler_from_name_parent_based_fixed(self): - sampler_on = _get_sampler_from_name(PARENT_BASED_ALWAYS_ON_SAMPLER) - sampler_off = _get_sampler_from_name(PARENT_BASED_ALWAYS_OFF_SAMPLER) + sampler_on = _get_sampler_from_name(PARENT_BASED_ALWAYS_ON_SAMPLER, None) + sampler_off = _get_sampler_from_name(PARENT_BASED_ALWAYS_OFF_SAMPLER, None) self.assertIsInstance(sampler_on, ParentBased) self.assertIs(sampler_on._root, ALWAYS_ON) @@ -699,15 +862,17 @@ def test_get_sampler_from_name_parent_based_fixed(self): self.assertIs(sampler_off._root, ALWAYS_OFF) def test_get_sampler_from_name_parent_based_trace_id_ratio(self): - sampler = _get_sampler_from_name(PARENT_BASED_TRACE_ID_RATIO_SAMPLER) + sampler = _get_sampler_from_name(PARENT_BASED_TRACE_ID_RATIO_SAMPLER, "0.25") self.assertIsInstance(sampler, ParentBased) self.assertIsInstance(sampler._root, TraceIdRatioBased) + self.assertEqual(sampler._root._rate, 0.25) def test_get_sampler_from_name_parent_based_trace_id_ratio_defaults(self): - sampler = _get_sampler_from_name(PARENT_BASED_TRACE_ID_RATIO_SAMPLER) + sampler = _get_sampler_from_name(PARENT_BASED_TRACE_ID_RATIO_SAMPLER, None) self.assertIsInstance(sampler._root, TraceIdRatioBased) + self.assertEqual(sampler._root._rate, 1.0) def test_get_sampler_from_name_invalid_type_defaults_parentbased_always_on(self): - sampler = _get_sampler_from_name("not-a-sampler") + sampler = _get_sampler_from_name("not-a-sampler", None) self.assertIsInstance(sampler, ParentBased) self.assertIs(sampler._root, ALWAYS_ON) From c604e70c26ad3d02c39375d565c861ac4283895b Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 24 Mar 2026 14:56:40 -0700 Subject: [PATCH 17/21] Add CHANGELOG --- sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md index a304408ab63c..686a9943c902 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md @@ -9,6 +9,8 @@ ### Bugs Fixed ### Other Changes +- Move sampler argument extraction logic from distro to respective samplers + ([#45849](https://github.com/Azure/azure-sdk-for-python/pull/45849)) ## 1.0.0b49 (2026-03-19) From ebdb7318c0f7e972967c701d6d3abdbe60ccae6b Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 24 Mar 2026 16:12:33 -0700 Subject: [PATCH 18/21] Address copilot feedback --- .../tests/trace/test_rate_limited_sampling.py | 2 +- .../tests/trace/test_sampling.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py index 5b06e116e19c..52c8a4f1b2d2 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py @@ -174,7 +174,7 @@ def test_sampler_creation(self): self.assertEqual(sampler.get_description(), f"RateLimitedSampler{{{target_rate}}}") # Test that negative explicit traces per second logs error and defaults to 5.0 - def test_negative_rate_raises_error(self): + def test_negative_rate_defaults_to_5(self): sampler = RateLimitedSampler(-1.0) self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py index 581e4d25ee6c..bffb692af6cb 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py @@ -11,16 +11,19 @@ # pylint: disable=protected-access class TestApplicationInsightsSampler(unittest.TestCase): + @mock.patch.dict("os.environ", {}, clear=True) def test_constructor(self): sampler = ApplicationInsightsSampler() self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100) + @mock.patch.dict("os.environ", {}, clear=True) def test_constructor_ratio(self): sampler = ApplicationInsightsSampler(0.75) self.assertEqual(sampler._ratio, 0.75) self.assertEqual(sampler._sample_rate, 75) + @mock.patch.dict("os.environ", {}, clear=True) def test_invalid_ratio(self): # Invalid explicit ratio logs an error and defaults to 1.0 instead of raising sampler = ApplicationInsightsSampler(1.01) @@ -30,12 +33,14 @@ def test_invalid_ratio(self): self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0) + @mock.patch.dict("os.environ", {}, clear=True) def test_invalid_type_ratio_defaults(self): # Non-numeric explicit ratio logs an error and defaults to 1.0 sampler = ApplicationInsightsSampler({}) # type: ignore self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0) + @mock.patch.dict("os.environ", {}, clear=True) def test_user_passed_value_through_distro(self): sampler = ApplicationInsightsSampler(sampling_ratio=0.5) self.assertEqual(sampler._ratio, 0.5) @@ -84,14 +89,17 @@ def test_should_sample_not_sampled(self, score_mock): self.assertEqual(result.attributes["_MS.sampleRate"], 50) self.assertFalse(result.decision.is_sampled()) + @mock.patch.dict("os.environ", {}, clear=True) def test_sampler_factory(self): sampler = azure_monitor_opentelemetry_sampler_factory("1.0") self.assertEqual(sampler._ratio, 1.0) + @mock.patch.dict("os.environ", {}, clear=True) def test_sampler_factory_none(self): sampler = azure_monitor_opentelemetry_sampler_factory(None) self.assertEqual(sampler._ratio, 1.0) + @mock.patch.dict("os.environ", {}, clear=True) def test_sampler_factory_empty(self): sampler = azure_monitor_opentelemetry_sampler_factory("") self.assertEqual(sampler._ratio, 1.0) From 0e9f4ef442b608e45df0a8065db65f7f8dcb36c6 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 24 Mar 2026 17:14:06 -0700 Subject: [PATCH 19/21] Account for infinite values --- .../export/trace/_rate_limited_sampling.py | 7 ++-- .../exporter/export/trace/_sampling.py | 8 +++-- .../tests/trace/test_rate_limited_sampling.py | 29 +++++++++++++++ .../tests/trace/test_sampling.py | 35 +++++++++++++++++++ 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py index aad78a00e728..21a499ee6963 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py @@ -99,7 +99,8 @@ def __init__(self, traces_per_second: Optional[float] = None): default_traces_per_second = 5.0 if traces_per_second is not None: try: - if traces_per_second < 0.0: + traces_per_second = float(traces_per_second) + if not math.isfinite(traces_per_second) or traces_per_second < 0.0: _logger.error( _INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE, traces_per_second, default_traces_per_second ) @@ -113,7 +114,7 @@ def __init__(self, traces_per_second: Optional[float] = None): sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) try: sampler_value = float(sampling_arg) if sampling_arg is not None else default_traces_per_second - if sampler_value < 0.0: + if not math.isfinite(sampler_value) or sampler_value < 0.0: _logger.error( _INVALID_TRACES_PER_SECOND_MESSAGE_NEGATIVE_VALUE, sampler_value, default_traces_per_second ) @@ -121,7 +122,7 @@ def __init__(self, traces_per_second: Optional[float] = None): else: _logger.info("Using rate limited sampler: %s traces per second", sampler_value) traces_per_second = sampler_value - except ValueError as e: # pylint: disable=unused-variable + except ValueError: _logger.error( # pylint: disable=C0301 _INVALID_TRACES_PER_SECOND_MESSAGE, sampling_arg, diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py index 0416a01f8e7d..b15e310c36df 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import math import os from typing import Optional, Sequence from logging import getLogger @@ -41,7 +42,8 @@ def __init__(self, sampling_ratio: Optional[float] = None): default_sampling_ratio = 1.0 if sampling_ratio is not None: try: - if sampling_ratio < 0.0 or sampling_ratio > 1.0: + sampling_ratio = float(sampling_ratio) + if not math.isfinite(sampling_ratio) or sampling_ratio < 0.0 or sampling_ratio > 1.0: _logger.error( "Invalid value '%s' for sampling ratio. " "Sampling ratio must be in the range [0.0, 1.0]. " @@ -59,7 +61,7 @@ def __init__(self, sampling_ratio: Optional[float] = None): sampling_arg = os.environ.get(OTEL_TRACES_SAMPLER_ARG) try: sampler_value = float(sampling_arg) if sampling_arg is not None else default_sampling_ratio - if sampler_value < 0.0 or sampler_value > 1.0: + if not math.isfinite(sampler_value) or sampler_value < 0.0 or sampler_value > 1.0: _logger.error( "Invalid value '%s' for OTEL_TRACES_SAMPLER_ARG. " "It should be a value between 0 and 1. Defaulting to %s.", @@ -70,7 +72,7 @@ def __init__(self, sampling_ratio: Optional[float] = None): else: _logger.info("Using sampling ratio: %s", sampler_value) sampling_ratio = sampler_value - except ValueError as e: # pylint: disable=unused-variable + except ValueError: _logger.error( # pylint: disable=C _INVALID_FLOAT_MESSAGE, OTEL_TRACES_SAMPLER_ARG, diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py index 52c8a4f1b2d2..d6891602beea 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py @@ -214,6 +214,35 @@ def test_constructor_env_var_invalid_float(self): sampler = RateLimitedSampler() self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + # Test that infinite explicit traces per second logs error and defaults to 5.0 + def test_infinite_traces_per_second_explicit(self): + with patch.dict("os.environ", {}, clear=True): + sampler = RateLimitedSampler(float('inf')) + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + sampler = RateLimitedSampler(float('-inf')) + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + + # Test infinite value from env var falls back to 5.0 + def test_infinite_traces_per_second_env_var(self): + with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "inf"}): + sampler = RateLimitedSampler() + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "-inf"}): + sampler = RateLimitedSampler() + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + + # Test that NaN explicit traces per second logs error and defaults to 5.0 + def test_nan_traces_per_second_explicit(self): + with patch.dict("os.environ", {}, clear=True): + sampler = RateLimitedSampler(float('nan')) + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + + # Test NaN value from env var falls back to 5.0 + def test_nan_traces_per_second_env_var(self): + with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "nan"}): + sampler = RateLimitedSampler() + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + # Test sampling behavior with zero target rate def test_zero_rate_sampling(self): sampler = RateLimitedSampler(0.0) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py index bffb692af6cb..d94cdbc98603 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py @@ -73,6 +73,41 @@ def test_constructor_sampler_arg_invalid_float(self): self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0) + @mock.patch.dict("os.environ", {}, clear=True) + def test_infinite_ratio_explicit(self): + # Infinite explicit ratio logs an error and defaults to 1.0 + sampler = ApplicationInsightsSampler(float('inf')) + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) + sampler = ApplicationInsightsSampler(float('-inf')) + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) + + def test_infinite_ratio_env_var(self): + # Infinite value from env var falls back to 1.0 + with mock.patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "inf"}): + sampler = ApplicationInsightsSampler() + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) + with mock.patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "-inf"}): + sampler = ApplicationInsightsSampler() + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) + + @mock.patch.dict("os.environ", {}, clear=True) + def test_nan_ratio_explicit(self): + # NaN explicit ratio logs an error and defaults to 1.0 + sampler = ApplicationInsightsSampler(float('nan')) + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) + + def test_nan_ratio_env_var(self): + # NaN value from env var falls back to 1.0 + with mock.patch.dict("os.environ", {"OTEL_TRACES_SAMPLER_ARG": "nan"}): + sampler = ApplicationInsightsSampler() + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) + @mock.patch("azure.monitor.opentelemetry.exporter.export.trace._sampling._get_DJB2_sample_score") def test_should_sample(self, score_mock): sampler = ApplicationInsightsSampler(0.75) From 58289146824fcd627b8652cffa38406183f6ffba Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 25 Mar 2026 10:29:41 -0700 Subject: [PATCH 20/21] Fix the value error check --- .../exporter/export/trace/_rate_limited_sampling.py | 2 +- .../opentelemetry/exporter/export/trace/_sampling.py | 4 ++-- .../tests/trace/test_rate_limited_sampling.py | 6 ++++++ .../tests/trace/test_sampling.py | 7 +++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py index 21a499ee6963..a772a59cd1db 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_rate_limited_sampling.py @@ -107,7 +107,7 @@ def __init__(self, traces_per_second: Optional[float] = None): traces_per_second = default_traces_per_second else: _logger.info("Using rate limited sampler: %s traces per second", traces_per_second) - except TypeError: + except (ValueError, TypeError): _logger.error(_INVALID_TRACES_PER_SECOND_MESSAGE, traces_per_second, default_traces_per_second) traces_per_second = default_traces_per_second else: diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py index b15e310c36df..b707082f91fb 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_sampling.py @@ -52,7 +52,7 @@ def __init__(self, sampling_ratio: Optional[float] = None): default_sampling_ratio, ) sampling_ratio = default_sampling_ratio - except TypeError: + except (ValueError, TypeError): _logger.error( "Invalid value '%s' for sampling ratio. Defaulting to %s.", sampling_ratio, default_sampling_ratio ) @@ -73,7 +73,7 @@ def __init__(self, sampling_ratio: Optional[float] = None): _logger.info("Using sampling ratio: %s", sampler_value) sampling_ratio = sampler_value except ValueError: - _logger.error( # pylint: disable=C + _logger.error( # pylint: disable=C0301 _INVALID_FLOAT_MESSAGE, OTEL_TRACES_SAMPLER_ARG, default_sampling_ratio, diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py index d6891602beea..7b705453d06a 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py @@ -236,6 +236,12 @@ def test_nan_traces_per_second_explicit(self): with patch.dict("os.environ", {}, clear=True): sampler = RateLimitedSampler(float('nan')) self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") + + # Test that non-numeric explicit traces per second logs error and defaults to 5.0 + def test_non_numeric_traces_per_second_explicit(self): + with patch.dict("os.environ", {}, clear=True): + sampler = RateLimitedSampler("def") # type: ignore + self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") # Test NaN value from env var falls back to 5.0 def test_nan_traces_per_second_env_var(self): diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py index d94cdbc98603..0aa10c7543a9 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py @@ -82,6 +82,13 @@ def test_infinite_ratio_explicit(self): sampler = ApplicationInsightsSampler(float('-inf')) self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0) + + @mock.patch.dict("os.environ", {}, clear=True) + def test_invalid_type_ratio_explicit(self): + # Non-numeric explicit ratio logs an error and defaults to 1.0 + sampler = ApplicationInsightsSampler("abc") # type: ignore + self.assertEqual(sampler._ratio, 1.0) + self.assertEqual(sampler._sample_rate, 100.0) def test_infinite_ratio_env_var(self): # Infinite value from env var falls back to 1.0 From f8ecaaa004caa46b43e825b4b246a2256f9cc61a Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 25 Mar 2026 11:22:22 -0700 Subject: [PATCH 21/21] Fix lint --- .../tests/trace/test_rate_limited_sampling.py | 10 +++++----- .../tests/trace/test_sampling.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py index 7b705453d06a..8253f95bf4e8 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_rate_limited_sampling.py @@ -217,9 +217,9 @@ def test_constructor_env_var_invalid_float(self): # Test that infinite explicit traces per second logs error and defaults to 5.0 def test_infinite_traces_per_second_explicit(self): with patch.dict("os.environ", {}, clear=True): - sampler = RateLimitedSampler(float('inf')) + sampler = RateLimitedSampler(float("inf")) self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") - sampler = RateLimitedSampler(float('-inf')) + sampler = RateLimitedSampler(float("-inf")) self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") # Test infinite value from env var falls back to 5.0 @@ -234,13 +234,13 @@ def test_infinite_traces_per_second_env_var(self): # Test that NaN explicit traces per second logs error and defaults to 5.0 def test_nan_traces_per_second_explicit(self): with patch.dict("os.environ", {}, clear=True): - sampler = RateLimitedSampler(float('nan')) + sampler = RateLimitedSampler(float("nan")) self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") - + # Test that non-numeric explicit traces per second logs error and defaults to 5.0 def test_non_numeric_traces_per_second_explicit(self): with patch.dict("os.environ", {}, clear=True): - sampler = RateLimitedSampler("def") # type: ignore + sampler = RateLimitedSampler("def") # type: ignore self.assertEqual(sampler.get_description(), "RateLimitedSampler{5.0}") # Test NaN value from env var falls back to 5.0 diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py index 0aa10c7543a9..eecc815e3341 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_sampling.py @@ -76,17 +76,17 @@ def test_constructor_sampler_arg_invalid_float(self): @mock.patch.dict("os.environ", {}, clear=True) def test_infinite_ratio_explicit(self): # Infinite explicit ratio logs an error and defaults to 1.0 - sampler = ApplicationInsightsSampler(float('inf')) + sampler = ApplicationInsightsSampler(float("inf")) self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0) - sampler = ApplicationInsightsSampler(float('-inf')) + sampler = ApplicationInsightsSampler(float("-inf")) self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0) - + @mock.patch.dict("os.environ", {}, clear=True) def test_invalid_type_ratio_explicit(self): # Non-numeric explicit ratio logs an error and defaults to 1.0 - sampler = ApplicationInsightsSampler("abc") # type: ignore + sampler = ApplicationInsightsSampler("abc") # type: ignore self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0) @@ -104,7 +104,7 @@ def test_infinite_ratio_env_var(self): @mock.patch.dict("os.environ", {}, clear=True) def test_nan_ratio_explicit(self): # NaN explicit ratio logs an error and defaults to 1.0 - sampler = ApplicationInsightsSampler(float('nan')) + sampler = ApplicationInsightsSampler(float("nan")) self.assertEqual(sampler._ratio, 1.0) self.assertEqual(sampler._sample_rate, 100.0)