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

WIP feat(archives): include recording metadata in S3 tags #63

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/main/java/io/cryostat/ExceptionMappers.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import org.projectnessie.cel.tools.ScriptException;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;

public class ExceptionMappers {
@ServerExceptionMapper
Expand All @@ -42,4 +43,9 @@ public RestResponse<Void> mapValidationException(jakarta.validation.ValidationEx
public RestResponse<Void> mapScriptException(ScriptException ex) {
return RestResponse.status(HttpResponseStatus.BAD_REQUEST.code());
}

@ServerExceptionMapper
public RestResponse<Void> mapNoSuchKeyException(NoSuchKeyException ex) {
return RestResponse.status(HttpResponseStatus.NOT_FOUND.code());
}
}
7 changes: 7 additions & 0 deletions src/main/java/io/cryostat/Producers.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Named;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.projectnessie.cel.tools.ScriptHost;
Expand Down Expand Up @@ -59,6 +60,12 @@ public static FileSystem produceFileSystem() {
return new FileSystem();
}

@Produces
@ApplicationScoped
public static Base32 produceBase32() {
return new Base32();
}

@Produces
@ApplicationScoped
@DefaultBean
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/cryostat/recordings/ActiveRecording.java
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ private void notify(String category, ActiveRecording recording) {
recordingHelper.toExternalForm(recording))));
}

