Skip to content

Commit 58e1d8e

Browse files
committed
Fix handling of health dir paths on Windows
1 parent 3a97d9f commit 58e1d8e

File tree

2 files changed

+111
-53
lines changed

2 files changed

+111
-53
lines changed

newrelic/core/agent_control_health.py

Lines changed: 86 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
import logging
1616
import os
1717
import sched
18+
import sys
1819
import threading
1920
import time
2021
import uuid
2122
from enum import IntEnum
2223
from pathlib import Path
2324
from urllib.parse import urlparse
25+
from urllib.request import url2pathname
2426

2527
from newrelic.core.config import _environ_as_bool, _environ_as_int
2628

@@ -66,43 +68,6 @@ class HealthStatus(IntEnum):
6668
NR_CONNECTION_ERROR_CODES = frozenset([HealthStatus.FAILED_NR_CONNECTION.value, HealthStatus.FORCED_DISCONNECT.value])
6769

6870

69-
def is_valid_file_delivery_location(file_uri):
70-
# Verify whether file directory provided to agent via env var is a valid file URI to determine whether health
71-
# check should run
72-
try:
73-
parsed_uri = urlparse(file_uri)
74-
if not parsed_uri.scheme or not parsed_uri.path:
75-
_logger.warning(
76-
"Configured Agent Control health delivery location is not a complete file URI. Health check will not be "
77-
"enabled. "
78-
)
79-
return False
80-
81-
if parsed_uri.scheme != "file":
82-
_logger.warning(
83-
"Configured Agent Control health delivery location does not have a valid scheme. Health check will not be "
84-
"enabled."
85-
)
86-
return False
87-
88-
path = Path(parsed_uri.path)
89-
90-
# Check if the path exists
91-
if not path.exists():
92-
_logger.warning(
93-
"Configured Agent Control health delivery location does not exist. Health check will not be enabled."
94-
)
95-
return False
96-
97-
return True
98-
99-
except Exception:
100-
_logger.warning(
101-
"Configured Agent Control health delivery location is not valid. Health check will not be enabled."
102-
)
103-
return False
104-
105-
10671
class AgentControlHealth:
10772
_instance_lock = threading.Lock()
10873
_instance = None
@@ -127,6 +92,7 @@ def __init__(self):
12792
self.status_message = HEALTHY_STATUS_MESSAGE
12893
self.start_time_unix_nano = None
12994
self.pid_file_id_map = {}
95+
self._health_delivery_location_cache = {}
13096

13197
@property
13298
def health_check_enabled(self):
@@ -135,16 +101,87 @@ def health_check_enabled(self):
135101
if not agent_control_enabled:
136102
return False
137103

138-
return is_valid_file_delivery_location(self.health_delivery_location)
104+
return self.health_delivery_location_is_valid
139105

