Repository: DataDog/dd-trace-py
Pull request #12662: chore(agent): move agent configs to envier
Author: login: mabdinur
Labels: name: changelog/no-changelog
description: A changelog entry is not required for this PR.
Created: 2025-03-06T21:37:10Z | Merged: 2025-03-11T14:03:03Z

## Checklist
- [x] PR author has checked that all the criteria below are met
- The PR description includes an overview of the change
- The PR description articulates the motivation for the change
- The change includes tests OR the PR description describes a testing strategy
- The PR description notes risks associated with the change, if any
- Newly-added code is easy to change
- The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html)
- The change includes or references documentation updates if necessary
- Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting))

## Reviewer Checklist
- [x] Reviewer has checked that all the criteria below are met 
- Title is accurate
- All changes are related to the pull request's stated goal
- Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes
- Testing strategy adequately addresses listed risks
- Newly-added code is easy to change
- Release note makes sense to a user of the library
- If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment
- Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)



Diff:
diff --git a/ddtrace/internal/agent.py b/ddtrace/internal/agent.py
index d1ee2fa6716..cfbcc4cf383 100644
--- a/ddtrace/internal/agent.py
+++ b/ddtrace/internal/agent.py
@@ -1,11 +1,13 @@
 import json
 import os
 import socket
+from typing import Optional
 from typing import TypeVar
 from typing import Union
 
 from ddtrace.internal.constants import DEFAULT_TIMEOUT
 from ddtrace.internal.logger import get_logger
+from ddtrace.settings._core import DDConfig
 
 from .http import HTTPConnection
 from .http import HTTPSConnection
@@ -26,29 +28,11 @@
 log = get_logger(__name__)
 
 
-# This method returns if a hostname is an IPv6 address
-def is_ipv6_hostname(hostname):
-    # type: (Union[T, str]) -> bool
-    if not isinstance(hostname, str):
-        return False
-    try:
-        socket.inet_pton(socket.AF_INET6, hostname)
-        return True
-    except socket.error:  # not a valid address
-        return False
-
-
-def get_trace_url():
-    # type: () -> str
-    """Return the Agent URL computed from the environment.
-
-    Raises a ``ValueError`` if the URL is not supported by the Agent.
-    """
-    user_supplied_host = os.environ.get("DD_AGENT_HOST", os.environ.get("DD_TRACE_AGENT_HOSTNAME"))
-    user_supplied_port = os.environ.get("DD_AGENT_PORT", os.environ.get("DD_TRACE_AGENT_PORT"))
-
-    url = os.environ.get("DD_TRACE_AGENT_URL")
+def _derive_trace_url(config: "AgentConfig") -> str:
+    user_supplied_host = config._trace_agent_hostname or config._agent_host
+    user_supplied_port = config._trace_agent_port or config._agent_port
 
+    url = config._trace_agent_url
     if not url:
         if user_supplied_host is not None or user_supplied_port is not None:
             host = user_supplied_host or DEFAULT_HOSTNAME
@@ -64,11 +48,10 @@ def get_trace_url():
     return url
 
 
-def get_stats_url():
-    # type: () -> str
-    user_supplied_host = os.environ.get("DD_AGENT_HOST", os.environ.get("DD_DOGSTATSD_HOST"))
-    user_supplied_port = os.getenv("DD_DOGSTATSD_PORT")
-    url = os.getenv("DD_DOGSTATSD_URL")
+def _derive_stats_url(config: "AgentConfig") -> str:
+    user_supplied_host = config._dogstatsd_host or config._agent_host
+    user_supplied_port = config._dogstatsd_port or config._agent_port
+    url = config._dogstatsd_url
 
     if not url:
         if user_supplied_host is not None or user_supplied_port is not None:
@@ -84,9 +67,114 @@ def get_stats_url():
     return url
 
 
+# This method returns if a hostname is an IPv6 address
+def is_ipv6_hostname(hostname):
+    # type: (Union[T, str]) -> bool
+    if not isinstance(hostname, str):
+        return False
+    try:
+        socket.inet_pton(socket.AF_INET6, hostname)
+        return True
+    except socket.error:  # not a valid address
+        return False
+
+
+class AgentConfig(DDConfig):
+    __prefix__ = "dd"
+
+    _trace_agent_hostname = DDConfig.v(
+        Optional[str],
+        "trace_agent_hostname",
+        default=None,
+        help_type="String",
+        help="Legacy configuration, stores the hostname of the trace agent",
+    )
+
+    _trace_agent_port = DDConfig.v(
+        Optional[int],
+        "trace_agent_port",
+        default=None,
+        help_type="Int",
+        help="Legacy configuration, stores the port of the trace agent",
+    )
+
+    _trace_agent_url = DDConfig.v(
+        Optional[str],
+        "trace_agent_url",
+        default=None,
+        help_type="String",
+        help="Stores the URL of the trace agent",
+    )
+
+    trace_agent_timeout_seconds = DDConfig.v(
+        float,
+        "trace_agent_timeout_seconds",
+        default=DEFAULT_TIMEOUT,
+        help_type="Float",
+        help="Stores the timeout in seconds for the trace agent",
+    )
+
+    _dogstatsd_host = DDConfig.v(
+        Optional[str],
+        "dogstatsd_host",
+        default=None,
+        help_type="String",
+        help="Stores the hostname of the agent receiving DogStatsD metrics",
+    )
+
+    _dogstatsd_port = DDConfig.v(
+        Optional[int],
+        "dogstatsd_port",
+        default=None,
+        help_type="Int",
+        help="Stores the port of the agent receiving DogStatsD metrics",
+    )
+
+    _dogstatsd_url = DDConfig.v(
+        Optional[str],
+        "dogstatsd_url",
+        default=None,
+        help_type="String",
+        help="Stores the URL of the DogStatsD agent",
+    )
+
+    _agent_host = DDConfig.v(
+        Optional[str],
+        "agent_host",
+        default=None,
+        help_type="String",
+        help="Stores the hostname of the agent",
+    )
+
+    _agent_port = DDConfig.v(
+        Optional[int],
+        "agent_port",
+        default=None,
+        help_type="Int",
+        help="Stores the port of the agent",
+    )
+    # Effective trace agent URL (this is the one that will be used)
+    trace_agent_url = DDConfig.d(str, _derive_trace_url)
+    # Effective DogStatsD URL (this is the one that will be used)
+    dogstatsd_url = DDConfig.d(str, _derive_stats_url)
+
+
+config = AgentConfig()
+
+
+def get_trace_url() -> str:
+    """Return the Agent URL computed from the environment."""
+    return config.trace_agent_url
+
+
+def get_stats_url() -> str:
+    """Return the DogStatsD URL computed from the environment."""
+    return config.dogstatsd_url
+
+
 def info(url=None):
     agent_url = get_trace_url() if url is None else url
