15
15
import logging
16
16
import os
17
17
import sched
18
+ import sys
18
19
import threading
19
20
import time
20
21
import uuid
21
22
from enum import IntEnum
22
23
from pathlib import Path
23
24
from urllib .parse import urlparse
25
+ from urllib .request import url2pathname
24
26
25
27
from newrelic .core .config import _environ_as_bool , _environ_as_int
26
28
@@ -66,43 +68,6 @@ class HealthStatus(IntEnum):
66
68
NR_CONNECTION_ERROR_CODES = frozenset ([HealthStatus .FAILED_NR_CONNECTION .value , HealthStatus .FORCED_DISCONNECT .value ])
67
69
68
70
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
-
106
71
class AgentControlHealth :
107
72
_instance_lock = threading .Lock ()
108
73
_instance = None
@@ -127,6 +92,7 @@ def __init__(self):
127
92
self .status_message = HEALTHY_STATUS_MESSAGE
128
93
self .start_time_unix_nano = None
129
94
self .pid_file_id_map = {}
95
+ self ._health_delivery_location_cache = {}
130
96
131
97
@property
132
98
def health_check_enabled (self ):
@@ -135,16 +101,87 @@ def health_check_enabled(self):
135
101
if not agent_control_enabled :
136
102
return False
137
103
138
- return is_valid_file_delivery_location ( self .health_delivery_location )
104
+ return self .health_delivery_location_is_valid
139
105
140
106
@property
141
107
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 = (
144
109
os .environ .get ("NEW_RELIC_AGENT_CONTROL_HEALTH_DELIVERY_LOCATION" , "" ) or "file:///newrelic/apm/health"
145
110
)
146
111
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 )
148
185
149
186
@property
150
187
def is_healthy (self ):
@@ -185,13 +222,16 @@ def write_to_health_file(self):
185
222
status_time_unix_nano = time .time_ns ()
186
223
187
224
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
+
189
230
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
193
233
194
- with full_path .open ("w" ) as f :
234
+ with health_file_path .open ("w" ) as f :
195
235
f .write (f"healthy: { is_healthy } \n " )
196
236
f .write (f"status: { self .status_message } \n " )
197
237
f .write (f"start_time_unix_nano: { self .start_time_unix_nano } \n " )
0 commit comments