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

refactor(auto-rules): Periodic archiver should not use http req to self #557

23 changes: 3 additions & 20 deletions src/main/java/io/cryostat/net/web/http/api/v1/HttpApiV1Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,10 @@
*/
package io.cryostat.net.web.http.api.v1;

import java.nio.file.Path;

import javax.inject.Named;

import io.cryostat.MainModule;
import io.cryostat.core.sys.Clock;
import io.cryostat.core.sys.FileSystem;
import io.cryostat.messaging.notifications.NotificationFactory;
import io.cryostat.net.TargetConnectionManager;
import io.cryostat.net.web.http.RequestHandler;
import io.cryostat.platform.PlatformClient;
import io.cryostat.recordings.RecordingArchiveHelper;

import dagger.Binds;
import dagger.Module;
Expand Down Expand Up @@ -99,19 +92,9 @@ abstract RequestHandler bindTargetRecordingPatchBodyHandler(

@Provides
static TargetRecordingPatchSave provideTargetRecordingPatchSave(
FileSystem fs,
@Named(MainModule.RECORDINGS_PATH) Path recordingsPath,
TargetConnectionManager targetConnectionManager,
Clock clock,
PlatformClient platformClient,
RecordingArchiveHelper recordingArchiveHelper,
NotificationFactory notificationFactory) {
return new TargetRecordingPatchSave(
fs,
recordingsPath,
targetConnectionManager,
clock,
platformClient,
notificationFactory);
return new TargetRecordingPatchSave(recordingArchiveHelper, notificationFactory);
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,42 +38,35 @@
package io.cryostat.net.web.http.api.v1;

import java.util.Map;
import java.util.Optional;

import javax.inject.Inject;

import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;

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.reports.ReportService;
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.RecordingArchiveHelper;

import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.impl.HttpStatusException;

class TargetRecordingDeleteHandler extends AbstractAuthenticatedRequestHandler {

private final TargetConnectionManager targetConnectionManager;
private final ReportService reportService;
private final NotificationFactory notificationFactory;
private static final String NOTIFICATION_CATEGORY = "RecordingDeleted";
private final RecordingArchiveHelper recordingArchiveHelper;

@Inject
TargetRecordingDeleteHandler(
AuthManager auth,
TargetConnectionManager targetConnectionManager,
NotificationFactory notificationFactory,
ReportService reportService) {
RecordingArchiveHelper recordingArchiveHelper) {
super(auth);
this.notificationFactory = notificationFactory;
this.targetConnectionManager = targetConnectionManager;
this.reportService = reportService;
this.recordingArchiveHelper = recordingArchiveHelper;
}

@Override
Expand All @@ -100,37 +93,26 @@ public boolean isAsync() {
public void handleAuthenticated(RoutingContext ctx) throws Exception {
String recordingName = ctx.pathParam("recordingName");
ConnectionDescriptor connectionDescriptor = getConnectionDescriptorFromContext(ctx);
targetConnectionManager.executeConnectedTask(
connectionDescriptor,
connection -> {
Optional<IRecordingDescriptor> descriptor =
connection.getService().getAvailableRecordings().stream()
.filter(recording -> recording.getName().equals(recordingName))
.findFirst();
if (descriptor.isPresent()) {
connection.getService().close(descriptor.get());
reportService.delete(connectionDescriptor, recordingName);
notificationFactory
.createBuilder()
.metaCategory(NOTIFICATION_CATEGORY)
.metaType(HttpMimeType.JSON)
.message(
Map.of(
"recording",
recordingName,
"target",
connectionDescriptor.getTargetId()))
.build()
.send();
ctx.response().setStatusCode(200);
ctx.response().end();
} else {
throw new HttpStatusException(
404,
String.format(
"No recording with name \"%s\" found", recordingName));
}
return null;
});

try {
recordingArchiveHelper.deleteRecording(connectionDescriptor, recordingName);
} catch (Exception e) {
throw new HttpStatusException(500, e);
}

notificationFactory
.createBuilder()
.metaCategory(NOTIFICATION_CATEGORY)
.metaType(HttpMimeType.JSON)
.message(
Map.of(
"recording",
recordingName,
"target",
connectionDescriptor.getTargetId()))
.build()
.send();
ctx.response().setStatusCode(200);
ctx.response().end();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,143 +37,56 @@
*/
package io.cryostat.net.web.http.api.v1;

import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Optional;

import javax.inject.Inject;
import javax.inject.Named;

import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor;

import io.cryostat.MainModule;
import io.cryostat.core.net.JFRConnection;
import io.cryostat.core.sys.Clock;
import io.cryostat.core.sys.FileSystem;
import io.cryostat.messaging.notifications.NotificationFactory;
import io.cryostat.net.ConnectionDescriptor;
import io.cryostat.net.TargetConnectionManager;
import io.cryostat.net.web.http.HttpMimeType;
import io.cryostat.platform.PlatformClient;
import io.cryostat.util.URIUtil;
import io.cryostat.recordings.RecordingArchiveHelper;
import io.cryostat.recordings.RecordingNotFoundException;

import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.impl.HttpStatusException;

class TargetRecordingPatchSave {

private final FileSystem fs;
private final Path recordingsPath;
private final TargetConnectionManager targetConnectionManager;
private final Clock clock;
private final PlatformClient platformClient;
private final NotificationFactory notificationFactory;
private final RecordingArchiveHelper recordingArchiveHelper;
private static final String NOTIFICATION_CATEGORY = "RecordingArchived";

@Inject
TargetRecordingPatchSave(
FileSystem fs,
@Named(MainModule.RECORDINGS_PATH) Path recordingsPath,
TargetConnectionManager targetConnectionManager,
Clock clock,
PlatformClient platformClient,
RecordingArchiveHelper recordingArchiveHelper,
NotificationFactory notificationFactory) {
this.fs = fs;
this.recordingsPath = recordingsPath;
this.targetConnectionManager = targetConnectionManager;
this.clock = clock;
this.platformClient = platformClient;
this.recordingArchiveHelper = recordingArchiveHelper;
this.notificationFactory = notificationFactory;
}

void handle(RoutingContext ctx, ConnectionDescriptor connectionDescriptor) throws Exception {
String recordingName = ctx.pathParam("recordingName");

String saveName =
targetConnectionManager.executeConnectedTask(
connectionDescriptor,
connection -> {
Optional<IRecordingDescriptor> descriptor =
connection.getService().getAvailableRecordings().stream()
.filter(
recording ->
recording
.getName()
.equals(recordingName))
.findFirst();
if (descriptor.isPresent()) {
return saveRecording(connection, descriptor.get());
} else {
throw new HttpStatusException(
404,
String.format(
"Recording with name \"%s\" not found",
recordingName));
}
});
ctx.response().setStatusCode(200);
ctx.response().end(saveName);
notificationFactory
.createBuilder()
.metaCategory(NOTIFICATION_CATEGORY)
.metaType(HttpMimeType.JSON)
.message(
Map.of("recording", saveName, "target", connectionDescriptor.getTargetId()))
.build()
.send();
}

private String saveRecording(JFRConnection connection, IRecordingDescriptor descriptor)
throws Exception {
String recordingName = descriptor.getName();
if (recordingName.endsWith(".jfr")) {
recordingName = recordingName.substring(0, recordingName.length() - 4);
}

// TODO: To avoid having to perform this lookup each time, we should implement
// something like a map from targetIds to corresponding ServiceRefs
String targetName =
platformClient.listDiscoverableServices().stream()
.filter(
serviceRef -> {
try {
return serviceRef
.getServiceUri()
.equals(
URIUtil.convert(
connection.getJMXURL()))
&& serviceRef.getAlias().isPresent();
} catch (URISyntaxException | IOException ioe) {
return false;
}
})
.map(s -> s.getAlias().get())
.findFirst()
.orElse(connection.getHost())
.replaceAll("[\\._]+", "-");
try {
String saveName =
recordingArchiveHelper.saveRecording(connectionDescriptor, recordingName);

String timestamp =
clock.now().truncatedTo(ChronoUnit.SECONDS).toString().replaceAll("[-:]+", "");
String destination = String.format("%s_%s_%s", targetName, recordingName, timestamp);
// TODO byte-sized rename limit is arbitrary. Probably plenty since recordings are also
// differentiated by second-resolution timestamp
byte count = 1;
while (fs.exists(recordingsPath.resolve(destination + ".jfr"))) {
destination =
String.format("%s_%s_%s.%d", targetName, recordingName, timestamp, count++);
if (count == Byte.MAX_VALUE) {
throw new IOException(
"Recording could not be saved. File already exists and rename attempts were exhausted.");
}
}
destination += ".jfr";
try (InputStream stream = connection.getService().openStream(descriptor, false)) {
fs.copy(stream, recordingsPath.resolve(destination));
ctx.response().setStatusCode(200);
ctx.response().end(saveName);
notificationFactory
.createBuilder()
.metaCategory(NOTIFICATION_CATEGORY)
.metaType(HttpMimeType.JSON)
.message(
Map.of(
"recording",
saveName,
"target",
connectionDescriptor.getTargetId()))
.build()
.send();
} catch (RecordingNotFoundException e) {
throw new HttpStatusException(404, e);
}
return destination;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
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 io.cryostat.recordings.RecordingTargetHelper;

import com.google.gson.Gson;
import io.vertx.core.MultiMap;
Expand All @@ -77,7 +77,7 @@ public class TargetRecordingsPostHandler extends AbstractAuthenticatedRequestHan

static final String PATH = "targets/:targetId/recordings";
private final TargetConnectionManager targetConnectionManager;
private final RecordingCreationHelper recordingCreationHelper;
private final RecordingTargetHelper recordingTargetHelper;
private final RecordingOptionsBuilderFactory recordingOptionsBuilderFactory;
private final Provider<WebServer> webServerProvider;
private final Gson gson;
Expand All @@ -86,13 +86,13 @@ public class TargetRecordingsPostHandler extends AbstractAuthenticatedRequestHan
TargetRecordingsPostHandler(
AuthManager auth,
TargetConnectionManager targetConnectionManager,
RecordingCreationHelper recordingCreationHelper,
RecordingTargetHelper recordingTargetHelper,
RecordingOptionsBuilderFactory recordingOptionsBuilderFactory,
Provider<WebServer> webServerProvider,
Gson gson) {
super(auth);
this.targetConnectionManager = targetConnectionManager;
this.recordingCreationHelper = recordingCreationHelper;
this.recordingTargetHelper = recordingTargetHelper;
this.recordingOptionsBuilderFactory = recordingOptionsBuilderFactory;
this.webServerProvider = webServerProvider;
this.gson = gson;
Expand Down Expand Up @@ -160,10 +160,10 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception {
builder = builder.maxSize(Long.parseLong(attrs.get("maxSize")));
}
Pair<String, TemplateType> template =
RecordingCreationHelper.parseEventSpecifierToTemplate(
RecordingTargetHelper.parseEventSpecifierToTemplate(
eventSpecifier);
IRecordingDescriptor descriptor =
recordingCreationHelper.startRecording(
recordingTargetHelper.startRecording(
connectionDescriptor,
builder.build(),
template.getLeft(),
Expand Down
Loading