1515import logging
1616import os
1717import sched
18+ import sys
1819import threading
1920import time
2021import uuid
2122from enum import IntEnum
2223from pathlib import Path
2324from urllib .parse import urlparse
25+ from urllib .request import url2pathname
2426
2527from newrelic .core .config import _environ_as_bool , _environ_as_int
2628
@@ -66,43 +68,6 @@ class HealthStatus(IntEnum):
6668NR_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-
10671class 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 " )
0 commit comments