diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java index 0f0fceb4b..dd6a3be5d 100644 --- a/src/main/java/io/cryostat/recordings/RecordingHelper.java +++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java @@ -75,6 +75,7 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.ResponseBuilder; import jdk.jfr.RecordingState; +import org.apache.commons.codec.binary.Base32; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; @@ -122,6 +123,8 @@ public class RecordingHelper { @Inject Clock clock; @Inject S3Presigner presigner; + @Inject Base32 base32; + @Inject @Named(Producers.BASE64_URL) Base64 base64Url; @@ -369,7 +372,7 @@ public String saveRecording(ActiveRecording recording, Instant expiry) throws Ex String filename = String.format("%s_%s_%s.jfr", transformedAlias, recording.name, timestamp); int mib = 1024 * 1024; - String key = String.format("%s/%s", recording.target.jvmId, filename); + String key = archivedRecordingKey(recording.target.jvmId, filename); String multipartId = null; List> parts = new ArrayList<>(); try (var stream = remoteRecordingStreamFactory.open(recording); @@ -380,9 +383,7 @@ public String saveRecording(ActiveRecording recording, Instant expiry) throws Ex .bucket(archiveBucket) .key(key) .contentType(JFR_MIME) - .tagging( - createMetadataTagging( - new Metadata(recording.metadata, expiry))); + .tagging(createActiveRecordingTagging(recording, expiry)); if (expiry != null && expiry.isAfter(Instant.now())) { builder = builder.expires(expiry); } @@ -467,12 +468,17 @@ public String saveRecording(ActiveRecording recording, Instant expiry) throws Ex throw e; } if (expiry == null) { + LinkedRecordingDescriptor serializedRecording = toExternalForm(recording); bus.publish( MessagingServer.class.getName(), new Notification( "ActiveRecordingSaved", - new RecordingEvent( - recording.target.connectUrl, toExternalForm(recording)))); + new RecordingEvent(recording.target.connectUrl, serializedRecording))); + bus.publish( + MessagingServer.class.getName(), + new Notification( + "ArchivedRecordingCreated", + new RecordingEvent(recording.target.connectUrl, serializedRecording))); } return filename; } @@ -497,7 +503,11 @@ private Optional getArchivedRecordingMetadata(String storageKey) { } } - private String decodeBase64(String encoded) { + String decodeBase32(String encoded) { + return new String(base32.decode(encoded), StandardCharsets.UTF_8); + } + + String decodeBase64(String encoded) { return new String(base64Url.decode(encoded), StandardCharsets.UTF_8); } @@ -588,7 +598,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(), @@ -597,8 +607,16 @@ public void deleteArchivedRecording(String jvmId, String filename) { new RecordingEvent(URI.create("localhost:0"), Map.of("name", filename)))); } + Tagging createActiveRecordingTagging(ActiveRecording recording, Instant expiry) { + Map labels = new HashMap<>(recording.metadata.labels()); + labels.put("connectUrl", recording.target.connectUrl.toString()); + labels.put("jvmId", recording.target.jvmId); + Metadata metadata = new Metadata(labels, expiry); + return createMetadataTagging(metadata); + } + // Metadata - private Tagging createMetadataTagging(Metadata metadata) { + Tagging createMetadataTagging(Metadata metadata) { // TODO attach other metadata than labels somehow. Prefixed keys to create partitioning? var tags = new ArrayList(); tags.addAll( diff --git a/src/main/java/io/cryostat/recordings/Recordings.java b/src/main/java/io/cryostat/recordings/Recordings.java index 88de455d3..843f4bf39 100644 --- a/src/main/java/io/cryostat/recordings/Recordings.java +++ b/src/main/java/io/cryostat/recordings/Recordings.java @@ -19,7 +19,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -77,7 +76,6 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import jdk.jfr.RecordingState; -import org.apache.commons.codec.binary.Base32; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; @@ -99,8 +97,6 @@ import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.S3Object; -import software.amazon.awssdk.services.s3.model.Tag; -import software.amazon.awssdk.services.s3.model.Tagging; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; @@ -121,8 +117,6 @@ public class Recordings { @Inject ObjectMapper mapper; @Inject RecordingHelper recordingHelper; - @Inject Base32 base32; - @Inject @Named(Producers.BASE64_URL) Base64 base64Url; @@ -220,6 +214,7 @@ public void agentPush( if (rawLabels != null) { rawLabels.getMap().forEach((k, v) -> labels.put(k, v.toString())); } + labels.put("jvmId", jvmId); Metadata metadata = new Metadata(labels); logger.infov( "recording:{0}, labels:{1}, maxFiles:{2}", recording.fileName(), labels, maxFiles); @@ -345,13 +340,15 @@ Map doUpload(FileUpload recording, Metadata metadata, String jvm if (!filename.endsWith(".jfr")) { filename = filename + ".jfr"; } + Map labels = new HashMap<>(metadata.labels); + labels.put("jvmId", jvmId); String key = recordingHelper.archivedRecordingKey(jvmId, filename); storage.putObject( PutObjectRequest.builder() .bucket(archiveBucket) .key(key) .contentType(RecordingHelper.JFR_MIME) - .tagging(createMetadataTagging(metadata)) + .tagging(recordingHelper.createMetadataTagging(new Metadata(labels))) .build(), RequestBody.fromFile(recording.filePath())); logger.info("Upload complete"); @@ -663,7 +660,7 @@ public void deleteRecording(@RestPath long targetId, @RestPath long remoteId) th @RolesAllowed("write") public void deleteArchivedRecording(@RestPath String encodedJvmId, @RestPath String filename) throws Exception { - var jvmId = decodeBase32(encodedJvmId); + var jvmId = recordingHelper.decodeBase32(encodedJvmId); logger.infov("Handling archived recording deletion: {0} / {1}", jvmId, filename); var metadata = recordingHelper @@ -676,7 +673,8 @@ public void deleteArchivedRecording(@RestPath String encodedJvmId, @RestPath Str .map(c -> c.toString()) .orElseGet(() -> metadata.labels.computeIfAbsent("connectUrl", k -> jvmId)); logger.infov( - "Archived recording from connectUrl {0} has metadata: {1}", connectUrl, metadata); + "Archived recording from connectUrl \"{0}\" has metadata: {1}", + connectUrl, metadata); logger.infov( "Sending S3 deletion request for {0} {1}", archiveBucket, recordingHelper.archivedRecordingKey(jvmId, filename)); @@ -834,35 +832,6 @@ public Response redirectPresignedDownload(@RestPath String encodedKey) .build(); } - private Tagging createMetadataTagging(Metadata metadata) { - // TODO attach other metadata than labels somehow. Prefixed keys to create partitioning? - return Tagging.builder() - .tagSet( - metadata.labels.entrySet().stream() - .map( - e -> - Tag.builder() - .key( - base64Url.encodeAsString( - e.getKey() - .getBytes( - StandardCharsets - .UTF_8))) - .value( - base64Url.encodeAsString( - e.getValue() - .getBytes( - StandardCharsets - .UTF_8))) - .build()) - .toList()) - .build(); - } - - private String decodeBase32(String encoded) { - return new String(base32.decode(encoded), StandardCharsets.UTF_8); - } - private static Map getRecordingOptions( IFlightRecorderService service, RecordingOptionsBuilder builder) throws Exception { IConstrainedMap recordingOptions = builder.build();