140106
@property
141107
def health_delivery_location(self):
142-
# Set a default file path if env var is not set or set to an empty string
143-
health_file_location = (
108+
file_uri = (
144109
os.environ.get("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", "") or "file:///newrelic/apm/health"
145110
)
146111

147-
return health_file_location
112+
# Return from cache if already parsed
113+
if file_uri in self._health_delivery_location_cache:
114+
return self._health_delivery_location_cache[file_uri]
115+
116+
# Parse and add to cache
117+
path = self.parse_health_delivery_location(file_uri)
118+
if path is not None:
119+
self._health_delivery_location_cache[file_uri] = path
120+
121+
return path
122+
123+
@property
124+
def health_delivery_location_is_valid(self):
125+
# Verify whether file directory provided to agent via env var is a valid file URI to determine whether health
126+
# check should run
127+
try:
128+
path = self.health_delivery_location
129+
if path is None:
130+
# Warning already logged in parse_health_delivery_location()
131+
return False
132+
133+
# Check if the path exists
134+
if not path.exists():
135+
_logger.warning(
136+
"Configured Agent Control health delivery location does not exist. Health check will not be enabled."
137+
)
138+
return False
139+
140+
return True
141+
142+
except Exception:
143+
_logger.warning(
144+
"Configured Agent Control health delivery location is not valid. Health check will not be enabled."
145+
)
146+
return False
147+
148+
@classmethod
149+
def parse_health_delivery_location(cls, file_uri):
150+
"""Parse the health delivery location and return it as a Path object."""
151+
152+
# No built in method to correctly parse file URI to a path on Python < 3.13.
153+
# In the future, Path.from_uri() can be used directly.
154+
155+
# For now, parse with urllib.parse.urlparse and convert to a Path object.
156+
parsed_uri = urlparse(file_uri)
157+
158+
# Ensure URI has at least a scheme and path
159+
if not parsed_uri.scheme or not parsed_uri.path:
160+
_logger.warning(
161+
"Configured Agent Control health delivery location is not a complete file URI. Health check will not be enabled."
162+
)
163+
return None
164+
165+
# Ensure URI has a file scheme
166+
if parsed_uri.scheme != "file":
167+
_logger.warning(
168+
"Configured Agent Control health delivery location does not have a valid scheme. Health check will not be enabled."
169+
)
170+
return None
171+
172+
# Handle Windows systems carefully due to inconsistent path handling
173+
if sys.platform == "win32":
174+
if parsed_uri.netloc:
175+
# Matching behavior of pip where netloc is prepended with a double backslash
176+
# https://github.com/pypa/pip/blob/022248f6484fe87dc0ef5aec3437f4c7971fd14b/pip/download.py#L442
177+
urlpathname = url2pathname(rf"\\\\{parsed_uri.netloc}{parsed_uri.path}")
178+
return Path(urlpathname)
179+
else:
180+
# If there's no netloc, we use url2pathname to fix leading slashes
181+
return Path(url2pathname(parsed_uri.path))
182+
else:
183+
# On non-Windows systems we can use the parsed path directly
184+
return Path(parsed_uri.path)
148185

149186
@property
150187
def is_healthy(self):
@@ -185,13 +222,16 @@ def write_to_health_file(self):
185222
status_time_unix_nano = time.time_ns()
186223

187224
try:
188-
file_path = urlparse(self.health_delivery_location).path
225+
health_dir_path = self.health_delivery_location
226+
if health_dir_path is None:
227+
# Allow except block to handle logging a warning
228+
raise ValueError("Health delivery location is not valid.")
229+
189230
file_id = self.get_file_id()
190-
file_name = f"health-{file_id}.yml"
191-
full_path = Path(file_path) / file_name
192-
is_healthy = self.is_healthy
231+
health_file_path = health_dir_path / f"health-{file_id}.yml"
232+
is_healthy = self.is_healthy # Cache property value to avoid multiple calls
193233

194-
with full_path.open("w") as f:
234+
with health_file_path.open("w") as f:
195235
f.write(f"healthy: {is_healthy}\n")
196236
f.write(f"status: {self.status_message}\n")
197237
f.write(f"start_time_unix_nano: {self.start_time_unix_nano}\n")

tests/agent_features/test_agent_control_health_check.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,12 @@
1717
from pathlib import Path
1818

1919
import pytest
20+
import sys
2021
from testing_support.fixtures import initialize_agent
2122
from testing_support.http_client_recorder import HttpClientRecorder
2223

2324
from newrelic.config import _reset_configuration_done, initialize
24-
from newrelic.core.agent_control_health import (
25-
HealthStatus,
26-
agent_control_health_instance,
27-
is_valid_file_delivery_location,
28-
)
25+
from newrelic.core.agent_control_health import HealthStatus, agent_control_health_instance
2926
from newrelic.core.agent_protocol import AgentProtocol
3027
from newrelic.core.application import Application
3128
from newrelic.core.config import finalize_application_settings, global_settings
@@ -41,8 +38,29 @@ def get_health_file_contents(tmp_path):
4138

4239

4340
@pytest.mark.parametrize("file_uri", ["", "file://", "/test/dir", "foo:/test/dir"])
44-
def test_invalid_file_directory_supplied(file_uri):
45-
assert not is_valid_file_delivery_location(file_uri)
41+
def test_invalid_file_directory_supplied(monkeypatch, file_uri):
42+
# Setup expected env vars to run agent control health check
43+
monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True")
44+
monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_uri)
45+
46+
agent_control_instance = agent_control_health_instance()
47+
assert not agent_control_instance.health_delivery_location_is_valid
48+
49+
50+
@pytest.mark.skipif(sys.platform != "win32", reason="Only valid for Windows")
51+
@pytest.mark.parametrize("leading_slash", [True, False], ids=["leading_slash", "no_leading_slash"])
52+
def test_inconsistent_paths_on_windows(monkeypatch, tmp_path, leading_slash):
53+
file_uri = tmp_path.as_uri()
54+
if not leading_slash:
55+
assert file_uri.startswith("file:///")
56+
file_uri.replace("file:///", "file://")
57+
58+
# Setup expected env vars to run agent control health check
59+
monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_ENABLED", "True")
60+
monkeypatch.setenv("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION", file_uri)
61+
62+
agent_control_instance = agent_control_health_instance()
63+
assert agent_control_instance.health_delivery_location_is_valid
4664

4765

4866
def test_agent_control_not_enabled(monkeypatch, tmp_path):

0 commit comments

Comments
 (0)