-    timeout = float(os.getenv("DD_TRACE_AGENT_TIMEOUT_SECONDS", DEFAULT_TIMEOUT))
+    timeout = config.trace_agent_timeout_seconds
     _conn = get_connection(agent_url, timeout=timeout)
     try:
         _conn.request("GET", "info", headers={"content-type": "application/json"})
diff --git a/ddtrace/internal/remoteconfig/client.py b/ddtrace/internal/remoteconfig/client.py
index bea2974f518..4b4c393d9c6 100644
--- a/ddtrace/internal/remoteconfig/client.py
+++ b/ddtrace/internal/remoteconfig/client.py
@@ -319,7 +319,7 @@ def _send_request(self, payload: str) -> Optional[Mapping[str, Any]]:
             if config.log_payloads:
                 log.debug("[%s][P: %s] RC request payload: %s", os.getpid(), os.getppid(), payload)  # noqa: G200
 
-            conn = agent.get_connection(self.agent_url, timeout=ddtrace.config._agent_timeout_seconds)
+            conn = agent.get_connection(self.agent_url, timeout=agent.config.trace_agent_timeout_seconds)
             conn.request("POST", REMOTE_CONFIG_AGENT_ENDPOINT, payload, self._headers)
             resp = conn.getresponse()
             data_length = resp.headers.get("Content-Length")
diff --git a/ddtrace/internal/writer/writer.py b/ddtrace/internal/writer/writer.py
index bc03b65bcbd..fc06052fd99 100644
--- a/ddtrace/internal/writer/writer.py
+++ b/ddtrace/internal/writer/writer.py
@@ -22,6 +22,7 @@
 from ...internal.utils.formats import parse_tags_str
 from ...internal.utils.http import Response
 from ...internal.utils.time import StopWatch
+from .. import agent
 from .. import compat
 from .. import periodic
 from .. import service
