Skip to content

Commit d8e5e7b

Browse files
jansyk13cursoragent
andcommitted
feat: add graceful startup error handling to Java agent
Add a `graceful:` agent argument prefix that keeps the JVM running when the JMX exporter fails to start. The error is still logged and any started resources are closed, but System.exit(1) is skipped. Default behavior remains fail-fast for backward compatibility. This only affects startup errors; scrape-time errors continue to be reported via the jmx_scrape_error metric. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Jan Sykora <jan@cast.ai>
1 parent f26b115 commit d8e5e7b

5 files changed

Lines changed: 406 additions & 71 deletions

File tree

jmx_prometheus_javaagent/src/main/java/io/prometheus/jmx/Arguments.java

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public class Arguments {
5555
*/
5656
private static final String DEFAULT_HOST = "0.0.0.0";
5757

58+
/**
59+
* Prefix that enables graceful error handling for startup errors.
60+
*/
61+
private static final String GRACEFUL_PREFIX = "graceful:";
62+
5863
/**
5964
* Regular expression pattern for parsing agent arguments.
6065
*
@@ -102,6 +107,15 @@ public class Arguments {
102107
*/
103108
private final String filename;
104109

110+
/**
111+
* Flag indicating whether graceful error handling is enabled.
112+
*
113+
* <p>When {@code true}, startup errors are logged and resources are cleaned up without calling
114+
* {@code System.exit(1)}. When {@code false}, the agent fails fast and exits the JVM on startup
115+
* errors.
116+
*/
117+
private final boolean gracefulErrorHandling;
118+
105119
/**
106120
* Constructs an Arguments instance with the specified configuration.
107121
*
@@ -110,13 +124,15 @@ public class Arguments {
110124
* disabled
111125
* @param port the port number for HTTP server, may be {@code null} when HTTP is disabled
112126
* @param filename the path to the configuration file, must not be {@code null}
127+
* @param gracefulErrorHandling whether graceful error handling is enabled
113128
* @throws NullPointerException if {@code filename} is {@code null}
114129
*/
115-
private Arguments(boolean httpEnabled, String host, Integer port, String filename) {
130+
private Arguments(boolean httpEnabled, String host, Integer port, String filename, boolean gracefulErrorHandling) {
116131
this.httpEnabled = httpEnabled;
117132
this.host = host;
118133
this.port = port;
119134
this.filename = Objects.requireNonNull(filename, "filename cannot be null");
135+
this.gracefulErrorHandling = gracefulErrorHandling;
120136
}
121137

122138
/**
@@ -169,6 +185,18 @@ public String getFilename() {
169185
return filename;
170186
}
171187

188+
/**
189+
* Returns whether graceful error handling is enabled.
190+
*
191+
* <p>When {@code true}, startup errors are logged and resources are cleaned up without calling
192+
* {@code System.exit(1)}.
193+
*
194+
* @return {@code true} if graceful error handling is enabled, {@code false} otherwise
195+
*/
196+
public boolean isGracefulErrorHandling() {
197+
return gracefulErrorHandling;
198+
}
199+
172200
/**
173201
* Parses the Java agent argument string into an Arguments instance.
174202
*
@@ -178,6 +206,9 @@ public String getFilename() {
178206
* <li>{@code port:configFile} - Enables HTTP on default host (0.0.0.0) and specified port
179207
* <li>{@code host:port:configFile} - Enables HTTP on specified host and port
180208
* <li>{@code configFile} - Disables HTTP (only OpenTelemetry export possible via config)
209+
* <li>{@code graceful:port:configFile} - Enables HTTP and graceful error handling
210+
* <li>{@code graceful:host:port:configFile} - Enables HTTP on specified host/port and graceful error handling
211+
* <li>{@code graceful:configFile} - Disables HTTP and enables graceful error handling
181212
* </ul>
182213
*
183214
* <p>Host can be a hostname, IPv4 address, or IPv6 address enclosed in square brackets.
@@ -191,14 +222,64 @@ public static Arguments parse(String agentArgument) {
191222
throw new ConfigurationException(format("Malformed arguments [%s]", agentArgument));
192223
}
193224

194-
Pattern pattern = Pattern.compile(CONFIGURATION_REGEX);
195-
Matcher matcher = pattern.matcher(agentArgument);
225+
boolean gracefulErrorHandling = parseGracefulPrefix(agentArgument);
226+
String remainingArgument = stripGracefulPrefix(agentArgument);
227+
228+
ParsedHttpArguments parsedHttpArguments = parseHttpArguments(remainingArgument);
196229

230+
return new Arguments(
231+
parsedHttpArguments.httpEnabled,
232+
parsedHttpArguments.host,
233+
parsedHttpArguments.port,
234+
parsedHttpArguments.filename,
235+
gracefulErrorHandling);
236+
}
237+
238+
/**
239+
* Determines whether the agent argument starts with the graceful error handling prefix.
240+
*
241+
* @param agentArgument the agent argument string to check, must not be {@code null}
242+
* @return {@code true} if the argument starts with {@code graceful:}, {@code false} otherwise
243+
*/
244+
private static boolean parseGracefulPrefix(String agentArgument) {
245+
return agentArgument.startsWith(GRACEFUL_PREFIX);
246+
}
247+
248+
/**
249+
* Strips the optional graceful error handling prefix from the agent argument.
250+
*
251+
* @param agentArgument the agent argument string, must not be {@code null}
252+
* @return the argument string with the graceful prefix removed, if present
253+
* @throws ConfigurationException if only the graceful prefix was provided
254+
*/
255+
private static String stripGracefulPrefix(String agentArgument) {
256+
if (agentArgument.startsWith(GRACEFUL_PREFIX)) {
257+
String remaining = agentArgument.substring(GRACEFUL_PREFIX.length());
258+
if (remaining.isEmpty()) {
259+
throw new ConfigurationException(format("Malformed arguments [%s]", remaining));
260+
}
261+
return remaining;
262+
}
263+
return agentArgument;
264+
}
265+
266+
/**
267+
* Parses the host, port, and filename from the agent argument after any graceful prefix has been
268+
* removed.
269+
*
270+
* @param agentArgument the agent argument string with graceful prefix already stripped
271+
* @return a {@link ParsedHttpArguments} containing the parsed values
272+
* @throws ConfigurationException if the argument is malformed
273+
*/
274+
private static ParsedHttpArguments parseHttpArguments(String agentArgument) {
197275
boolean httpEnabled = false;
198276
String host = null;
199277
Integer port = null;
200278
String filename;
201279

280+
Pattern pattern = Pattern.compile(CONFIGURATION_REGEX);
281+
Matcher matcher = pattern.matcher(agentArgument);
282+
202283
if (matcher.matches()) {
203284
httpEnabled = true;
204285

@@ -225,6 +306,24 @@ public static Arguments parse(String agentArgument) {
225306
filename = agentArgument;
226307
}
227308

228-
return new Arguments(httpEnabled, host, port, filename);
309+
return new ParsedHttpArguments(httpEnabled, host, port, filename);
310+
}
311+
312+
/**
313+
* Simple holder for HTTP-related parsed arguments.
314+
*/
315+
private static class ParsedHttpArguments {
316+
317+
final boolean httpEnabled;
318+
final String host;
319+
final Integer port;
320+
final String filename;
321+
322+
ParsedHttpArguments(boolean httpEnabled, String host, Integer port, String filename) {
323+
this.httpEnabled = httpEnabled;
324+
this.host = host;
325+
this.port = port;
326+
this.filename = filename;
327+
}
229328
}
230329
}

jmx_prometheus_javaagent/src/main/java/io/prometheus/jmx/JavaAgent.java

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.lang.instrument.Instrumentation;
3636
import java.lang.management.ManagementFactory;
3737
import java.net.InetAddress;
38+
import java.util.function.IntConsumer;
3839

3940
/**
4041
* Java agent for the Prometheus JMX exporter.
@@ -128,8 +129,15 @@ public static void agentmain(String agentArgument, Instrumentation instrumentati
128129
* invalid
129130
*/
130131
public static void premain(String agentArgument, Instrumentation instrumentation) {
132+
Arguments arguments;
133+
try {
134+
arguments = Arguments.parse(agentArgument);
135+
} catch (Throwable t) {
136+
handleError(t, null, null, false);
137+
return;
138+
}
139+
131140
try {
132-
Arguments arguments = Arguments.parse(agentArgument);
133141
File file = new File(arguments.getFilename());
134142
MapAccessor mapAccessor = MapAccessor.of(YamlSupport.loadYaml(file));
135143

@@ -149,7 +157,7 @@ public static void premain(String agentArgument, Instrumentation instrumentation
149157
start(arguments, file, mapAccessor);
150158
}
151159
} catch (Throwable t) {
152-
handleError(t, null, null);
160+
handleError(t, null, null, arguments.isGracefulErrorHandling());
153161
}
154162
}
155163

@@ -174,6 +182,7 @@ public static void premain(String agentArgument, Instrumentation instrumentation
174182
* {@code null}
175183
*/
176184
private static void startAsync(int startDelaySeconds, Arguments arguments, File file, MapAccessor mapAccessor) {
185+
boolean gracefulErrorHandling = arguments.isGracefulErrorHandling();
177186
Thread thread = new Thread(
178187
() -> {
179188
try {
@@ -183,7 +192,7 @@ private static void startAsync(int startDelaySeconds, Arguments arguments, File
183192
Thread.currentThread().interrupt();
184193
LOGGER.warn("Startup delay of %d seconds interrupted", startDelaySeconds);
185194
} catch (Throwable t) {
186-
handleError(t, null, null);
195+
handleError(t, null, null, gracefulErrorHandling);
187196
}
188197
},
189198
THREAD_NAME);
@@ -249,7 +258,7 @@ static void start(Arguments arguments, File file, MapAccessor mapAccessor) {
249258

250259
LOGGER.info("Running ...");
251260
} catch (Throwable t) {
252-
handleError(t, openTelemetryExporter, httpServer);
261+
handleError(t, openTelemetryExporter, httpServer, arguments.isGracefulErrorHandling());
253262
}
254263
}
255264

@@ -306,36 +315,64 @@ private static OpenTelemetryExporter startOpenTelemetryExporter(File file) throw
306315
}
307316

308317
/**
309-
* Handles a startup failure by logging the error and cleaning up resources.
318+
* Handles a startup failure by logging the error, cleaning up resources, and optionally exiting
319+
* the JVM.
310320
*
311-
* <p>This method:
321+
* <p>When {@code gracefulErrorHandling} is {@code false}, this method prints the error, closes
322+
* resources, and calls {@code System.exit(1)}. When {@code gracefulErrorHandling} is {@code
323+
* true}, it prints the error and closes resources but does not exit the JVM.
312324
*
313-
* <ul>
314-
* <li>Prints the error stack trace to stderr (synchronized to prevent interleaving)
315-
* <li>Closes any started resources (OpenTelemetry exporter, HTTP server)
316-
* <li>Exits the JVM with status code 1
317-
* </ul>
325+
* @param t the throwable that caused the failure, may be {@code null}
326+
* @param openTelemetryExporter the OpenTelemetry exporter to close, may be {@code null}
327+
* @param httpServer the HTTP server to close, may be {@code null}
328+
* @param gracefulErrorHandling whether to skip the JVM exit
329+
*/
330+
private static void handleError(
331+
Throwable t,
332+
OpenTelemetryExporter openTelemetryExporter,
333+
HTTPServer httpServer,
334+
boolean gracefulErrorHandling) {
335+
handleError(t, openTelemetryExporter, httpServer, gracefulErrorHandling, System::exit);
336+
}
337+
338+
/**
339+
* Handles a startup failure by logging the error, cleaning up resources, and optionally exiting
340+
* the JVM.
318341
*
319-
* <p>This method never returns; it always calls {@code System.exit(1)}.
342+
* <p>This overload allows tests to inject an exit action instead of calling {@link
343+
* System#exit(int)}.
320344
*
321345
* @param t the throwable that caused the failure, may be {@code null}
322346
* @param openTelemetryExporter the OpenTelemetry exporter to close, may be {@code null}
323347
* @param httpServer the HTTP server to close, may be {@code null}
348+
* @param gracefulErrorHandling if {@code true}, log and clean up without exiting the JVM
349+
* @param exit the exit action to invoke when not in graceful mode
324350
*/
325-
private static void handleError(Throwable t, OpenTelemetryExporter openTelemetryExporter, HTTPServer httpServer) {
351+
static void handleError(
352+
Throwable t,
353+
OpenTelemetryExporter openTelemetryExporter,
354+
HTTPServer httpServer,
355+
boolean gracefulErrorHandling,
356+
IntConsumer exit) {
326357
synchronized (System.err) {
327358
System.err.println("Failed to start Prometheus JMX Exporter ...");
328359
System.err.println();
329360
t.printStackTrace(System.err);
330361
System.err.println();
331-
System.err.println("Prometheus JMX Exporter exiting");
362+
if (gracefulErrorHandling) {
363+
System.err.println("Prometheus JMX Exporter continuing in graceful error handling mode");
364+
} else {
365+
System.err.println("Prometheus JMX Exporter exiting");
366+
}
332367
System.err.flush();
333368
}
334369

335370
close(openTelemetryExporter);
336371
close(httpServer);
337372

338-
System.exit(1);
373+
if (!gracefulErrorHandling) {
374+
exit.accept(1);
375+
}
339376
}
340377

341378
/**

0 commit comments

Comments
 (0)