Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): enable dynamic JFR stop, delete #176

12 changes: 8 additions & 4 deletions src/main/java/io/cryostat/agent/WebServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ private HttpHandler wrap(HttpHandler handler) {
handler.handle(x);
} catch (Exception e) {
log.error("Unhandled exception", e);
x.sendResponseHeaders(HttpStatus.SC_INTERNAL_SERVER_ERROR, 0);
x.sendResponseHeaders(
HttpStatus.SC_INTERNAL_SERVER_ERROR, RemoteContext.BODY_LENGTH_NONE);
andrewazores marked this conversation as resolved.
Show resolved Hide resolved
x.close();
}
};
Expand All @@ -184,15 +185,18 @@ public void handle(HttpExchange exchange) throws IOException {
case "POST":
synchronized (WebServer.this.credentials) {
executor.execute(registration.get()::tryRegister);
exchange.sendResponseHeaders(HttpStatus.SC_NO_CONTENT, -1);
exchange.sendResponseHeaders(
HttpStatus.SC_NO_CONTENT, RemoteContext.BODY_LENGTH_NONE);
}
break;
case "GET":
exchange.sendResponseHeaders(HttpStatus.SC_NO_CONTENT, -1);
exchange.sendResponseHeaders(
HttpStatus.SC_NO_CONTENT, RemoteContext.BODY_LENGTH_NONE);
break;
default:
log.warn("Unknown request method {}", mtd);
exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1);
exchange.sendResponseHeaders(
HttpStatus.SC_METHOD_NOT_ALLOWED, RemoteContext.BODY_LENGTH_NONE);
break;
}
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class EventTemplatesContext implements RemoteContext {

@Override
public String path() {
return "/event-templates";
return "/event-templates/";
andrewazores marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
Expand All @@ -53,7 +53,7 @@ public void handle(HttpExchange exchange) throws IOException {
switch (mtd) {
case "GET":
try {
exchange.sendResponseHeaders(HttpStatus.SC_OK, 0);
exchange.sendResponseHeaders(HttpStatus.SC_OK, BODY_LENGTH_UNKNOWN);
try (OutputStream response = exchange.getResponseBody()) {
FlightRecorderMXBean bean =
ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class);
Expand All @@ -69,7 +69,8 @@ public void handle(HttpExchange exchange) throws IOException {
break;
default:
log.warn("Unknown request method {}", mtd);
exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1);
exchange.sendResponseHeaders(
HttpStatus.SC_METHOD_NOT_ALLOWED, BODY_LENGTH_NONE);
break;
}
} finally {
Expand Down
10 changes: 6 additions & 4 deletions src/main/java/io/cryostat/agent/remote/EventTypesContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class EventTypesContext implements RemoteContext {

@Override
public String path() {
return "/event-types";
return "/event-types/";
}

@Override
Expand All @@ -59,17 +59,19 @@ public void handle(HttpExchange exchange) throws IOException {
events.addAll(getEventTypes());
} catch (Exception e) {
log.error("events serialization failure", e);
exchange.sendResponseHeaders(HttpStatus.SC_INTERNAL_SERVER_ERROR, 0);
exchange.sendResponseHeaders(
HttpStatus.SC_INTERNAL_SERVER_ERROR, BODY_LENGTH_NONE);
break;
}
exchange.sendResponseHeaders(HttpStatus.SC_OK, 0);
exchange.sendResponseHeaders(HttpStatus.SC_OK, BODY_LENGTH_UNKNOWN);
try (OutputStream response = exchange.getResponseBody()) {
mapper.writeValue(response, events);
}
break;
default:
log.warn("Unknown request method {}", mtd);
exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1);
exchange.sendResponseHeaders(
HttpStatus.SC_METHOD_NOT_ALLOWED, BODY_LENGTH_NONE);
break;
}
} finally {
Expand Down
8 changes: 5 additions & 3 deletions src/main/java/io/cryostat/agent/remote/MBeanContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class MBeanContext implements RemoteContext {

@Override
public String path() {
return "/mbean-metrics";
return "/mbean-metrics/";
}

@Override
Expand All @@ -70,7 +70,8 @@ public void handle(HttpExchange exchange) throws IOException {
case "GET":
try {
MBeanMetrics metrics = getMBeanMetrics();
exchange.sendResponseHeaders(HttpStatus.SC_OK, 0);
exchange.sendResponseHeaders(
HttpStatus.SC_OK, RemoteContext.BODY_LENGTH_UNKNOWN);
try (OutputStream response = exchange.getResponseBody()) {
mapper.writeValue(response, metrics);
}
Expand All @@ -80,7 +81,8 @@ public void handle(HttpExchange exchange) throws IOException {
break;
default:
log.warn("Unknown request method {}", mtd);
exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1);
exchange.sendResponseHeaders(
HttpStatus.SC_METHOD_NOT_ALLOWED, RemoteContext.BODY_LENGTH_NONE);
break;
}
} finally {
Expand Down
155 changes: 123 additions & 32 deletions src/main/java/io/cryostat/agent/remote/RecordingsContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.inject.Inject;
Expand Down Expand Up @@ -50,12 +53,17 @@
import com.sun.net.httpserver.HttpExchange;
import io.smallrye.config.SmallRyeConfig;
import jdk.jfr.FlightRecorder;
import jdk.jfr.Recording;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class RecordingsContext implements RemoteContext {

private static final String PATH = "/recordings";
private static final Pattern PATH_ID_PATTERN =
Pattern.compile("^" + PATH + "/(\\d+)$", Pattern.MULTILINE);

private final Logger log = LoggerFactory.getLogger(getClass());
private final SmallRyeConfig config;
private final ObjectMapper mapper;
Expand All @@ -76,7 +84,7 @@ class RecordingsContext implements RemoteContext {

@Override
public String path() {
return "/recordings";
return PATH;
}

@Override
Expand All @@ -86,60 +94,143 @@ public void handle(HttpExchange exchange) throws IOException {
if (!ensureMethodAccepted(exchange)) {
return;
}
int id = Integer.MIN_VALUE;
switch (mtd) {
case "GET":
try (OutputStream response = exchange.getResponseBody()) {
List<SerializableRecordingDescriptor> recordings = getRecordings();
exchange.sendResponseHeaders(HttpStatus.SC_OK, 0);
mapper.writeValue(response, recordings);
} catch (Exception e) {
log.error("recordings serialization failure", e);
id = extractId(exchange);
if (id == Integer.MIN_VALUE) {
handleGetList(exchange);
} else {
exchange.sendResponseHeaders(
HttpStatus.SC_NOT_IMPLEMENTED, BODY_LENGTH_NONE);
}
break;
case "POST":
try (InputStream body = exchange.getRequestBody()) {
StartRecordingRequest req =
mapper.readValue(body, StartRecordingRequest.class);
if (!req.isValid()) {
exchange.sendResponseHeaders(HttpStatus.SC_BAD_REQUEST, -1);
return;
}
SerializableRecordingDescriptor recording = startRecording(req);
exchange.sendResponseHeaders(HttpStatus.SC_CREATED, 0);
try (OutputStream response = exchange.getResponseBody()) {
mapper.writeValue(response, recording);
}
} catch (QuantityConversionException
| ServiceNotAvailableException
| FlightRecorderException
| org.openjdk.jmc.rjmx.services.jfr.FlightRecorderException
| InvalidEventTemplateException
| InvalidXmlException
| IOException e) {
log.error("Failed to start recording", e);
exchange.sendResponseHeaders(HttpStatus.SC_INTERNAL_SERVER_ERROR, -1);
handleStart(exchange);
break;
case "PATCH":
id = extractId(exchange);
if (id >= 0) {
handleStop(exchange, id);
} else {
exchange.sendResponseHeaders(HttpStatus.SC_BAD_REQUEST, BODY_LENGTH_NONE);
}
break;
case "DELETE":
id = extractId(exchange);
if (id >= 0) {
handleDelete(exchange, id);
} else {
exchange.sendResponseHeaders(HttpStatus.SC_BAD_REQUEST, BODY_LENGTH_NONE);
}
break;
default:
log.warn("Unknown request method {}", mtd);
exchange.sendResponseHeaders(HttpStatus.SC_METHOD_NOT_ALLOWED, -1);
exchange.sendResponseHeaders(
HttpStatus.SC_METHOD_NOT_ALLOWED, BODY_LENGTH_NONE);
break;
}
} finally {
exchange.close();
}
}

private static int extractId(HttpExchange exchange) throws IOException {
Matcher m = PATH_ID_PATTERN.matcher(exchange.getRequestURI().getPath());
if (!m.find()) {
return Integer.MIN_VALUE;
}
return Integer.parseInt(m.group(1));
}

private void handleGetList(HttpExchange exchange) {
try (OutputStream response = exchange.getResponseBody()) {
List<SerializableRecordingDescriptor> recordings = getRecordings();
exchange.sendResponseHeaders(HttpStatus.SC_OK, BODY_LENGTH_UNKNOWN);
mapper.writeValue(response, recordings);
} catch (Exception e) {
log.error("recordings serialization failure", e);
}
}

private void handleStart(HttpExchange exchange) throws IOException {
try (InputStream body = exchange.getRequestBody()) {
StartRecordingRequest req = mapper.readValue(body, StartRecordingRequest.class);
if (!req.isValid()) {
exchange.sendResponseHeaders(HttpStatus.SC_BAD_REQUEST, BODY_LENGTH_NONE);
return;
}
SerializableRecordingDescriptor recording = startRecording(req);
exchange.sendResponseHeaders(HttpStatus.SC_CREATED, BODY_LENGTH_UNKNOWN);
try (OutputStream response = exchange.getResponseBody()) {
mapper.writeValue(response, recording);
}
} catch (QuantityConversionException
| ServiceNotAvailableException
| FlightRecorderException
| org.openjdk.jmc.rjmx.services.jfr.FlightRecorderException
| InvalidEventTemplateException
| InvalidXmlException
| IOException e) {
log.error("Failed to start recording", e);
exchange.sendResponseHeaders(HttpStatus.SC_INTERNAL_SERVER_ERROR, BODY_LENGTH_NONE);
}
}

private void handleStop(HttpExchange exchange, int id) throws IOException {
invokeOnRecording(
exchange,
id,
r -> {
try {
boolean stopped = r.stop();
if (!stopped) {
sendHeader(exchange, HttpStatus.SC_BAD_REQUEST);
} else {
sendHeader(exchange, HttpStatus.SC_NO_CONTENT);
}
} catch (IllegalStateException e) {
sendHeader(exchange, HttpStatus.SC_CONFLICT);
}
});
}

private void handleDelete(HttpExchange exchange, int id) throws IOException {
invokeOnRecording(
exchange,
id,
r -> {
r.close();
sendHeader(exchange, HttpStatus.SC_NO_CONTENT);
});
}

private void invokeOnRecording(HttpExchange exchange, long id, Consumer<Recording> consumer) {
FlightRecorder.getFlightRecorder().getRecordings().stream()
.filter(r -> r.getId() == id)
.findFirst()
.ifPresentOrElse(
consumer::accept, () -> sendHeader(exchange, HttpStatus.SC_NOT_FOUND));
}

private void sendHeader(HttpExchange exchange, int status) {
try {
exchange.sendResponseHeaders(status, BODY_LENGTH_NONE);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}

private boolean ensureMethodAccepted(HttpExchange exchange) throws IOException {
Set<String> blocked = Set.of("POST");
Set<String> alwaysAllowed = Set.of("GET");
String mtd = exchange.getRequestMethod();
boolean restricted = blocked.contains(mtd);
boolean restricted = !alwaysAllowed.contains(mtd);
if (!restricted) {
return true;
}
boolean passed = restricted && MutatingRemoteContext.apiWritesEnabled(config);
if (!passed) {
exchange.sendResponseHeaders(HttpStatus.SC_FORBIDDEN, -1);
exchange.sendResponseHeaders(HttpStatus.SC_FORBIDDEN, BODY_LENGTH_NONE);
}
return passed;
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/cryostat/agent/remote/RemoteContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
import com.sun.net.httpserver.HttpHandler;

public interface RemoteContext extends HttpHandler {

public static final int BODY_LENGTH_NONE = -1;
public static final int BODY_LENGTH_UNKNOWN = 0;

String path();

default boolean available() {
Expand Down
Loading