From 10b38f40ee6d800c2e7498d8d76519291ad5b020 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 14 Aug 2023 13:57:04 -0400 Subject: [PATCH] feat(agent): implement Agent HTTP recording retrieval (#1607) --- .../java/io/cryostat/net/AgentClient.java | 20 ++++++++++++++++++ .../java/io/cryostat/net/AgentJFRService.java | 21 +++++++++++++++---- .../java/io/cryostat/net/web/WebServer.java | 9 ++------ .../recordings/RecordingArchiveHelper.java | 2 +- src/main/java/io/cryostat/util/URIUtil.java | 14 +++++++++++++ 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/cryostat/net/AgentClient.java b/src/main/java/io/cryostat/net/AgentClient.java index c3eef512cd..a0d143af42 100644 --- a/src/main/java/io/cryostat/net/AgentClient.java +++ b/src/main/java/io/cryostat/net/AgentClient.java @@ -24,6 +24,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; +import java.io.InputStream; import javax.script.ScriptException; @@ -130,6 +131,25 @@ Future startRecording(StartRecordingRequest req) { }); } + Future openStream(long id) { + Future> f = + invoke( + HttpMethod.GET, + "/recordings/" + id, + BodyCodec.buffer()); + return f.map( + resp -> { + int statusCode = resp.statusCode(); + if (HttpStatusCodeIdentifier.isSuccessCode(statusCode)) { + return resp.body(); + } else if (statusCode == 403) { + throw new UnsupportedOperationException(); + } else { + throw new RuntimeException("Unknown failure"); + } + }); + } + Future stopRecording(long id) { Future> f = invoke( diff --git a/src/main/java/io/cryostat/net/AgentJFRService.java b/src/main/java/io/cryostat/net/AgentJFRService.java index 9345f2c719..1173465005 100644 --- a/src/main/java/io/cryostat/net/AgentJFRService.java +++ b/src/main/java/io/cryostat/net/AgentJFRService.java @@ -15,6 +15,8 @@ */ package io.cryostat.net; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; @@ -50,6 +52,8 @@ import io.cryostat.core.templates.MergedTemplateService; import io.cryostat.core.templates.Template; import io.cryostat.core.templates.TemplateType; +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; import org.jsoup.nodes.Document; @@ -186,20 +190,29 @@ public boolean isEnabled() { } @Override - public InputStream openStream(IRecordingDescriptor arg0, boolean arg1) + public InputStream openStream(IRecordingDescriptor descriptor, boolean removeOnClose) throws FlightRecorderException { - throw new UnimplementedException(); + Future f = client.openStream(descriptor.getId()); + try { + Buffer b = f.toCompletionStage().toCompletableFuture().get(); + return new BufferedInputStream( + new ByteArrayInputStream(b.getBytes()) + ); + } catch (ExecutionException | InterruptedException e) { + logger.warn(e); + throw new FlightRecorderException("Failed to open remote recording stream", e); + } } @Override - public InputStream openStream(IRecordingDescriptor arg0, IQuantity arg1, boolean arg2) + public InputStream openStream(IRecordingDescriptor descriptor, IQuantity lastPartDuration, boolean removeOnClose) throws FlightRecorderException { throw new UnimplementedException(); } @Override public InputStream openStream( - IRecordingDescriptor arg0, IQuantity arg1, IQuantity arg2, boolean arg3) + IRecordingDescriptor descriptor, IQuantity startTime, IQuantity endTime, boolean removeOnClose) throws FlightRecorderException { throw new UnimplementedException(); } diff --git a/src/main/java/io/cryostat/net/web/WebServer.java b/src/main/java/io/cryostat/net/web/WebServer.java index 278ad84e18..0b3846a76b 100644 --- a/src/main/java/io/cryostat/net/web/WebServer.java +++ b/src/main/java/io/cryostat/net/web/WebServer.java @@ -50,6 +50,7 @@ import io.cryostat.net.web.http.api.ApiVersion; import io.cryostat.net.web.http.api.v2.ApiException; import io.cryostat.util.HttpStatusCodeIdentifier; +import io.cryostat.util.URIUtil; import com.google.gson.Gson; import io.vertx.core.AbstractVerticle; @@ -296,13 +297,7 @@ public String getAssetDownloadURL(ApiVersion apiVersion, String... pathSegments) } private String getTargetId(JFRConnection conn) throws IOException { - // TODO this is a hack, the JFRConnection interface should be refactored to expose a more - // general connection URL / targetId method since the JMX implementation is now only one - // possible implementation - if (conn instanceof AgentConnection) { - return ((AgentConnection) conn).getUri().toString(); - } - return conn.getJMXURL().toString(); + return URIUtil.getConnectionUri(conn).toString(); } public FileUpload getTempFileUpload( diff --git a/src/main/java/io/cryostat/recordings/RecordingArchiveHelper.java b/src/main/java/io/cryostat/recordings/RecordingArchiveHelper.java index 5ca7130d5f..c5d45fab29 100644 --- a/src/main/java/io/cryostat/recordings/RecordingArchiveHelper.java +++ b/src/main/java/io/cryostat/recordings/RecordingArchiveHelper.java @@ -916,7 +916,7 @@ private void validateRecordingPath( Path writeRecordingToDestination(JFRConnection connection, IRecordingDescriptor descriptor) throws IOException, URISyntaxException, FlightRecorderException, Exception { - URI serviceUri = URIUtil.convert(connection.getJMXURL()); + URI serviceUri = URIUtil.getConnectionUri(connection); String jvmId = jvmIdHelper.getJvmId(serviceUri.toString()); Path specificRecordingsPath = getRecordingSubdirectoryPath(jvmId); if (!fs.exists(specificRecordingsPath)) { diff --git a/src/main/java/io/cryostat/util/URIUtil.java b/src/main/java/io/cryostat/util/URIUtil.java index 30a76c39a5..d794d325a9 100644 --- a/src/main/java/io/cryostat/util/URIUtil.java +++ b/src/main/java/io/cryostat/util/URIUtil.java @@ -15,11 +15,15 @@ */ package io.cryostat.util; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import javax.management.remote.JMXServiceURL; +import io.cryostat.core.net.JFRConnection; +import io.cryostat.net.AgentConnection; + public class URIUtil { private URIUtil() {} @@ -48,4 +52,14 @@ public static URI getRmiTarget(JMXServiceURL serviceUrl) throws URISyntaxExcepti } return new URI(pathPart.substring("/jndi/".length(), pathPart.length())); } + + public static URI getConnectionUri(JFRConnection connection) throws IOException { + // TODO this is a hack, the JFRConnection interface should be refactored to expose a more + // general connection URL / targetId method since the JMX implementation is now only one + // possible implementation + if (connection instanceof AgentConnection) { + return ((AgentConnection) connection).getUri(); + } + return URI.create(connection.getJMXURL().toString()); + } }