Skip to content

Commit 38f917b

Browse files
authored
fix(agentex): build valid OTLP metrics URLs (#346)
## Summary - fix worker OTLP metrics URL construction for normal `DD_AGENT_HOST` values like `localhost`, service names, and IPv4 literals - keep IPv6 literals bracketed and preserve explicit ports - add focused unit coverage for metrics URL construction Closes #313. Supersedes stale external PR #323 with the same underlying fix on a fresh branch. ## Tests - `uv run ruff check agentex/src/temporal/run_worker.py agentex/tests/unit/temporal/test_run_worker_metrics_url.py` - `uv run --package agentex-backend --group test pytest agentex/tests/unit/temporal/test_run_worker_metrics_url.py -m unit -q` - `uv run --package agentex-backend --group test pytest agentex/tests/unit/temporal -m unit -q` - Runtime repro with `temporalio 1.23.0`: old `http://[{DD_AGENT_HOST}]:4317` URLs reject `localhost`, `datadog-agent`, and `10.0.0.5` with `Invalid OTel URL: invalid IPv6 address`; `build_metrics_url(...)` outputs are accepted by `Runtime(telemetry=TelemetryConfig(metrics=OpenTelemetryConfig(url=...)))`. <!-- greptile_comment --> <h3>Greptile Summary</h3> Fixes the OTLP metrics URL construction for Temporal workers by replacing the broken `http://[{DD_AGENT_HOST}]:4317` string interpolation with a dedicated `build_metrics_url` function that correctly handles all common `DD_AGENT_HOST` shapes and adds focused unit tests for each case. - `build_metrics_url` correctly handles plain hostnames, IPv4 literals, bare IPv6 addresses (brackets them), pre-bracketed IPv6 (including the previously-identified empty-trailing-port edge case `"[::1]:"`, now covered by a test), and explicit ports for all host types. - One unguarded edge case remains: a malformed input that begins with `"["` but has no closing `"]"` bypasses the inner parsing block and is then double-bracketed by the final colon-presence check, producing an invalid URL like `http://[[::1]:4317`. <details><summary><h3>Confidence Score: 5/5</h3></summary> Safe to merge — the fix correctly resolves the invalid IPv6-bracket wrapping for all realistic DD_AGENT_HOST values and is well-covered by the new unit tests. The core bug (wrapping every host in IPv6 brackets) is definitively fixed and tested across all meaningful host shapes. The one unguarded path — a malformed input that begins with an unclosed bracket — is an unrealistic operator configuration and does not affect production behaviour. No files require special attention; the logic in run_worker.py is straightforward and the test file covers all realistic cases. </details> <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | agentex/src/temporal/run_worker.py | Replaces the broken `http://[{DD_AGENT_HOST}]:4317` template with `build_metrics_url`, correctly handling plain hostnames, IPv4, bare IPv6 (brackets them), pre-bracketed IPv6 (strips and re-brackets), and explicit ports. One edge case remains: a malformed input like `"[::1"` (missing closing bracket) bypasses the inner block and then gets double-bracketed by the trailing guard, producing an invalid URL. | | agentex/tests/unit/temporal/test_run_worker_metrics_url.py | New unit tests covering None/empty input, plain hostnames, IPv4, bare IPv6, pre-bracketed IPv6 (including the previously-reported `"[::1]:"` empty-port case), and explicit-port variants. No tests for the malformed `"[::1"` (missing closing bracket) case. | </details> <details><summary><h3>Flowchart</h3></summary> <a href="#gh-light-mode-only"> ```mermaid %%{init: {'theme': 'neutral'}}%% flowchart TD A[build_metrics_url] --> B{host_url falsy?} B -- Yes --> C[return None] B -- No --> D[host = host_url.strip] D --> E{host empty?} E -- Yes --> C E -- No --> F[port = 4317 default] F --> G{host starts with bracket?} G -- Yes --> H[find closing bracket] H --> I{bracket found?} I -- Yes --> J[parse rest after bracket] J --> K{rest starts with colon?} K -- Yes --> L[port = rest after colon or default] K -- No --> M[port stays default] L --> N[host = inner IPv6 address] M --> N I -- No --> O[host unchanged with leading bracket - BUG PATH] G -- No --> P{exactly one colon in host?} P -- Yes --> Q[split host and port] P -- No --> R[host unchanged] Q --> S{colon in host?} N --> S O --> S R --> S S -- Yes --> T[wrap host in brackets] S -- No --> U[return http://host:port] T --> U ``` </a> <a href="#gh-dark-mode-only"> ```mermaid %%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[build_metrics_url] --> B{host_url falsy?} B -- Yes --> C[return None] B -- No --> D[host = host_url.strip] D --> E{host empty?} E -- Yes --> C E -- No --> F[port = 4317 default] F --> G{host starts with bracket?} G -- Yes --> H[find closing bracket] H --> I{bracket found?} I -- Yes --> J[parse rest after bracket] J --> K{rest starts with colon?} K -- Yes --> L[port = rest after colon or default] K -- No --> M[port stays default] L --> N[host = inner IPv6 address] M --> N I -- No --> O[host unchanged with leading bracket - BUG PATH] G -- No --> P{exactly one colon in host?} P -- Yes --> Q[split host and port] P -- No --> R[host unchanged] Q --> S{colon in host?} N --> S O --> S R --> S S -- Yes --> T[wrap host in brackets] S -- No --> U[return http://host:port] T --> U ``` </a> </details> <sub>Reviews (2): Last reviewed commit: ["fix(agentex): default empty OTLP metrics..."](24de57e) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=42166768)</sub> <!-- /greptile_comment -->
1 parent 648e81c commit 38f917b

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

agentex/src/temporal/run_worker.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@
4848

4949
# Task queue name for agentex server operations
5050
AGENTEX_SERVER_TASK_QUEUE = "agentex-server"
51+
OTLP_METRICS_DEFAULT_PORT = 4317
52+
53+
54+
def build_metrics_url(host_url: str | None) -> str | None:
55+
"""Build the worker OTLP metrics URL from DD_AGENT_HOST."""
56+
if not host_url:
57+
return None
58+
59+
host = host_url.strip()
60+
if not host:
61+
return None
62+
63+
port: str | int = OTLP_METRICS_DEFAULT_PORT
64+
if host.startswith("["):
65+
bracket_end = host.find("]")
66+
if bracket_end != -1:
67+
rest = host[bracket_end + 1 :]
68+
port = rest[1:] or port if rest.startswith(":") else port
69+
host = host[1:bracket_end]
70+
elif host.count(":") == 1:
71+
host, port = host.split(":", 1)
72+
73+
if ":" in host:
74+
host = f"[{host}]"
75+
return f"http://{host}:{port}"
5176

5277
# Global worker instance
5378
health_check_worker: Worker | None = None
@@ -93,7 +118,7 @@ async def run_worker(
93118

94119
# Check for metrics configuration
95120
host_url = os.environ.get("DD_AGENT_HOST")
96-
metrics_url = f"http://[{host_url}]:4317" if host_url else None
121+
metrics_url = build_metrics_url(host_url)
97122
if metrics_url:
98123
logger.info(f"Configuring worker with metrics URL: {metrics_url}")
99124

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
from src.temporal.run_worker import build_metrics_url
3+
4+
5+
@pytest.mark.unit
6+
def test_metrics_url_is_none_without_host():
7+
assert build_metrics_url(None) is None
8+
assert build_metrics_url("") is None
9+
assert build_metrics_url(" ") is None
10+
11+
12+
@pytest.mark.unit
13+
@pytest.mark.parametrize("host", ["localhost", "datadog-agent", "10.0.0.5"])
14+
def test_hostname_and_ipv4_hosts_are_not_bracketed(host):
15+
assert build_metrics_url(host) == f"http://{host}:4317"
16+
17+
18+
@pytest.mark.unit
19+
@pytest.mark.parametrize(
20+
("host", "expected"),
21+
[
22+
("::1", "http://[::1]:4317"),
23+
("fe80::1", "http://[fe80::1]:4317"),
24+
("[::1]", "http://[::1]:4317"),
25+
("[::1]:", "http://[::1]:4317"),
26+
],
27+
)
28+
def test_ipv6_literals_are_bracketed(host, expected):
29+
assert build_metrics_url(host) == expected
30+
31+
32+
@pytest.mark.unit
33+
@pytest.mark.parametrize(
34+
("host", "expected"),
35+
[
36+
("datadog-agent:5555", "http://datadog-agent:5555"),
37+
("10.0.0.5:5555", "http://10.0.0.5:5555"),
38+
("[::1]:5555", "http://[::1]:5555"),
39+
],
40+
)
41+
def test_explicit_port_is_preserved(host, expected):
42+
assert build_metrics_url(host) == expected

0 commit comments

Comments
 (0)