Skip to content

Commit 0dcd194

Browse files
committed
feat(reports): use S3 presigned URL for archived recording reports
1 parent b7c4d13 commit 0dcd194

File tree

5 files changed

+87
-17
lines changed

5 files changed

+87
-17
lines changed

compose/reports.yml

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ services:
2727
-Dcom.sun.management.jmxremote.ssl=false
2828
-Dcom.sun.management.jmxremote.local.only=false
2929
QUARKUS_HTTP_PORT: 10001
30+
CRYOSTAT_STORAGE_BASE_URI: http://s3:8333
3031
healthcheck:
3132
test: curl --fail http://localhost:10001/ || exit 1
3233
retries: 3

src/main/java/io/cryostat/ConfigProperties.java

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public class ConfigProperties {
3535
public static final String CONNECTIONS_UPLOAD_TIMEOUT = "cryostat.connections.upload-timeout";
3636

3737
public static final String REPORTS_SIDECAR_URL = "quarkus.rest-client.reports.url";
38+
public static final String REPORTS_USE_PRESIGNED_TRANSFER =
39+
"cryostat.services.reports.use-presigned-transfer";
3840
public static final String REPORTS_MEMORY_CACHE_ENABLED =
3941
"cryostat.services.reports.memory-cache.enabled";
4042
public static final String REPORTS_STORAGE_CACHE_ENABLED =

src/main/java/io/cryostat/reports/ReportSidecarService.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,20 @@
3030
import org.jboss.resteasy.reactive.PartType;
3131
import org.jboss.resteasy.reactive.RestForm;
3232

33-
@Path("/report")
3433
@RegisterRestClient(configKey = "reports")
3534
@ApplicationScoped
3635
public interface ReportSidecarService {
36+
@Path("/report")
3737
@POST
3838
@Consumes(MediaType.MULTIPART_FORM_DATA)
3939
Uni<Map<String, AnalysisResult>> generate(
4040
@RestForm("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) InputStream file);
41+
42+
@Path("/remote_report")
43+
@POST
44+
@Consumes(MediaType.MULTIPART_FORM_DATA)
45+
Uni<Map<String, AnalysisResult>> generatePresigned(
46+
@RestForm("path") @PartType(MediaType.TEXT_PLAIN) String path,
47+
@RestForm("query") @PartType(MediaType.TEXT_PLAIN) String query,
48+
@RestForm("filter") @PartType(MediaType.TEXT_PLAIN) String filter);
4149
}

src/main/java/io/cryostat/reports/ReportsServiceImpl.java

+74-16
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717

1818
import java.io.BufferedInputStream;
1919
import java.io.InputStream;
20+
import java.net.URI;
21+
import java.net.URISyntaxException;
2022
import java.time.Duration;
23+
import java.time.Instant;
2124
import java.util.Map;
25+
import java.util.Optional;
2226
import java.util.function.Predicate;
2327

2428
import org.openjdk.jmc.flightrecorder.rules.IRule;
@@ -27,15 +31,21 @@
2731
import io.cryostat.core.reports.InterruptibleReportGenerator;
2832
import io.cryostat.core.reports.InterruptibleReportGenerator.AnalysisResult;
2933
import io.cryostat.recordings.ActiveRecording;
34+
import io.cryostat.recordings.ArchivedRecordings.ArchivedRecording;
3035
import io.cryostat.recordings.RecordingHelper;
3136

3237
import com.fasterxml.jackson.databind.ObjectMapper;
3338
import io.smallrye.mutiny.Uni;
3439
import jakarta.enterprise.context.ApplicationScoped;
3540
import jakarta.inject.Inject;
41+
import jakarta.ws.rs.InternalServerErrorException;
42+
import org.apache.commons.lang3.tuple.Pair;
3643
import org.eclipse.microprofile.config.inject.ConfigProperty;
3744
import org.eclipse.microprofile.rest.client.inject.RestClient;
3845
import org.jboss.logging.Logger;
46+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
47+
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
48+
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
3949

4050
@ApplicationScoped
4151
class ReportsServiceImpl implements ReportsService {
@@ -48,10 +58,20 @@ class ReportsServiceImpl implements ReportsService {
4858
@ConfigProperty(name = ConfigProperties.REPORTS_SIDECAR_URL)
4959
String sidecarUri;
5060

61+
@ConfigProperty(name = ConfigProperties.REPORTS_USE_PRESIGNED_TRANSFER)
62+
boolean usePresignedTransfer;
63+
64+
@ConfigProperty(name = ConfigProperties.AWS_BUCKET_NAME_ARCHIVES)
65+
String archiveBucket;
66+
67+
@ConfigProperty(name = ConfigProperties.STORAGE_EXT_URL)
68+
Optional<String> externalStorageUrl;
69+
5170
@Inject ObjectMapper mapper;
5271
@Inject RecordingHelper helper;
5372
@Inject InterruptibleReportGenerator reportGenerator;
5473
@Inject @RestClient ReportSidecarService sidecar;
74+
@Inject S3Presigner presigner;
5575
@Inject Logger logger;
5676

5777
@Override
@@ -63,7 +83,7 @@ public Uni<Map<String, AnalysisResult>> reportFor(
6383
} catch (Exception e) {
6484
throw new ReportGenerationException(e);
6585
}
66-
if (NO_SIDECAR_URL.equals(sidecarUri)) {
86+
if (!useSidecar()) {
6787
logger.tracev(
6888
"inprocess reportFor active recording {0} {1}",
6989
recording.target.jvmId, recording.remoteId);
@@ -72,7 +92,7 @@ public Uni<Map<String, AnalysisResult>> reportFor(
7292
logger.tracev(
7393
"sidecar reportFor active recording {0} {1}",
7494
recording.target.jvmId, recording.remoteId);
75-
return fireRequest(stream);
95+
return sidecar.generate(stream);
7696
}
7797
}
7898

@@ -85,12 +105,22 @@ public Uni<Map<String, AnalysisResult>> reportFor(
85105
} catch (Exception e) {
86106
throw new ReportGenerationException(e);
87107
}
88-
if (NO_SIDECAR_URL.equals(sidecarUri)) {
108+
if (!useSidecar()) {
89109
logger.tracev("inprocess reportFor archived recording {0} {1}", jvmId, filename);
90110
return process(stream, predicate);
111+
} else if (usePresignedSidecar()) {
112+
logger.tracev(
113+
"sidecar reportFor presigned archived recording {0} {1}", jvmId, filename);
114+
try {
115+
var uri = getPresignedPath(jvmId, filename);
116+
return sidecar.generatePresigned(uri.getPath(), uri.getQuery(), null);
117+
} catch (URISyntaxException e) {
118+
logger.error(e);
119+
throw new InternalServerErrorException(e);
120+
}
91121
} else {
92122
logger.tracev("sidecar reportFor archived recording {0} {1}", jvmId, filename);
93-
return fireRequest(stream);
123+
return sidecar.generate(stream);
94124
}
95125
}
96126

@@ -104,6 +134,24 @@ public Uni<Map<String, AnalysisResult>> reportFor(String jvmId, String filename)
104134
return reportFor(jvmId, filename, r -> true);
105135
}
106136

137+
@Override
138+
public boolean keyExists(ActiveRecording recording) {
139+
return false;
140+
}
141+
142+
@Override
143+
public boolean keyExists(String jvmId, String filename) {
144+
return false;
145+
}
146+
147+
private boolean useSidecar() {
148+
return sidecarUri != null && !sidecarUri.isBlank() && !NO_SIDECAR_URL.equals(sidecarUri);
149+
}
150+
151+
private boolean usePresignedSidecar() {
152+
return useSidecar() && usePresignedTransfer;
153+
}
154+
107155
private Uni<Map<String, AnalysisResult>> process(
108156
InputStream stream, Predicate<IRule> predicate) {
109157
return Uni.createFrom()
@@ -112,23 +160,33 @@ private Uni<Map<String, AnalysisResult>> process(
112160
new BufferedInputStream(stream), predicate));
113161
}
114162

115-
private Uni<Map<String, AnalysisResult>> fireRequest(InputStream stream) {
116-
return sidecar.generate(stream);
163+
private URI getPresignedUri(ActiveRecording recording) throws Exception {
164+
// TODO refactor, this is copied out of Recordings.java
165+
String savename = recording.name;
166+
ArchivedRecording rec =
167+
helper.archiveRecording(recording, savename, Instant.now().plusSeconds(60));
168+
return getPresignedPath(recording.target.jvmId, rec.name());
169+
}
170+
171+
private URI getPresignedPath(String jvmId, String filename) throws URISyntaxException {
172+
// TODO refactor, this is copied out of Recordings.java
173+
logger.infov("Handling presigned download request for {0}/{1}", jvmId, filename);
174+
GetObjectRequest getRequest =
175+
GetObjectRequest.builder()
176+
.bucket(archiveBucket)
177+
.key(helper.archivedRecordingKey(Pair.of(jvmId, filename)))
178+
.build();
179+
GetObjectPresignRequest presignRequest =
180+
GetObjectPresignRequest.builder()
181+
.signatureDuration(Duration.ofMinutes(1))
182+
.getObjectRequest(getRequest)
183+
.build();
184+
return URI.create(presigner.presignGetObject(presignRequest).url().toString()).normalize();
117185
}
118186

119187
public static class ReportGenerationException extends RuntimeException {
120188
public ReportGenerationException(Throwable cause) {
121189
super(cause);
122190
}
123191
}
124-
125-
@Override
126-
public boolean keyExists(ActiveRecording recording) {
127-
return false;
128-
}
129-
130-
@Override
131-
public boolean keyExists(String jvmId, String filename) {
132-
return false;
133-
}
134192
}

src/main/resources/application.properties

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ quarkus.cache.caffeine.archivedreports.expire-after-access=10m
4040
cryostat.services.reports.storage-cache.enabled=true
4141
cryostat.services.reports.storage-cache.name=archivedreports
4242
cryostat.services.reports.storage-cache.expiry-duration=24h
43+
cryostat.services.reports.use-presigned-transfer=true
4344

4445
cryostat.http.proxy.tls-enabled=false
4546
cryostat.http.proxy.host=${quarkus.http.host}

0 commit comments

Comments
 (0)