@@ -155,7 +156,7 @@ def __init__(
         if processing_interval is None:
             processing_interval = config._trace_writer_interval_seconds
         if timeout is None:
-            timeout = config._agent_timeout_seconds
+            timeout = agent.config.trace_agent_timeout_seconds
         super(HTTPWriter, self).__init__(interval=processing_interval)
         self.intake_url = intake_url
         self._buffer_size = buffer_size
@@ -450,7 +451,7 @@ def __init__(
         if processing_interval is None:
             processing_interval = config._trace_writer_interval_seconds
         if timeout is None:
-            timeout = config._agent_timeout_seconds
+            timeout = agent.config.trace_agent_timeout_seconds
         if buffer_size is not None and buffer_size <= 0:
             raise ValueError("Writer buffer size must be positive")
         if max_payload_size is not None and max_payload_size <= 0:
diff --git a/ddtrace/settings/_config.py b/ddtrace/settings/_config.py
index d095e818015..2397dc37b21 100644
--- a/ddtrace/settings/_config.py
+++ b/ddtrace/settings/_config.py
@@ -492,14 +492,15 @@ def __init__(self):
         )
         self._trace_writer_log_err_payload = _get_config("_DD_TRACE_WRITER_LOG_ERROR_PAYLOADS", False, asbool)
 
-        self._trace_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_TRACE_AGENT_HOSTNAME"])
-        self._trace_agent_port = _get_config(["DD_AGENT_PORT", "DD_TRACE_AGENT_PORT"])
+        # TODO: Remove the configurations below. ddtrace.internal.agent.config should be used instead.
         self._trace_agent_url = _get_config("DD_TRACE_AGENT_URL")
-
-        self._stats_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_DOGSTATSD_HOST"])
-        self._stats_agent_port = _get_config("DD_DOGSTATSD_PORT")
-        self._stats_agent_url = _get_config("DD_DOGSTATSD_URL")
         self._agent_timeout_seconds = _get_config("DD_TRACE_AGENT_TIMEOUT_SECONDS", DEFAULT_TIMEOUT, float)
+        # Report Telemetry for Agent Connection Configurations. We need to do this here to avoid circular imports
+        from ddtrace.internal.agent import config as agent_config
+
+        from ._telemetry import report_telemetry
+
+        report_telemetry(agent_config)
 
         self._span_traceback_max_size = _get_config("DD_TRACE_SPAN_TRACEBACK_MAX_SIZE", 30, int)
 
diff --git a/releasenotes/notes/update-agent-tags-200920a48913617c.yaml b/releasenotes/notes/update-agent-tags-200920a48913617c.yaml
new file mode 100644
index 00000000000..be463559411
--- /dev/null
+++ b/releasenotes/notes/update-agent-tags-200920a48913617c.yaml
@@ -0,0 +1,18 @@
+---
+other:
+  - |
+    agent: Migrates agent connection configurations to envier. This provides better documentation and validation for expected types. 
+    The following configuration are affected:
+    - `DD_TRACE_AGENT_URL`
+    - `DD_DOGSTATSD_URL`
+    - `DD_TRACE_AGENT_HOSTNAME`
+    - `DD_AGENT_HOST`
+    - `DD_TRACE_AGENT_PORT`
+    - `DD_DOGSTATSD_PORT`
+    - `DD_AGENT_PORT`
+    - `DD_TRACE_AGENT_TIMEOUT_SECONDS`
+    Of the above impacted configurations, these had a precedence change:
+    - `DD_TRACE_AGENT_HOSTNAME` over `DD_AGENT_HOST`
+    - `DD_TRACE_AGENT_PORT` over `DD_AGENT_PORT`
+    - `DD_DOGSTATSD_HOST` over `DD_AGENT_HOST`
+    - `DD_DOGSTATSD_PORT` over `DD_AGENT_PORT`
diff --git a/tests/telemetry/test_writer.py b/tests/telemetry/test_writer.py
index 94bfa2dc7b3..7ab1906e3d6 100644
--- a/tests/telemetry/test_writer.py
+++ b/tests/telemetry/test_writer.py
@@ -117,6 +117,8 @@ def test_app_started_event(telemetry_writer, test_agent_session, mock_time):
                     {"name": "DD_SPAN_SAMPLING_RULES", "origin": "unknown", "value": None},
                     {"name": "DD_SPAN_SAMPLING_RULES_FILE", "origin": "unknown", "value": None},
                     {"name": "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", "origin": "unknown", "value": True},
+                    {"name": "DD_TRACE_AGENT_HOSTNAME", "origin": "default", "value": None},
+                    {"name": "DD_TRACE_AGENT_PORT", "origin": "default", "value": None},
                     {"name": "DD_TRACE_AGENT_TIMEOUT_SECONDS", "origin": "unknown", "value": 2.0},
                     {"name": "DD_TRACE_API_VERSION", "origin": "unknown", "value": None},
                     {"name": "DD_TRACE_CLIENT_IP_ENABLED", "origin": "unknown", "value": None},
@@ -354,6 +356,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
         {"name": "DD_DJANGO_INCLUDE_USER_LOGIN", "origin": "default", "value": True},
         {"name": "DD_DJANGO_INCLUDE_USER_NAME", "origin": "default", "value": True},
         {"name": "DD_DJANGO_INCLUDE_USER_REALNAME", "origin": "default", "value": False},
+        {"name": "DD_DOGSTATSD_HOST", "origin": "default", "value": None},
         {"name": "DD_DOGSTATSD_PORT", "origin": "default", "value": None},
         {"name": "DD_DOGSTATSD_URL", "origin": "default", "value": None},
         {"name": "DD_DYNAMIC_INSTRUMENTATION_DIAGNOSTICS_INTERVAL", "origin": "default", "value": 3600},
@@ -453,6 +456,8 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
         {"name": "DD_TEST_SESSION_NAME", "origin": "default", "value": None},
         {"name": "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", "origin": "env_var", "value": True},
         {"name": "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED", "origin": "default", "value": False},
+        {"name": "DD_TRACE_AGENT_HOSTNAME", "origin": "default", "value": None},
+        {"name": "DD_TRACE_AGENT_PORT", "origin": "default", "value": None},
         {"name": "DD_TRACE_AGENT_TIMEOUT_SECONDS", "origin": "default", "value": 2.0},
         {"name": "DD_TRACE_API_VERSION", "origin": "env_var", "value": "v0.5"},
         {"name": "DD_TRACE_CLIENT_IP_ENABLED", "origin": "env_var", "value": True},
diff --git a/tests/tracer/test_agent.py b/tests/tracer/test_agent.py
index c3b602919cd..7e2ca9f7b19 100644
--- a/tests/tracer/test_agent.py
+++ b/tests/tracer/test_agent.py
@@ -23,71 +23,113 @@ def test_is_ipv6_hostname(hostname, expected):
     assert agent.is_ipv6_hostname(hostname) == expected
 
 
-@pytest.mark.subprocess(parametrize={"DD_AGENT_HOST": ["host", "2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF"]})
+@pytest.mark.subprocess(
+    parametrize={"DD_AGENT_HOST": ["host", "2001:db8:3333:4444:cccc:dddd:eeee:ffff"]},
+    env={"DD_TRACE_AGENT_HOST": None, "DD_TRACE_AGENT_URL": None, "DD_DOGSTATSD_URL": None, "DD_DOGSTATSD_HOST": None},
+)
 def test_hostname():
     import os
+    from urllib.parse import urlparse
+
+    from ddtrace.internal.agent import config
+
+    assert urlparse(config.trace_agent_url).hostname == os.environ.get("DD_AGENT_HOST")
+    assert urlparse(config.dogstatsd_url).hostname == os.environ.get("DD_AGENT_HOST"), urlparse(config.dogstatsd_url)
 
-    from ddtrace import config
 
-    assert config._trace_agent_hostname == os.environ["DD_AGENT_HOST"]
-    assert config._stats_agent_hostname == os.environ.get("DD_AGENT_HOST")
+@pytest.mark.subprocess(
+    env={"DD_TRACE_AGENT_HOSTNAME": "monkey", "DD_AGENT_HOST": "baboon", "DD_TRACE_AGENT_URL": None}
+)
+def test_trace_hostname():
+    from urllib.parse import urlparse
+
+    from ddtrace.internal.agent import config
 
+    assert urlparse(config.trace_agent_url).hostname == "monkey"
 
-@pytest.mark.subprocess(env={"DD_AGENT_HOST": None})
+
+@pytest.mark.subprocess(env={"DD_AGENT_HOST": None, "DD_TRACE_AGENT_HOSTNAME": None})
 def test_hostname_not_set():
-    from ddtrace import config
+    from urllib.parse import urlparse
+
+    from ddtrace.internal.agent import config
 
-    assert config._stats_agent_hostname is None
-    assert config._trace_agent_hostname is None
+    assert urlparse(config.trace_agent_url).hostname == "localhost"
 
 
-@pytest.mark.subprocess(env={"DD_AGENT_PORT": "1235", "DD_TRACE_AGENT_PORT": "9999"})
+@pytest.mark.subprocess(env={"DD_AGENT_PORT": "1235", "DD_TRACE_AGENT_PORT": "9999", "DD_TRACE_AGENT_URL": None})
 def test_trace_port():
-    from ddtrace import config
+    from urllib.parse import urlparse
+
+    from ddtrace.internal.agent import config
 
-    assert config._trace_agent_port == "1235"
+    assert urlparse(config.trace_agent_url).port == 9999
 
 
-@pytest.mark.subprocess(env={"DD_TRACE_AGENT_PORT": "9999"})
-def test_trace_port_legacy():
-    from ddtrace import config
+@pytest.mark.subprocess(env={"DD_AGENT_PORT": "1235", "DD_TRACE_AGENT_PORT": None, "DD_TRACE_AGENT_URL": None})
+def test_agent_port():
+    from urllib.parse import urlparse
 
-    assert config._trace_agent_port == "9999"
+    from ddtrace.internal.agent import config
 
+    assert urlparse(config.trace_agent_url).port == 1235
 
-@pytest.mark.subprocess(env={"DD_AGENT_PORT": None, "DD_TRACE_AGENT_PORT": None})
+
+@pytest.mark.subprocess(env={"DD_AGENT_PORT": None, "DD_TRACE_AGENT_PORT": None, "DD_TRACE_AGENT_URL": None})
 def test_trace_port_not_set():
-    from ddtrace import config
+    from urllib.parse import urlparse
+
+    from ddtrace.internal.agent import config
 
-    assert config._trace_agent_port is None
+    assert urlparse(config.trace_agent_url).port == 8126
 
 
-@pytest.mark.subprocess(env={"DD_DOGSTATSD_PORT": "1235"})
+@pytest.mark.subprocess(env={"DD_DOGSTATSD_PORT": "1235", "DD_DOGSTATSD_URL": None})
 def test_stats_port():
-    from ddtrace import config
+    from urllib.parse import urlparse
 
-    assert config._stats_agent_port == "1235"
+    from ddtrace.internal.agent import config
 
+    assert urlparse(config.dogstatsd_url).port == 1235
 
-@pytest.mark.subprocess(env={"DD_DOGSTATSD_PORT": None})
+
+@pytest.mark.subprocess(env={"DD_DOGSTATSD_PORT": None, "DD_AGENT_PORT": None, "DD_DOGSTATSD_URL": None})
 def test_stats_port_not_set():
-    from ddtrace import config
+    from urllib.parse import urlparse
+
+    from ddtrace.internal.agent import config
 
-    assert config._stats_agent_port is None
+    assert urlparse(config.dogstatsd_url).port == 8125
 
 
-@pytest.mark.subprocess(env={"DD_TRACE_AGENT_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_TRACE_AGENT_URL": None,
+        "DD_TRACE_AGENT_HOST": None,
+        "DD_TRACE_AGENT_PORT": None,
+        "DD_AGENT_HOST": None,
+        "DD_AGENT_PORT": None,
+    }
+)
 def test_trace_url_uds():
     # with nothing set by user, and the default UDS available, we choose UDS
     import mock
 
-    from ddtrace.internal import agent
-
     with mock.patch("os.path.exists", return_value=True):
+        from ddtrace.internal import agent
+
         assert agent.get_trace_url() == "unix:///var/run/datadog/apm.socket"
 
 
-@pytest.mark.subprocess(env={"DD_TRACE_AGENT_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_TRACE_AGENT_URL": None,
+        "DD_TRACE_AGENT_HOST": None,
+        "DD_TRACE_AGENT_PORT": None,
+        "DD_AGENT_HOST": None,
+        "DD_AGENT_PORT": None,
+    }
+)
 def test_trace_url_default():
     # with nothing set by user, and the default UDS unavailable, we choose default http address
     import mock
@@ -98,7 +140,9 @@ def test_trace_url_default():
         assert agent.get_trace_url() == "http://localhost:8126"
 
 
-@pytest.mark.subprocess(env={"DD_TRACE_AGENT_PORT": "1235", "DD_TRACE_AGENT_URL": None})
+@pytest.mark.subprocess(
+    env={"DD_TRACE_AGENT_PORT": "1235", "DD_TRACE_AGENT_URL": None, "DD_AGENT_HOST": None, "DD_TRACE_AGENT_HOST": None}
+)
 def test_trace_url_with_port():
     # with port set by user, and default UDS unavailable, we choose user settings
     import mock
@@ -106,10 +150,19 @@ def test_trace_url_with_port():
     from ddtrace.internal import agent
 
     with mock.patch("os.path.exists", return_value=False):
-        assert agent.get_trace_url() == "http://localhost:1235"
+        url = agent.get_trace_url()
+        assert url == "http://localhost:1235", url
 
 
-@pytest.mark.subprocess(env={"DD_AGENT_HOST": "mars", "DD_TRACE_AGENT_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_AGENT_HOST": "mars",
+        "DD_TRACE_AGENT_HOST": None,
+        "DD_TRACE_AGENT_URL": None,
+        "DD_TRACE_AGENT_PORT": None,
+        "DD_AGENT_PORT": None,
+    }
+)
 def test_trace_url_with_host():
     # with host set by user, and default UDS unavailable, we choose user settings
     import mock
@@ -120,7 +173,14 @@ def test_trace_url_with_host():
         assert agent.get_trace_url() == "http://mars:8126", agent.get_trace_url()
 
 
-@pytest.mark.subprocess(env={"DD_AGENT_HOST": "mars", "DD_TRACE_AGENT_PORT": "1235", "DD_TRACE_AGENT_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_AGENT_HOST": "mars",
+        "DD_TRACE_AGENT_PORT": "1235",
+        "DD_TRACE_AGENT_URL": None,
+        "DD_TRACE_AGENT_HOST": None,
+    }
+)
 def test_trace_url_with_host_and_port():
     # with host and port set by user, and default UDS unavailable, we choose user settings
     import mock
@@ -131,7 +191,9 @@ def test_trace_url_with_host_and_port():
         assert agent.get_trace_url() == "http://mars:1235"
 
 
-@pytest.mark.subprocess(env={"DD_TRACE_AGENT_PORT": "1235", "DD_TRACE_AGENT_URL": None})
+@pytest.mark.subprocess(
+    env={"DD_TRACE_AGENT_PORT": "1235", "DD_TRACE_AGENT_URL": None, "DD_TRACE_AGENT_HOST": None, "DD_AGENT_HOST": None}
+)
 def test_trace_url_with_uds_and_port():
     # with port set by user, and default UDS available, we choose user settings
     import mock
@@ -142,7 +204,15 @@ def test_trace_url_with_uds_and_port():
         assert agent.get_trace_url() == "http://localhost:1235"
 
 
-@pytest.mark.subprocess(env={"DD_AGENT_HOST": "mars", "DD_TRACE_AGENT_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_AGENT_HOST": "mars",
+        "DD_TRACE_AGENT_HOST": None,
+        "DD_TRACE_AGENT_URL": None,
+        "DD_TRACE_AGENT_PORT": None,
+        "DD_AGENT_PORT": None,
+    }
+)
 def test_trace_url_with_uds_and_host():
     # with host set by user, and default UDS available, we choose user settings
     import mock
@@ -153,7 +223,14 @@ def test_trace_url_with_uds_and_host():
         assert agent.get_trace_url() == "http://mars:8126"
 
 
-@pytest.mark.subprocess(env={"DD_TRACE_AGENT_PORT": "1235", "DD_AGENT_HOST": "mars", "DD_TRACE_AGENT_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_TRACE_AGENT_PORT": "1235",
+        "DD_AGENT_HOST": "mars",
+        "DD_TRACE_AGENT_URL": None,
+        "DD_TRACE_AGENT_HOST": None,
+    }
+)
 def test_trace_url_with_uds_host_and_port():
     # with host and port set by user, and default UDS available, we choose user settings
     import mock
@@ -190,7 +267,15 @@ def test_trace_url_with_url_host_and_port():
         assert agent.get_trace_url() == "http://saturn:1111"
 
 
-@pytest.mark.subprocess(env={"DD_DOGSTATSD_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_DOGSTATSD_URL": None,
+        "DD_AGENT_HOST": None,
+        "DD_DOGSTATSD_PORT": None,
+        "DD_AGENT_PORT": None,
+        "DD_DOGSTATSD_HOST": None,
+    }
+)
 def test_stats_url_default():
     # with nothing set by user, and the default UDS unavailable, we choose default http address
     import mock
@@ -201,7 +286,15 @@ def test_stats_url_default():
         assert agent.get_stats_url() == "udp://localhost:8125"
 
 
-@pytest.mark.subprocess(env={"DD_DOGSTATSD_PORT": "1235", "DD_DOGSTATSD_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_DOGSTATSD_PORT": "1235",
+        "DD_DOGSTATSD_URL": None,
+        "DD_DOGSTATSD_HOST": None,
+        "DD_AGENT_PORT": None,
+        "DD_AGENT_HOST": None,
+    }
+)
 def test_stats_url_with_port():
     # with port set by user, and default UDS unavailable, we choose user settings
     import mock
@@ -212,7 +305,15 @@ def test_stats_url_with_port():
         assert agent.get_stats_url() == "udp://localhost:1235"
 
 
-@pytest.mark.subprocess(env={"DD_AGENT_HOST": "mars", "DD_DOGSTATSD_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_AGENT_HOST": "mars",
+        "DD_DOGSTATSD_HOST": None,
+        "DD_DOGSTATSD_URL": None,
+        "DD_DOGSTATSD_PORT": None,
+        "DD_AGENT_PORT": None,
+    }
+)
 def test_stats_url_with_host():
     # with host set by user, and default UDS unavailable, we choose user settings
     import mock
@@ -223,7 +324,9 @@ def test_stats_url_with_host():
         assert agent.get_stats_url() == "udp://mars:8125"
 
 
-@pytest.mark.subprocess(env={"DD_AGENT_HOST": "mars", "DD_DOGSTATSD_PORT": "1235", "DD_DOGSTATSD_URL": None})
+@pytest.mark.subprocess(
+    env={"DD_AGENT_HOST": "mars", "DD_DOGSTATSD_PORT": "1235", "DD_DOGSTATSD_URL": None, "DD_DOGSTATSD_HOST": None}
+)
 def test_stats_url_with_host_and_port():
     # with host and port set by user, and default UDS unavailable, we choose user settings
     import mock
@@ -234,7 +337,15 @@ def test_stats_url_with_host_and_port():
         assert agent.get_stats_url() == "udp://mars:1235"
 
 
-@pytest.mark.subprocess(env={"DD_DOGSTATSD_PORT": "1235", "DD_DOGSTATSD_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_DOGSTATSD_PORT": "1235",
+        "DD_DOGSTATSD_URL": None,
+        "DD_DOGSTATSD_HOST": None,
+        "DD_AGENT_PORT": None,
+        "DD_AGENT_HOST": None,
+    }
+)
 def test_stats_url_with_uds_and_port():
     # with port set by user, and default UDS available, we choose user settings
     import mock
@@ -245,7 +356,15 @@ def test_stats_url_with_uds_and_port():
         assert agent.get_stats_url() == "udp://localhost:1235"
 
 
-@pytest.mark.subprocess(env={"DD_AGENT_HOST": "mars", "DD_DOGSTATSD_URL": None})
+@pytest.mark.subprocess(
+    env={
+        "DD_AGENT_HOST": "mars",
+        "DD_DOGSTATSD_URL": None,
+        "DD_DOGSTATSD_HOST": None,
+        "DD_DOGSTATSD_PORT": None,
+        "DD_AGENT_PORT": None,
+    }
+)
 def test_stats_url_with_uds_and_host():
     # with host set by user, and default UDS available, we choose user settings
     import mock
@@ -256,7 +375,9 @@ def test_stats_url_with_uds_and_host():
         assert agent.get_stats_url() == "udp://mars:8125"
 
 
-@pytest.mark.subprocess(env={"DD_DOGSTATSD_PORT": "1235", "DD_AGENT_HOST": "mars", "DD_DOGSTATSD_URL": None})
+@pytest.mark.subprocess(
+    env={"DD_DOGSTATSD_PORT": "1235", "DD_AGENT_HOST": "mars", "DD_DOGSTATSD_URL": None, "DD_DOGSTATSD_HOST": None}
+)
 def test_stats_url_with_uds_host_and_port():
     # with host and port set by user, and default UDS available, we choose user settings
     import mock
diff --git a/tests/tracer/test_tracer.py b/tests/tracer/test_tracer.py
index 48977bae0d5..6a9bc73ee10 100644
--- a/tests/tracer/test_tracer.py
+++ b/tests/tracer/test_tracer.py
@@ -588,8 +588,8 @@ def test_tracer_set_user_propagation_string_error(self):
             assert user_id == "44Om44O844K244O8SUQ="
 
 
-@pytest.mark.subprocess(env=dict(DD_AGENT_PORT="", DD_AGENT_HOST="", DD_TRACE_AGENT_URL=""))
-def test_tracer_url():
+@pytest.mark.subprocess(env=dict(DD_AGENT_PORT=None, DD_AGENT_HOST=None, DD_TRACE_AGENT_URL=None))
+def test_tracer_url_default():
     import ddtrace
 
     assert ddtrace.trace.tracer._writer.agent_url == "http://localhost:8126"


Reviews:
user: login: erikayasuda
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-07T21:06:17Z
user: login: mabdinur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T21:24:58Z
user: login: mabdinur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T21:25:20Z
user: login: mabdinur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T22:23:39Z
user: login: P403n1x87
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T09:37:26Z
user: login: mabdinur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:35:59Z
user: login: mabdinur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:40:02Z
user: login: P403n1x87
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T19:13:35Z
user: login: P403n1x87
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T19:16:03Z
user: login: mabdinur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T19:35:49Z
user: login: rachelyangdog
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:07:18Z

Review comments:
diff_hunk: @@ -0,0 +1,6 @@
+---
+other:
+  - |
+    agent: Migrates agent connection configurations to envier. This provides better documentation and validation for expected types. 
+    The following configurations are affected: DD_TRACE_AGENT_HOSTNAME, DD_AGENT_HOST, DD_TRACE_AGENT_PORT, DD_AGENT_PORT, DD_TRACE_AGENT_URL
+    DD_DOGSTATSD_HOST, DD_AGENT_HOST, DD_DOGSTATSD_PORT, DD_AGENT_PORT, DD_DOGSTATSD_URL, and DD_TRACE_AGENT_TIMEOUT_SECONDS.
path: releasenotes/notes/update-agent-tags-200920a48913617c.yaml
user: login: erikayasuda
body: Could you maybe explicitly call out here the change in precedence? Maybe something like:
```suggestion
    The following configuration are affected:
    - `DD_TRACE_AGENT_URL`
    - `DD_DOGSTATSD_URL`
    - `DD_TRACE_AGENT_HOSTNAME`
    - `DD_AGENT_HOST`
    - `DD_TRACE_AGENT_PORT`
    - `DD_DOGSTATSD_PORT`
    - `DD_AGENT_PORT`
    - `DD_TRACE_AGENT_TIMEOUT_SECONDS`
    Of the above impacted configurations, these had a precedence change:
    - `DD_TRACE_AGENT_HOSTNAME` over `DD_AGENT_HOST`
    - `DD_TRACE_AGENT_PORT` over `DD_AGENT_PORT`
    - `DD_DOGSTATSD_HOST` over `DD_AGENT_HOST`
    - `DD_DOGSTATSD_PORT` over `DD_AGENT_PORT`
```
I might've flipped a few, I would definitely double check what I have here 😅 
created_at: 2025-03-07T20:37:21Z
author_association: COLLABORATOR
diff_hunk: @@ -672,6 +678,8 @@ def __init__(self):
         self._inject_was_attempted = _get_config("_DD_INJECT_WAS_ATTEMPTED", False, asbool)
         self._inferred_proxy_services_enabled = _get_config("DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED", False, asbool)
 
+        # report telemetry for agent configurations
+
path: ddtrace/settings/_config.py
user: login: erikayasuda
body: I'm assuming we don't need this here?
```suggestion
```
created_at: 2025-03-07T20:45:00Z
author_association: COLLABORATOR
diff_hunk: @@ -33,7 +33,14 @@ def test_hostname():
     assert config._stats_agent_hostname == os.environ.get("DD_AGENT_HOST")
 
 
-@pytest.mark.subprocess(env={"DD_AGENT_HOST": None})
+@pytest.mark.subprocess(env={"DD_TRACE_AGENT_HOSTNAME": "monkey", "DD_AGENT_HOST": "baboon"})
+def test_trace_hostname():
+    from ddtrace import config
+
+    assert config._trace_agent_hostname == "monkey"
path: tests/tracer/test_agent.py
user: login: erikayasuda
body: Just because I happened to see the test under this... 👀  We don't need to test something like this in this specific test:
```python
assert config._stats_agent_hostname == "baboon"
```
Because (1) this is a test for the `_trace_agent_hostname` anyways but also because (2) `_stats_agent_hostname` could be set by either `dogstatsd_host` OR `agent_host`? So just setting `DD_AGENT_HOST` isn't a correct assumption about the `_stats_agent_hostname`?
created_at: 2025-03-07T20:50:12Z
author_association: COLLABORATOR
diff_hunk: @@ -492,14 +492,20 @@ def __init__(self):
         )
         self._trace_writer_log_err_payload = _get_config("_DD_TRACE_WRITER_LOG_ERROR_PAYLOADS", False, asbool)
 
