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

New rule definitions trigger on existing targets #538

Merged
merged 10 commits into from
Jul 2, 2021
2 changes: 2 additions & 0 deletions src/main/java/io/cryostat/MainModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import io.cryostat.net.NetworkModule;
import io.cryostat.net.web.http.HttpMimeType;
import io.cryostat.platform.PlatformModule;
import io.cryostat.recordings.RecordingsModule;
import io.cryostat.rules.Rule;
import io.cryostat.rules.RulesModule;
import io.cryostat.sys.SystemModule;
Expand All @@ -80,6 +81,7 @@
CommandsModule.class,
TemplatesModule.class,
RulesModule.class,
RecordingsModule.class,
})
public abstract class MainModule {
public static final String RECORDINGS_PATH = "RECORDINGS_PATH";
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/io/cryostat/net/ConnectionDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.util.Optional;

import io.cryostat.core.net.Credentials;
import io.cryostat.platform.ServiceRef;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
Expand All @@ -49,10 +50,18 @@ public class ConnectionDescriptor {
private final String targetId;
private final Optional<Credentials> credentials;

public ConnectionDescriptor(ServiceRef serviceRef) {
this(serviceRef.getServiceUri().toString());
}

public ConnectionDescriptor(String targetId) {
this(targetId, null);
}

public ConnectionDescriptor(ServiceRef serviceRef, Credentials credentials) {
this(serviceRef.getServiceUri().toString(), credentials);
}

public ConnectionDescriptor(String targetId, Credentials credentials) {
this.targetId = targetId;
this.credentials = Optional.ofNullable(credentials);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
Expand All @@ -48,26 +47,22 @@
import javax.inject.Inject;
import javax.inject.Provider;

import org.openjdk.jmc.common.unit.IConstrainedMap;
import org.openjdk.jmc.common.unit.QuantityConversionException;
import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID;
import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder;
import org.openjdk.jmc.rjmx.services.jfr.IEventTypeInfo;
import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;

import io.cryostat.commands.internal.EventOptionsBuilder;
import io.cryostat.commands.internal.RecordingOptionsBuilderFactory;
import io.cryostat.core.net.JFRConnection;
import io.cryostat.core.templates.Template;
import io.cryostat.core.templates.TemplateType;
import io.cryostat.jmc.serialization.HyperlinkedSerializableRecordingDescriptor;
import io.cryostat.messaging.notifications.NotificationFactory;
import io.cryostat.net.AuthManager;
import io.cryostat.net.ConnectionDescriptor;
import io.cryostat.net.TargetConnectionManager;
import io.cryostat.net.web.WebServer;
import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler;
import io.cryostat.net.web.http.HttpMimeType;
import io.cryostat.net.web.http.api.ApiVersion;
import io.cryostat.recordings.RecordingCreationHelper;

import com.google.gson.Gson;
import io.vertx.core.MultiMap;
Expand All @@ -76,45 +71,31 @@
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.impl.HttpStatusException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;

public class TargetRecordingsPostHandler extends AbstractAuthenticatedRequestHandler {

// TODO extract this somewhere more appropriate
public static final Template ALL_EVENTS_TEMPLATE =
new Template(
"ALL",
"Enable all available events in the target JVM, with default option values. This will be very expensive and is intended primarily for testing Cryostat's own capabilities.",
"Cryostat",
TemplateType.TARGET);

private static final Pattern TEMPLATE_PATTERN =
Pattern.compile("^template=([\\w]+)(?:,type=([\\w]+))?$");

static final String PATH = "targets/:targetId/recordings";
private final TargetConnectionManager targetConnectionManager;
private final RecordingCreationHelper recordingCreationHelper;
private final RecordingOptionsBuilderFactory recordingOptionsBuilderFactory;
private final EventOptionsBuilder.Factory eventOptionsBuilderFactory;
private final Provider<WebServer> webServerProvider;
private final Gson gson;
private final NotificationFactory notificationFactory;
private static final String NOTIFICATION_CATEGORY = "RecordingCreated";

@Inject
TargetRecordingsPostHandler(
AuthManager auth,
TargetConnectionManager targetConnectionManager,
RecordingCreationHelper recordingCreationHelper,
RecordingOptionsBuilderFactory recordingOptionsBuilderFactory,
EventOptionsBuilder.Factory eventOptionsBuilderFactory,
Provider<WebServer> webServerProvider,
Gson gson,
NotificationFactory notificationFactory) {
Gson gson) {
super(auth);
this.targetConnectionManager = targetConnectionManager;
this.recordingCreationHelper = recordingCreationHelper;
this.recordingOptionsBuilderFactory = recordingOptionsBuilderFactory;
this.eventOptionsBuilderFactory = eventOptionsBuilderFactory;
this.webServerProvider = webServerProvider;
this.gson = gson;
this.notificationFactory = notificationFactory;
}

@Override
Expand Down Expand Up @@ -150,18 +131,11 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception {
}

try {
Optional<HyperlinkedSerializableRecordingDescriptor> descriptor =
ConnectionDescriptor connectionDescriptor = getConnectionDescriptorFromContext(ctx);
HyperlinkedSerializableRecordingDescriptor linkedDescriptor =
hareetd marked this conversation as resolved.
Show resolved Hide resolved
targetConnectionManager.executeConnectedTask(
getConnectionDescriptorFromContext(ctx),
connectionDescriptor,
connection -> {
if (getDescriptorByName(connection, recordingName).isPresent()) {
throw new HttpStatusException(
400,
String.format(
"Recording with name \"%s\" already exists",
recordingName));
}

RecordingOptionsBuilder builder =
recordingOptionsBuilderFactory
.create(connection.getService())
Expand All @@ -185,57 +159,34 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception {
if (attrs.contains("maxSize")) {
builder = builder.maxSize(Long.parseLong(attrs.get("maxSize")));
}
IConstrainedMap<String> recordingOptions = builder.build();
connection
.getService()
.start(
recordingOptions,
enableEvents(connection, eventSpecifier));
notificationFactory
.createBuilder()
.metaCategory(NOTIFICATION_CATEGORY)
.metaType(HttpMimeType.JSON)
.message(
Map.of(
"recording",
recordingName,
"target",
getConnectionDescriptorFromContext(ctx)
.getTargetId()))
.build()
.send();
return getDescriptorByName(connection, recordingName)
.map(
d -> {
try {
WebServer webServer =
webServerProvider.get();
return new HyperlinkedSerializableRecordingDescriptor(
d,
webServer.getDownloadURL(
connection, d.getName()),
webServer.getReportURL(
connection, d.getName()));
} catch (QuantityConversionException
| URISyntaxException
| IOException e) {
throw new HttpStatusException(500, e);
}
});
Pair<String, TemplateType> template =
RecordingCreationHelper.parseEventSpecifierToTemplate(
eventSpecifier);
IRecordingDescriptor descriptor =
recordingCreationHelper.startRecording(
connectionDescriptor,
builder.build(),
template.getLeft(),
template.getRight());
try {
WebServer webServer = webServerProvider.get();
return new HyperlinkedSerializableRecordingDescriptor(
descriptor,
webServer.getDownloadURL(
connection, descriptor.getName()),
webServer.getReportURL(
connection, descriptor.getName()));
} catch (QuantityConversionException
| URISyntaxException
| IOException e) {
throw new HttpStatusException(500, e);
}
});

descriptor.ifPresentOrElse(
linkedDescriptor -> {
ctx.response().setStatusCode(201);
ctx.response().putHeader(HttpHeaders.LOCATION, "/" + recordingName);
ctx.response()
.putHeader(HttpHeaders.CONTENT_TYPE, HttpMimeType.JSON.mime());
ctx.response().end(gson.toJson(linkedDescriptor));
},
() -> {
throw new HttpStatusException(
500, "Unexpected failure to create recording");
hareetd marked this conversation as resolved.
Show resolved Hide resolved
});
ctx.response().setStatusCode(201);
ctx.response().putHeader(HttpHeaders.LOCATION, "/" + recordingName);
ctx.response().putHeader(HttpHeaders.CONTENT_TYPE, HttpMimeType.JSON.mime());
ctx.response().end(gson.toJson(linkedDescriptor));
} catch (NumberFormatException nfe) {
throw new HttpStatusException(
400, String.format("Invalid argument: %s", nfe.getMessage()), nfe);
Expand All @@ -250,59 +201,4 @@ protected Optional<IRecordingDescriptor> getDescriptorByName(
.filter(recording -> recording.getName().equals(recordingName))
.findFirst();
}

protected IConstrainedMap<EventOptionID> enableEvents(JFRConnection connection, String events)
throws Exception {
Matcher m = TEMPLATE_PATTERN.matcher(events);
m.find();
String templateName = m.group(1);
String typeName = m.group(2);
if (ALL_EVENTS_TEMPLATE.getName().equals(templateName)) {
return enableAllEvents(connection);
}
if (typeName != null) {
return connection
.getTemplateService()
.getEvents(templateName, TemplateType.valueOf(typeName))
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"No template \"%s\" found with type %s",
templateName, typeName)));
}
// if template type not specified, try to find a Custom template by that name. If none,
// fall back on finding a Target built-in template by the name. If not, throw an
// exception and bail out.
return connection
.getTemplateService()
.getEvents(templateName, TemplateType.CUSTOM)
.or(
() -> {
try {
return connection
.getTemplateService()
.getEvents(templateName, TemplateType.TARGET);
} catch (Exception e) {
return Optional.empty();
}
})
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Invalid/unknown event template %s",
templateName)));
}

protected IConstrainedMap<EventOptionID> enableAllEvents(JFRConnection connection)
throws Exception {
EventOptionsBuilder builder = eventOptionsBuilderFactory.create(connection);

for (IEventTypeInfo eventTypeInfo : connection.getService().getAvailableEventTypes()) {
builder.addEvent(eventTypeInfo.getEventTypeID().getFullKey(), "enabled", "true");
}

return builder.build();
}
}
Loading