Skip to content

Commit

Permalink
feat(agent): implement Agent HTTP dynamic JFR start (#1566)
Browse files Browse the repository at this point in the history
* chore(svc): extract EventOptionsBuilder to -core and use new CryostatFlightRecorderService

* add unimplemented overrides

* test(smoketest): enable API writes on one agent-equipped sample app

* chore(serial): extract recording descriptor to -core

* chore(activerecordings): clean up an error handler

* feat(agent): implement dynamic start of JFR over HTTP

* bump -core version
  • Loading branch information
andrewazores committed Sep 19, 2023
1 parent f4a314b commit 9e5e1a2
Show file tree
Hide file tree
Showing 24 changed files with 238 additions and 409 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
<com.google.dagger.version>2.47</com.google.dagger.version>
<com.google.dagger.compiler.version>${com.google.dagger.version}</com.google.dagger.compiler.version>

<io.cryostat.core.version>2.21.1</io.cryostat.core.version>
<io.cryostat.core.version>2.22.0</io.cryostat.core.version>

<org.openjdk.nashorn.core.version>15.4</org.openjdk.nashorn.core.version>
<org.apache.commons.lang3.version>3.12.0</org.apache.commons.lang3.version>
Expand Down
1 change: 1 addition & 0 deletions smoketest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ runDemoApps() {
--env CRYOSTAT_AGENT_TRUST_ALL="true" \
--env CRYOSTAT_AGENT_AUTHORIZATION="Basic $(echo user:pass | base64)" \
--env CRYOSTAT_AGENT_REGISTRATION_PREFER_JMX="false" \
--env CRYOSTAT_AGENT_API_WRITES_ENABLED="true" \
--rm -d quay.io/andrewazores/quarkus-test:latest

# copy a jboss-client.jar into /clientlib first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor.RecordingState;

import io.cryostat.core.serialization.SerializableRecordingDescriptor;
import io.cryostat.recordings.RecordingMetadataManager.Metadata;

import org.apache.commons.lang3.builder.EqualsBuilder;
Expand Down

This file was deleted.

159 changes: 51 additions & 108 deletions src/main/java/io/cryostat/net/AgentClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,13 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;

import javax.management.ObjectName;
import javax.script.ScriptException;

import org.openjdk.jmc.common.unit.IConstrainedMap;
import org.openjdk.jmc.common.unit.IConstraint;
import org.openjdk.jmc.common.unit.IOptionDescriptor;
import org.openjdk.jmc.common.unit.IQuantity;
import org.openjdk.jmc.common.unit.QuantityConversionException;
import org.openjdk.jmc.common.unit.SimpleConstrainedMap;
import org.openjdk.jmc.common.unit.UnitLookup;
import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID;
import org.openjdk.jmc.flightrecorder.configuration.events.IEventTypeID;
import org.openjdk.jmc.flightrecorder.configuration.internal.EventTypeIDV2;
Expand All @@ -45,10 +42,14 @@
import io.cryostat.core.log.Logger;
import io.cryostat.core.net.Credentials;
import io.cryostat.core.net.MBeanMetrics;
import io.cryostat.core.serialization.SerializableRecordingDescriptor;
import io.cryostat.net.AgentJFRService.StartRecordingRequest;
import io.cryostat.util.HttpStatusCodeIdentifier;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
Expand Down Expand Up @@ -106,19 +107,40 @@ Future<MBeanMetrics> mbeanMetrics() {
.map(s -> gson.fromJson(s, MBeanMetrics.class));
}

Future<IRecordingDescriptor> startRecording(StartRecordingRequest req) {
Future<HttpResponse<String>> f =
invoke(
HttpMethod.POST,
"/recordings",
Buffer.buffer(gson.toJson(req)),
BodyCodec.string());
return f.map(
resp -> {
int statusCode = resp.statusCode();
if (HttpStatusCodeIdentifier.isSuccessCode(statusCode)) {
String body = resp.body();
return gson.fromJson(body, SerializableRecordingDescriptor.class)
.toJmcForm();
} else if (statusCode == 403) {
throw new UnsupportedOperationException();
} else {
throw new RuntimeException("Unknown failure");
}
});
}

Future<List<IRecordingDescriptor>> activeRecordings() {
Future<HttpResponse<JsonArray>> f =
invoke(HttpMethod.GET, "/recordings", BodyCodec.jsonArray());
Future<HttpResponse<String>> f = invoke(HttpMethod.GET, "/recordings", BodyCodec.string());
return f.map(HttpResponse::body)
.map(
arr ->
arr.stream()
.map(
o ->
(IRecordingDescriptor)
new AgentRecordingDescriptor(
(JsonObject) o))
.toList());
s ->
(List<SerializableRecordingDescriptor>)
gson.fromJson(
s,
new TypeToken<
List<
SerializableRecordingDescriptor>>() {}.getType()))
.map(arr -> arr.stream().map(SerializableRecordingDescriptor::toJmcForm).toList());
}

Future<Collection<? extends IEventTypeInfo>> eventTypes() {
Expand Down Expand Up @@ -189,6 +211,11 @@ Future<List<String>> eventTemplates() {
}

private <T> Future<HttpResponse<T>> invoke(HttpMethod mtd, String path, BodyCodec<T> codec) {
return invoke(mtd, path, null, codec);
}

private <T> Future<HttpResponse<T>> invoke(
HttpMethod mtd, String path, Buffer payload, BodyCodec<T> codec) {
return Future.fromCompletionStage(
CompletableFuture.supplyAsync(
() -> {
Expand Down Expand Up @@ -227,10 +254,17 @@ private <T> Future<HttpResponse<T>> invoke(HttpMethod mtd, String path, BodyCode
}

try {
return req.send()
.toCompletionStage()
.toCompletableFuture()
.get();
if (payload != null) {
return req.sendBuffer(payload)
.toCompletionStage()
.toCompletableFuture()
.get();
} else {
return req.send()
.toCompletionStage()
.toCompletableFuture()
.get();
}
} catch (InterruptedException | ExecutionException e) {
logger.error(e);
throw new RuntimeException(e);
Expand Down Expand Up @@ -273,97 +307,6 @@ AgentClient create(URI agentUri) {
}
}

private static class AgentRecordingDescriptor implements IRecordingDescriptor {

final JsonObject json;

AgentRecordingDescriptor(JsonObject json) {
this.json = json;
}

@Override
public IQuantity getDataStartTime() {
return getStartTime();
}

@Override
public IQuantity getDataEndTime() {
if (isContinuous()) {
return UnitLookup.EPOCH_MS.quantity(0);
}
return getDataStartTime().add(getDuration());
}

@Override
public IQuantity getDuration() {
return UnitLookup.MILLISECOND.quantity(json.getLong("duration"));
}

@Override
public Long getId() {
return json.getLong("id");
}

@Override
public IQuantity getMaxAge() {
return UnitLookup.MILLISECOND.quantity(json.getLong("maxAge"));
}

@Override
public IQuantity getMaxSize() {
return UnitLookup.BYTE.quantity(json.getLong("maxSize"));
}

@Override
public String getName() {
return json.getString("name");
}

@Override
public ObjectName getObjectName() {
return null;
}

@Override
public Map<String, ?> getOptions() {
return json.getJsonObject("options").getMap();
}

@Override
public IQuantity getStartTime() {
return UnitLookup.EPOCH_MS.quantity(json.getLong("startTime"));
}

@Override
public RecordingState getState() {
// avoid using Enum.valueOf() since that throws an exception if the name isn't part of
// the type, and it's nicer to not throw and catch exceptions
String state = json.getString("state");
switch (state) {
case "CREATED":
return RecordingState.CREATED;
case "RUNNING":
return RecordingState.RUNNING;
case "STOPPING":
return RecordingState.STOPPING;
case "STOPPED":
return RecordingState.STOPPED;
default:
return RecordingState.RUNNING;
}
}

@Override
public boolean getToDisk() {
return json.getBoolean("toDisk");
}

@Override
public boolean isContinuous() {
return json.getBoolean("isContinuous");
}
}

private static class AgentEventTypeInfo implements IEventTypeInfo {

final JsonObject json;
Expand Down
Loading

0 comments on commit 9e5e1a2

Please sign in to comment.