-        self._trace_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_TRACE_AGENT_HOSTNAME"])
-        self._trace_agent_port = _get_config(["DD_AGENT_PORT", "DD_TRACE_AGENT_PORT"])
+        # TODO: Remove the configurations below. ddtrace.internal.agent.config should be used instead.
+        self._trace_agent_hostname = _get_config(["DD_TRACE_AGENT_HOSTNAME", "DD_AGENT_HOST"])
+        self._trace_agent_port = _get_config(["DD_TRACE_AGENT_PORT", "DD_AGENT_PORT"])
         self._trace_agent_url = _get_config("DD_TRACE_AGENT_URL")
-
-        self._stats_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_DOGSTATSD_HOST"])
-        self._stats_agent_port = _get_config("DD_DOGSTATSD_PORT")
+        self._stats_agent_hostname = _get_config(["DD_DOGSTATSD_HOST", "DD_AGENT_HOST"])
+        self._stats_agent_port = _get_config(["DD_DOGSTATSD_PORT", "DD_AGENT_PORT"])
         self._stats_agent_url = _get_config("DD_DOGSTATSD_URL")
         self._agent_timeout_seconds = _get_config("DD_TRACE_AGENT_TIMEOUT_SECONDS", DEFAULT_TIMEOUT, float)
+        # Report Telemetry for Agent Connection Configurations. We need to do this here to avoid circular imports
path: ddtrace/settings/_config.py
user: login: erikayasuda
body: Where was the circular import we avoided by reporting here?
created_at: 2025-03-07T20:57:14Z
author_association: COLLABORATOR
diff_hunk: @@ -492,14 +492,20 @@ def __init__(self):
         )
         self._trace_writer_log_err_payload = _get_config("_DD_TRACE_WRITER_LOG_ERROR_PAYLOADS", False, asbool)
 
