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 f8baa0a8a05 fix(metrics/otel): bracket IPv6 host literals in exporter
endpoint URL (#66813)
f8baa0a8a05 is described below
commit f8baa0a8a05713efd7bbb886563a7a1b50174d1c
Author: Stefan Wang <[email protected]>
AuthorDate: Tue May 12 14:59:27 2026 -0700
fix(metrics/otel): bracket IPv6 host literals in exporter endpoint URL
(#66813)
get_otel_data_exporter() builds the OTLP exporter endpoint as a raw
f-string. When host is an IPv6 literal (e.g. ::1, 2001:db8::1) the
resulting URL is invalid per RFC 3986 section 3.2.2 — the v6 host needs
to be enclosed in [...] so the colon separators don't conflict with the
host:port delimiter.
Add _format_url_host() that brackets bare v6 literals and leaves
hostnames, IPv4 literals, and already-bracketed v6 strings unchanged.
Closes #66811
Signed-off-by: 1fanwang <[email protected]>
---
.../src/airflow_shared/observability/common.py | 17 +++++++++++-
.../observability/metrics/test_otel_logger.py | 32 ++++++++++++++++++++++
2 files changed, 48 insertions(+), 1 deletion(-)
diff --git a/shared/observability/src/airflow_shared/observability/common.py
b/shared/observability/src/airflow_shared/observability/common.py
index c611782daa0..e912664b359 100644
--- a/shared/observability/src/airflow_shared/observability/common.py
+++ b/shared/observability/src/airflow_shared/observability/common.py
@@ -30,6 +30,21 @@ if TYPE_CHECKING:
log = structlog.getLogger(__name__)
+def _format_url_host(host: str | None) -> str | None:
+ """
+ Bracket IPv6 host literals for embedding in a URL authority.
+
+ Per RFC 3986 §3.2.2, IPv6 hosts in a URI must be enclosed in square
+ brackets so the ``:`` separators do not conflict with the ``host:port``
+ delimiter. Hostnames, IPv4 literals, and already-bracketed v6 literals
+ are returned unchanged. ``None`` is passed through so existing
+ error-logging paths keep their shape.
+ """
+ if host is not None and ":" in host and not host.startswith("["):
+ return f"[{host}]"
+ return host
+
+
def get_otel_data_exporter(
*,
otel_env_config: OtelEnvConfig,
@@ -106,7 +121,7 @@ def get_otel_data_exporter(
endpoint_suffix = "traces" if otel_env_config.data_type ==
OtelDataType.TRACES else "metrics"
- endpoint_str = f"{protocol}://{host}:{port}/v1/{endpoint_suffix}"
+ endpoint_str =
f"{protocol}://{_format_url_host(host)}:{port}/v1/{endpoint_suffix}"
if otel_env_config.data_type == OtelDataType.TRACES:
exporter = OTLPSpanExporter(endpoint=endpoint_str)
else:
diff --git
a/shared/observability/tests/observability/metrics/test_otel_logger.py
b/shared/observability/tests/observability/metrics/test_otel_logger.py
index 4bc889d4f13..f4c4cd369bf 100644
--- a/shared/observability/tests/observability/metrics/test_otel_logger.py
+++ b/shared/observability/tests/observability/metrics/test_otel_logger.py
@@ -394,6 +394,38 @@ class TestOtelMetrics:
"grpc",
id="type_specific_vars_take_precedence",
),
+ pytest.param(
+ {},
+ "::1",
+ "4318",
+ "http://[::1]:4318/v1/metrics",
+ "http",
+ id="airflow_config_ipv6_loopback_is_bracketed",
+ ),
+ pytest.param(
+ {},
+ "2001:db8::1",
+ "4318",
+ "http://[2001:db8::1]:4318/v1/metrics",
+ "http",
+ id="airflow_config_ipv6_literal_is_bracketed",
+ ),
+ pytest.param(
+ {},
+ "[::1]",
+ "4318",
+ "http://[::1]:4318/v1/metrics",
+ "http",
+ id="airflow_config_already_bracketed_ipv6_is_preserved",
+ ),
+ pytest.param(
+ {},
+ "10.0.0.1",
+ "4318",
+ "http://10.0.0.1:4318/v1/metrics",
+ "http",
+ id="airflow_config_ipv4_literal_passes_through_unchanged",
+ ),
],
)
def test_config_priorities(