This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new f558ff13273 Stop discarding a MeterProvider configured via 
OTEL_CONFIG_FILE (#71529)
f558ff13273 is described below

commit f558ff13273c6fd6a81ca592a70b8c3761b748cd
Author: Stefan Wang <[email protected]>
AuthorDate: Tue Aug 18 12:12:44 2026 -0700

    Stop discarding a MeterProvider configured via OTEL_CONFIG_FILE (#71529)
    
    The OpenTelemetry declarative configuration spec makes OTEL_CONFIG_FILE the
    sole source of SDK construction. Airflow force-resets the SDK's once-guard 
and
    installs its own MeterProvider unconditionally, so a deployment running 
under
    opentelemetry-instrument with a config file loses every view, reader and
    exporter it declared, with no warning.
    
    The once-guard reset exists for the post-fork case in #64690 and stays in 
place
    for every path where Airflow owns the provider.
    
    Signed-off-by: 1fanwang <[email protected]>
    Co-authored-by: Copilot App <[email protected]>
---
 .../observability/metrics/otel_logger.py           | 23 +++++++--
 .../observability/metrics/test_otel_logger.py      | 59 +++++++++++++++++++++-
 2 files changed, 78 insertions(+), 4 deletions(-)

diff --git 
a/shared/observability/src/airflow_shared/observability/metrics/otel_logger.py 
b/shared/observability/src/airflow_shared/observability/metrics/otel_logger.py
index 9be7103a62b..bc8b819f144 100644
--- 
a/shared/observability/src/airflow_shared/observability/metrics/otel_logger.py
+++ 
b/shared/observability/src/airflow_shared/observability/metrics/otel_logger.py
@@ -19,6 +19,7 @@ from __future__ import annotations
 import atexit
 import datetime
 import logging
+import os
 import random
 import warnings
 from collections.abc import Callable
@@ -76,6 +77,11 @@ DEFAULT_METRIC_NAME_PREFIX = "airflow"
 # Delimiter is placed between the universal metric prefix and the unique 
metric name.
 DEFAULT_METRIC_NAME_DELIMITER = "."
 
+# Not imported from ``opentelemetry.sdk.environment_variables``: the constant 
postdates the
+# ``opentelemetry-api>=1.27.0`` floor this distribution declares, while the 
name is spec-fixed.
+# 
https://opentelemetry.io/docs/specs/otel/configuration/sdk/#declarative-configuration
+OTEL_CONFIG_FILE = "OTEL_CONFIG_FILE"
+
 
 def full_name(name: str, *, prefix: str = DEFAULT_METRIC_NAME_PREFIX) -> str:
     """Assembles the prefix, delimiter, and name and returns it as a string."""
@@ -457,11 +463,24 @@ def get_otel_logger(
     so that bucket boundaries adapt automatically to the observed data range.  
This avoids
     the need to hand-tune explicit bucket boundaries for metrics that span 
very different
     scales (milliseconds to hours).
+
+    A ``MeterProvider`` already built from ``OTEL_CONFIG_FILE`` is used as-is: 
the declarative
+    configuration spec makes that file the sole source of SDK construction.
     """
+    effective_prefix: str = prefix or DEFAULT_METRIC_NAME_PREFIX
+    validator = get_validator(metrics_allow_list, metrics_block_list)
+
+    configured_provider = metrics.get_meter_provider()
+    if os.environ.get(OTEL_CONFIG_FILE) and isinstance(configured_provider, 
MeterProvider):
+        log.info("%s is set; using the MeterProvider it built.", 
OTEL_CONFIG_FILE)
+        atexit_register_metrics_flush()
+        return SafeOtelLogger(
+            configured_provider, effective_prefix, validator, 
stat_name_handler, statsd_influxdb_enabled
+        )
+
     otel_env_config = load_metrics_env_config()
 
     effective_service_name: str = otel_env_config.service_name or service_name 
or "airflow"
-    effective_prefix: str = prefix or DEFAULT_METRIC_NAME_PREFIX
     resource = Resource.create(attributes={SERVICE_NAME: 
effective_service_name})
 
     # 
https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#periodic-exporting-metricreader
@@ -524,8 +543,6 @@ def get_otel_logger(
     # Register a hook that flushes any in-memory metrics at shutdown.
     atexit_register_metrics_flush()
 
-    validator = get_validator(metrics_allow_list, metrics_block_list)
-
     return SafeOtelLogger(
         metrics.get_meter_provider(), effective_prefix, validator, 
stat_name_handler, statsd_influxdb_enabled
     )
diff --git 
a/shared/observability/tests/observability/metrics/test_otel_logger.py 
b/shared/observability/tests/observability/metrics/test_otel_logger.py
index f1e90e53003..cc821771372 100644
--- a/shared/observability/tests/observability/metrics/test_otel_logger.py
+++ b/shared/observability/tests/observability/metrics/test_otel_logger.py
@@ -24,8 +24,14 @@ import time
 from unittest import mock
 
 import pytest
+from opentelemetry import metrics
 from opentelemetry.metrics import MeterProvider
-from opentelemetry.sdk.metrics.view import 
ExponentialBucketHistogramAggregation, View
+from opentelemetry.sdk.metrics import MeterProvider as SDKMeterProvider
+from opentelemetry.sdk.metrics.view import (
+    ExplicitBucketHistogramAggregation,
+    ExponentialBucketHistogramAggregation,
+    View,
+)
 
 from airflow_shared.observability.common import get_otel_data_exporter
 from airflow_shared.observability.metrics.otel_logger import (
@@ -61,6 +67,26 @@ def name():
     return "test_stats_run"
 
 
[email protected]
+def reset_meter_provider():
+    """Let a test install its own global MeterProvider, then restore the 
previous one.
+
+    ``set_meter_provider`` is guarded by a process-wide ``Once``, so tests 
that install a
+    provider have to clear it the same way ``get_otel_logger`` does after a 
fork.
+    """
+    import opentelemetry.metrics._internal as metrics_internal
+
+    def clear() -> None:
+        metrics_internal._METER_PROVIDER_SET_ONCE._done = False
+        metrics_internal._METER_PROVIDER = None
+
+    previous = metrics_internal._METER_PROVIDER
+    clear()
+    yield
+    clear()
+    metrics_internal._METER_PROVIDER = previous
+
+
 class TestOtelMetrics:
     def setup_method(self):
         self.meter = mock.Mock(MeterProvider)
@@ -502,6 +528,37 @@ class TestOtelMetrics:
         assert isinstance(view, View)
         assert isinstance(view._aggregation, 
ExponentialBucketHistogramAggregation)
 
+    def test_declaratively_configured_provider_is_not_replaced(self, 
reset_meter_provider):
+        """A MeterProvider built from OTEL_CONFIG_FILE must survive 
get_otel_logger().
+
+        The declarative configuration spec makes that file the sole source of 
SDK
+        construction, so replacing its provider silently drops the 
deployment's views.
+        See https://github.com/apache/airflow/issues/64690 for why the 
provider is
+        otherwise force-replaced.
+        """
+        declarative_view = View(
+            instrument_name="*_duration",
+            aggregation=ExplicitBucketHistogramAggregation(boundaries=(0.5, 1, 
2, 4, 8)),
+        )
+        configured_provider = SDKMeterProvider(views=[declarative_view], 
shutdown_on_exit=False)
+        metrics.set_meter_provider(configured_provider)
+
+        with env_vars({"OTEL_CONFIG_FILE": "/tmp/otel-config.yaml"}):
+            logger = get_otel_logger(host="localhost", port=4318)
+
+        assert logger.otel is configured_provider
+        assert metrics.get_meter_provider() is configured_provider
+        assert list(configured_provider._sdk_config.views) == 
[declarative_view]
+
+    def test_provider_is_replaced_without_declarative_config(self, 
reset_meter_provider):
+        """Without OTEL_CONFIG_FILE, Airflow still installs its own 
provider."""
+        pre_existing = SDKMeterProvider(shutdown_on_exit=False)
+        metrics.set_meter_provider(pre_existing)
+
+        logger = get_otel_logger(host="localhost", port=4318)
+
+        assert logger.otel is not pre_existing
+
     def test_atexit_flush_on_process_exit(self):
         """
         Run a process that initializes a logger, creates a stat and then exits.

Reply via email to