-        self._trace_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_TRACE_AGENT_HOSTNAME"])
-        self._trace_agent_port = _get_config(["DD_AGENT_PORT", "DD_TRACE_AGENT_PORT"])
+        # TODO: Remove the configurations below. ddtrace.internal.agent.config should be used instead.
+        self._trace_agent_hostname = _get_config(["DD_TRACE_AGENT_HOSTNAME", "DD_AGENT_HOST"])
+        self._trace_agent_port = _get_config(["DD_TRACE_AGENT_PORT", "DD_AGENT_PORT"])
         self._trace_agent_url = _get_config("DD_TRACE_AGENT_URL")
-
-        self._stats_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_DOGSTATSD_HOST"])
-        self._stats_agent_port = _get_config("DD_DOGSTATSD_PORT")
+        self._stats_agent_hostname = _get_config(["DD_DOGSTATSD_HOST", "DD_AGENT_HOST"])
+        self._stats_agent_port = _get_config(["DD_DOGSTATSD_PORT", "DD_AGENT_PORT"])
path: ddtrace/settings/_config.py
user: login: erikayasuda
body: Why was the order the opposite of this before this change? This change (also the change you made at the agent config level) is basically saying that we'll respect the legacy config over the newer one right? That makes sense to me until we add this as a breaking change in the next major, but why was that not the case before this?
created_at: 2025-03-07T20:58:57Z
author_association: COLLABORATOR
diff_hunk: @@ -453,6 +456,16 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
         {"name": "DD_TEST_SESSION_NAME", "origin": "default", "value": None},
         {"name": "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", "origin": "env_var", "value": True},
         {"name": "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED", "origin": "default", "value": False},