// FIXME the target connectUrl URI may no longer be known if the target
// has disappeared and we are emitting an event regarding an archived recording originally
// sourced from that target.
// This should embed the target jvmId and optionally the database ID.
public record RecordingEvent(URI target, Object recording) {
public RecordingEvent {
Objects.requireNonNull(target);
Expand Down
74 changes: 54 additions & 20 deletions src/main/java/io/cryostat/recordings/RecordingHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -98,7 +100,8 @@
@ApplicationScoped
public class RecordingHelper {

private static final String JFR_MIME = "application/jfr";
public static final String JFR_MIME = HttpMimeType.JFR.mime();

private static final Pattern TEMPLATE_PATTERN =
Pattern.compile("^template=([\\w]+)(?:,type=([\\w]+))?$");
public static final String DATASOURCE_FILENAME = "cryostat-analysis.jfr";
Expand Down Expand Up @@ -341,6 +344,11 @@ public List<S3Object> listArchivedRecordingObjects(String jvmId) {
}

public String saveRecording(Target target, ActiveRecording activeRecording) throws Exception {
return saveRecording(target, activeRecording, null);
}

public String saveRecording(Target target, ActiveRecording activeRecording, Instant expiry)
throws Exception {
// AWS object key name guidelines advise characters to avoid (% so we should not pass url
// encoded characters)
String transformedAlias =
Expand All @@ -350,22 +358,23 @@ public String saveRecording(Target target, ActiveRecording activeRecording) thro
String filename =
String.format("%s_%s_%s.jfr", transformedAlias, activeRecording.name, timestamp);
int mib = 1024 * 1024;
String key = String.format("%s/%s", target.jvmId, filename);
String key = archivedRecordingKey(target.jvmId, filename);
String multipartId = null;
List<Pair<Integer, String>> parts = new ArrayList<>();
try (var stream = remoteRecordingStreamFactory.open(activeRecording);
var ch = Channels.newChannel(stream)) {
ByteBuffer buf = ByteBuffer.allocate(20 * mib);
multipartId =
storage.createMultipartUpload(
CreateMultipartUploadRequest.builder()
.bucket(archiveBucket)
.key(key)
.contentType(JFR_MIME)
.tagging(
createMetadataTagging(activeRecording.metadata))
.build())
.uploadId();
CreateMultipartUploadRequest.Builder builder =
CreateMultipartUploadRequest.builder()
.bucket(archiveBucket)
.key(key)
.contentType(JFR_MIME)
.tagging(createActiveRecordingTagging(activeRecording.metadata));
if (expiry != null && expiry.isAfter(Instant.now())) {
builder = builder.expires(expiry);
}
CreateMultipartUploadRequest request = builder.build();
multipartId = storage.createMultipartUpload(request).uploadId();
int read = 0;
long accum = 0;
for (int i = 1; i <= 10_000; i++) {
Expand Down Expand Up @@ -449,12 +458,32 @@ public String saveRecording(Target target, ActiveRecording activeRecording) thro
new Notification(
"ActiveRecordingSaved",
new RecordingEvent(target.connectUrl, toExternalForm(activeRecording))));
bus.publish(
MessagingServer.class.getName(),
new Notification(
"ArchivedRecordingCreated",
new RecordingEvent(target.connectUrl, activeRecording.toExternalForm())));
return filename;
}

public String archivedRecordingKey(String jvmId, String filename) {
return (jvmId + "/" + filename).strip();
}

public String archivedRecordingKey(Pair<String, String> pair) {
return archivedRecordingKey(pair.getKey(), pair.getValue());
}

public String encodedKey(String jvmId, String filename) {
return base64Url.encodeAsString(
(jvmId + "/" + filename.strip()).getBytes(StandardCharsets.UTF_8));
(archivedRecordingKey(jvmId, filename)).getBytes(StandardCharsets.UTF_8));
}

// TODO refactor this and encapsulate archived recording keys as a record with override toString
public Pair<String, String> decodedKey(String encodedKey) {
String key = new String(base64Url.decode(encodedKey), StandardCharsets.UTF_8);
String[] parts = key.split("/");
return Pair.of(parts[0], parts[1]);
}

public InputStream getActiveInputStream(ActiveRecording recording) throws Exception {
Expand Down Expand Up @@ -482,7 +511,7 @@ public InputStream getArchivedRecordingStream(String encodedKey) {
}

public String downloadUrl(ActiveRecording recording) {
return "TODO";
return String.format("/api/v3/activedownload/%d", recording.id);
}

public String downloadUrl(String jvmId, String filename) {
Expand Down Expand Up @@ -524,7 +553,7 @@ public void deleteArchivedRecording(String jvmId, String filename) {
storage.deleteObject(
DeleteObjectRequest.builder()
.bucket(archiveBucket)
.key(String.format("%s/%s", jvmId, filename))
.key(archivedRecordingKey(jvmId, filename))
.build());
bus.publish(
MessagingServer.class.getName(),
Expand All @@ -534,7 +563,7 @@ public void deleteArchivedRecording(String jvmId, String filename) {
}

// Metadata
private Tagging createMetadataTagging(Metadata metadata) {
Tagging createMetadataTagging(Metadata metadata) {
// TODO attach other metadata than labels somehow. Prefixed keys to create partitioning?
return Tagging.builder()
.tagSet(
Expand Down Expand Up @@ -574,10 +603,7 @@ public Response uploadToJFRDatasource(long targetEntityId, long remoteId, URL up
MultipartForm form =
MultipartForm.create()
.binaryFileUpload(
"file",
DATASOURCE_FILENAME,
recordingPath.toString(),
HttpMimeType.OCTET_STREAM.toString());
"file", DATASOURCE_FILENAME, recordingPath.toString(), JFR_MIME);

try {
ResponseBuilder builder = new ResponseBuilderImpl();
Expand Down Expand Up @@ -629,6 +655,14 @@ Optional<Path> getRecordingCopyPath(
});
}

private Tagging createActiveRecordingTagging(ActiveRecording recording) {
Map<String, String> labels = new HashMap<>(recording.metadata.labels());
labels.put("connectUrl", recording.target.connectUrl.toString());
labels.put("jvmId", recording.target.jvmId);
Metadata metadata = new Metadata(labels);
return createMetadataTagging(metadata);
}

public enum RecordingReplace {
ALWAYS,
NEVER,
Expand Down
Loading
Loading