+        {
+            "name": "DD_TRACE_AGENT_HOSTNAME",
+            "origin": "default",
+            "value": None,
+        },
path: tests/telemetry/test_writer.py
user: login: mabdinur
body: ```suggestion
        {"name": "DD_TRACE_AGENT_HOSTNAME", "origin": "default", "value": None},
```
created_at: 2025-03-07T21:24:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -453,6 +456,16 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
         {"name": "DD_TEST_SESSION_NAME", "origin": "default", "value": None},
         {"name": "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", "origin": "env_var", "value": True},
         {"name": "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED", "origin": "default", "value": False},
+        {
+            "name": "DD_TRACE_AGENT_HOSTNAME",
+            "origin": "default",
+            "value": None,
+        },
+        {
+            "name": "DD_TRACE_AGENT_PORT",
+            "origin": "default",
+            "value": None,
+        },
path: tests/telemetry/test_writer.py
user: login: mabdinur
body: ```suggestion
        {"name": "DD_TRACE_AGENT_PORT", "origin": "default", "value": None},
```
created_at: 2025-03-07T21:25:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -492,14 +492,20 @@ def __init__(self):
         )
         self._trace_writer_log_err_payload = _get_config("_DD_TRACE_WRITER_LOG_ERROR_PAYLOADS", False, asbool)
 
-        self._trace_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_TRACE_AGENT_HOSTNAME"])
-        self._trace_agent_port = _get_config(["DD_AGENT_PORT", "DD_TRACE_AGENT_PORT"])
+        # TODO: Remove the configurations below. ddtrace.internal.agent.config should be used instead.
+        self._trace_agent_hostname = _get_config(["DD_TRACE_AGENT_HOSTNAME", "DD_AGENT_HOST"])
+        self._trace_agent_port = _get_config(["DD_TRACE_AGENT_PORT", "DD_AGENT_PORT"])
         self._trace_agent_url = _get_config("DD_TRACE_AGENT_URL")
-
-        self._stats_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_DOGSTATSD_HOST"])
-        self._stats_agent_port = _get_config("DD_DOGSTATSD_PORT")
+        self._stats_agent_hostname = _get_config(["DD_DOGSTATSD_HOST", "DD_AGENT_HOST"])
+        self._stats_agent_port = _get_config(["DD_DOGSTATSD_PORT", "DD_AGENT_PORT"])
         self._stats_agent_url = _get_config("DD_DOGSTATSD_URL")
         self._agent_timeout_seconds = _get_config("DD_TRACE_AGENT_TIMEOUT_SECONDS", DEFAULT_TIMEOUT, float)
+        # Report Telemetry for Agent Connection Configurations. We need to do this here to avoid circular imports
path: ddtrace/settings/_config.py
user: login: mabdinur
body: The telemetry writer imports from `ddtrace.internal.agent` to get the agent url. So we can report telemetry information when `ddtrace.internal.agent.config` is initialized. We need to delay it until after the telemetry writer is fully built. 

Since the config object is initialized after the telemetry writer I put the `report_telemetry(agent_config)` call here. Not sure if there's a better place to put this 😞.
created_at: 2025-03-07T22:23:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -492,14 +492,20 @@ def __init__(self):
         )
         self._trace_writer_log_err_payload = _get_config("_DD_TRACE_WRITER_LOG_ERROR_PAYLOADS", False, asbool)
 
-        self._trace_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_TRACE_AGENT_HOSTNAME"])
-        self._trace_agent_port = _get_config(["DD_AGENT_PORT", "DD_TRACE_AGENT_PORT"])
+        # TODO: Remove the configurations below. ddtrace.internal.agent.config should be used instead.
+        self._trace_agent_hostname = _get_config(["DD_TRACE_AGENT_HOSTNAME", "DD_AGENT_HOST"])
+        self._trace_agent_port = _get_config(["DD_TRACE_AGENT_PORT", "DD_AGENT_PORT"])
         self._trace_agent_url = _get_config("DD_TRACE_AGENT_URL")
-
-        self._stats_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_DOGSTATSD_HOST"])
-        self._stats_agent_port = _get_config("DD_DOGSTATSD_PORT")
+        self._stats_agent_hostname = _get_config(["DD_DOGSTATSD_HOST", "DD_AGENT_HOST"])
+        self._stats_agent_port = _get_config(["DD_DOGSTATSD_PORT", "DD_AGENT_PORT"])
         self._stats_agent_url = _get_config("DD_DOGSTATSD_URL")
         self._agent_timeout_seconds = _get_config("DD_TRACE_AGENT_TIMEOUT_SECONDS", DEFAULT_TIMEOUT, float)
+        # Report Telemetry for Agent Connection Configurations. We need to do this here to avoid circular imports
path: ddtrace/settings/_config.py
user: login: P403n1x87
body: I think we need to come up with a better design here to ensure we don't have this circular imports. Having to do this things on a regular basis will make things hard to work with in the future. This would be out of scope for this PR though, so fine to do it elsewhere.
created_at: 2025-03-10T09:37:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,7 +33,14 @@ def test_hostname():
     assert config._stats_agent_hostname == os.environ.get("DD_AGENT_HOST")
 
 
-@pytest.mark.subprocess(env={"DD_AGENT_HOST": None})
+@pytest.mark.subprocess(env={"DD_TRACE_AGENT_HOSTNAME": "monkey", "DD_AGENT_HOST": "baboon"})
+def test_trace_hostname():
+    from ddtrace import config
+
+    assert config._trace_agent_hostname == "monkey"
path: tests/tracer/test_agent.py
user: login: mabdinur
body: Yup, I updated all the tests to parse the hostname/port of the final agent url configuration. The current test is useless it validates that an unused configuration was set.
created_at: 2025-03-10T14:35:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -492,14 +492,20 @@ def __init__(self):
         )
         self._trace_writer_log_err_payload = _get_config("_DD_TRACE_WRITER_LOG_ERROR_PAYLOADS", False, asbool)
 
-        self._trace_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_TRACE_AGENT_HOSTNAME"])
-        self._trace_agent_port = _get_config(["DD_AGENT_PORT", "DD_TRACE_AGENT_PORT"])
+        # TODO: Remove the configurations below. ddtrace.internal.agent.config should be used instead.
+        self._trace_agent_hostname = _get_config(["DD_TRACE_AGENT_HOSTNAME", "DD_AGENT_HOST"])
+        self._trace_agent_port = _get_config(["DD_TRACE_AGENT_PORT", "DD_AGENT_PORT"])
         self._trace_agent_url = _get_config("DD_TRACE_AGENT_URL")
-
-        self._stats_agent_hostname = _get_config(["DD_AGENT_HOST", "DD_DOGSTATSD_HOST"])
-        self._stats_agent_port = _get_config("DD_DOGSTATSD_PORT")
+        self._stats_agent_hostname = _get_config(["DD_DOGSTATSD_HOST", "DD_AGENT_HOST"])
+        self._stats_agent_port = _get_config(["DD_DOGSTATSD_PORT", "DD_AGENT_PORT"])
path: ddtrace/settings/_config.py
user: login: mabdinur
body: I guess putting the "legacy config" as a backup config streamlines the adoption of a new config. I think this is pattern we use for most of our configurations (and in envier). 

I just updated the precedence of the agent connection configurations to be consistent with the other sdks. I am not sure which set of configurations we are planning to support moving forward.
created_at: 2025-03-10T14:40:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,9 +67,114 @@ def get_stats_url():
     return url
 
 
+# This method returns if a hostname is an IPv6 address
+def is_ipv6_hostname(hostname):
+    # type: (Union[T, str]) -> bool
+    if not isinstance(hostname, str):
+        return False
+    try:
+        socket.inet_pton(socket.AF_INET6, hostname)
+        return True
+    except socket.error:  # not a valid address
+        return False
+
+
+class AgentConfig(DDConfig):
+    __prefix__ = "dd"
+
+    _trace_agent_hostname = DDConfig.v(
+        Optional[str],
+        "trace_agent_hostname",
+        default=None,
+        help_type="String",
+        help="Legacy configuration, stores the hostname of the trace agent",
+    )
+
+    _trace_agent_port = DDConfig.v(
+        Optional[int],
+        "trace_agent_port",
+        default=None,
+        help_type="Int",
+        help="Legacy configuration, stores the port of the trace agent",
+    )
+
+    _trace_agent_url = DDConfig.v(
+        Optional[str],
+        "trace_agent_url",
+        default=None,
+        help_type="String",
+        help="Stores the URL of the trace agent",
+    )
+
+    trace_agent_timeout_seconds = DDConfig.v(
+        float,
+        "trace_agent_timeout_seconds",
+        default=DEFAULT_TIMEOUT,
+        help_type="Float",
+        help="Stores the timeout in seconds for the trace agent",
+    )
+
+    _dogstatsd_host = DDConfig.v(
+        Optional[str],
+        "dogstatsd_host",
+        default=None,
+        help_type="String",
+        help="Stores the hostname of the agent receiving DogStatsD metrics",
+    )
+
+    _dogstatsd_port = DDConfig.v(
+        Optional[int],
+        "dogstatsd_port",
+        default=None,
+        help_type="Int",
+        help="Stores the port of the agent receiving DogStatsD metrics",
+    )
+
+    _dogstatsd_url = DDConfig.v(
+        Optional[str],
+        "dogstatsd_url",
+        default=None,
+        help_type="String",
+        help="Stores the URL of the DogStatsD agent",
+    )
+
+    _agent_host = DDConfig.v(
+        Optional[str],
+        "agent_host",
+        default=None,
+        help_type="String",
+        help="Stores the hostname of the agent",
+    )
+
+    _agent_port = DDConfig.v(
+        Optional[int],
+        "agent_port",
+        default=None,
+        help_type="Int",
+        help="Stores the port of the agent",
+    )
+    # Effective trace agent URL (this is the one that will be used)
+    trace_agent_url = DDConfig.d(str, _derive_trace_url)
+    # Effective DogStatsD URL (this is the one that will be used)
+    dogstatsd_url = DDConfig.d(str, _derive_stats_url)
+
+
+config = AgentConfig()
+
+
+def get_trace_url() -> str:
+    """Return the Agent URL computed from the environment."""
+    return config.trace_agent_url
+
+
+def get_stats_url() -> str:
+    """Return the DogStatsD URL computed from the environment."""
+    return config.dogstatsd_url
path: ddtrace/internal/agent.py
user: login: P403n1x87
body: If it doesn't take too many changes, we could try to remove these functions as well
created_at: 2025-03-10T19:13:35Z
author_association: CONTRIBUTOR
path: releasenotes/notes/update-agent-tags-200920a48913617c.yaml
user: login: P403n1x87
body: Does this appear in the release notes? If so, I don't think this level of internal details would be useful for the users
created_at: 2025-03-10T19:16:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,9 +67,114 @@ def get_stats_url():
     return url
 
 
+# This method returns if a hostname is an IPv6 address
+def is_ipv6_hostname(hostname):
+    # type: (Union[T, str]) -> bool
+    if not isinstance(hostname, str):
+        return False
+    try:
+        socket.inet_pton(socket.AF_INET6, hostname)
+        return True
+    except socket.error:  # not a valid address
+        return False
+
+
+class AgentConfig(DDConfig):
+    __prefix__ = "dd"
+
+    _trace_agent_hostname = DDConfig.v(
+        Optional[str],
+        "trace_agent_hostname",
+        default=None,
+        help_type="String",
+        help="Legacy configuration, stores the hostname of the trace agent",
+    )
+
+    _trace_agent_port = DDConfig.v(
+        Optional[int],
+        "trace_agent_port",
+        default=None,
+        help_type="Int",
+        help="Legacy configuration, stores the port of the trace agent",
+    )
+
+    _trace_agent_url = DDConfig.v(
+        Optional[str],
+        "trace_agent_url",
+        default=None,
+        help_type="String",
+        help="Stores the URL of the trace agent",
+    )
+
+    trace_agent_timeout_seconds = DDConfig.v(
+        float,
+        "trace_agent_timeout_seconds",
+        default=DEFAULT_TIMEOUT,
+        help_type="Float",
+        help="Stores the timeout in seconds for the trace agent",
+    )
+
+    _dogstatsd_host = DDConfig.v(
+        Optional[str],
+        "dogstatsd_host",
+        default=None,
+        help_type="String",
+        help="Stores the hostname of the agent receiving DogStatsD metrics",
+    )
+
+    _dogstatsd_port = DDConfig.v(
+        Optional[int],
+        "dogstatsd_port",
+        default=None,
+        help_type="Int",
+        help="Stores the port of the agent receiving DogStatsD metrics",
+    )
+
+    _dogstatsd_url = DDConfig.v(
+        Optional[str],
+        "dogstatsd_url",
+        default=None,
+        help_type="String",
+        help="Stores the URL of the DogStatsD agent",
+    )
+
+    _agent_host = DDConfig.v(
+        Optional[str],
+        "agent_host",
+        default=None,
+        help_type="String",
+        help="Stores the hostname of the agent",
+    )
+
+    _agent_port = DDConfig.v(
+        Optional[int],
+        "agent_port",
+        default=None,
+        help_type="Int",
+        help="Stores the port of the agent",
+    )
+    # Effective trace agent URL (this is the one that will be used)
+    trace_agent_url = DDConfig.d(str, _derive_trace_url)
+    # Effective DogStatsD URL (this is the one that will be used)
+    dogstatsd_url = DDConfig.d(str, _derive_stats_url)
+
+
+config = AgentConfig()
+
+
+def get_trace_url() -> str:
+    """Return the Agent URL computed from the environment."""
+    return config.trace_agent_url
+
+
+def get_stats_url() -> str:
+    """Return the DogStatsD URL computed from the environment."""
+    return config.dogstatsd_url
path: ddtrace/internal/agent.py
user: login: mabdinur
body: Yup, my plan is to do it in a follow up. This will require reviews from all teams.
created_at: 2025-03-10T19:35:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-06T21:37:46Z
body: `CODEOWNERS` have been resolved as:

```
releasenotes/notes/update-agent-tags-200920a48913617c.yaml              @DataDog/apm-python
ddtrace/internal/agent.py                                               @DataDog/apm-core-python
ddtrace/internal/remoteconfig/client.py                                 @DataDog/remote-config @DataDog/apm-core-python
ddtrace/internal/writer/writer.py                                       @DataDog/apm-core-python
ddtrace/settings/_config.py                                             @DataDog/apm-core-python
tests/telemetry/test_writer.py                                          @DataDog/apm-python
tests/tracer/test_agent.py                                              @DataDog/apm-sdk-api-python
tests/tracer/test_tracer.py                                             @DataDog/apm-sdk-api-python
```
<!-- thollander/actions-comment-pull-request "codeowners_resolved" -->
author_association: CONTRIBUTOR
user: login: pr-commenter[bot]
created_at: 2025-03-06T22:22:34Z
body: ## Benchmarks

Benchmark execution time: 2025-03-08 05:39:08

Comparing candidate commit 1c3751ea46290d87e84fe725844adfd488a0bf5f in PR branch `munir/update-agent-tags` with baseline commit 907b0429ae341fb90d57c673324fb3107641bb3d in branch `main`.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 408 metrics, 2 unstable metrics.


author_association: NONE
user: login: mabdinur
created_at: 2025-03-06T23:27:10Z
body: Blocked by: https://github.com/DataDog/dd-go/pull/173800
author_association: CONTRIBUTOR