diff --git a/compose/sample_apps/gameserver-jdk11.yml b/compose/sample_apps/gameserver-jdk11.yml index e343948fd..92956c723 100644 --- a/compose/sample_apps/gameserver-jdk11.yml +++ b/compose/sample_apps/gameserver-jdk11.yml @@ -34,4 +34,6 @@ services: ENABLE_JMX: "true" JMX_HOST: gameserver-jdk11 JMX_PORT: "7091" - JVM_OPTS: -javaagent:/opt/cryostat/agent.jar + JVM_OPTS: >- + -Dio.cryostat.agent.shaded.org.slf4j.simpleLogger.defaultLogLevel=trace + -javaagent:/opt/cryostat/agent.jar diff --git a/compose/sample_apps/gameserver-jdk17.yml b/compose/sample_apps/gameserver-jdk17.yml index beb01a648..01ebd5e7a 100644 --- a/compose/sample_apps/gameserver-jdk17.yml +++ b/compose/sample_apps/gameserver-jdk17.yml @@ -34,6 +34,8 @@ services: ENABLE_JMX: "true" JMX_HOST: gameserver-jdk17 JMX_PORT: "7092" - JVM_OPTS: -javaagent:/opt/cryostat/agent.jar + JVM_OPTS: >- + -Dio.cryostat.agent.shaded.org.slf4j.simpleLogger.defaultLogLevel=trace + -javaagent:/opt/cryostat/agent.jar volumes: - ${DIR}/compose/auth_certs:/auth_certs:z diff --git a/compose/sample_apps/gameserver-jdk21.yml b/compose/sample_apps/gameserver-jdk21.yml index ab998ff6c..8ef9555e4 100644 --- a/compose/sample_apps/gameserver-jdk21.yml +++ b/compose/sample_apps/gameserver-jdk21.yml @@ -34,6 +34,8 @@ services: ENABLE_JMX: "true" JMX_HOST: gameserver-jdk21 JMX_PORT: "7093" - JVM_OPTS: -javaagent:/opt/cryostat/agent.jar + JVM_OPTS: >- + -Dio.cryostat.agent.shaded.org.slf4j.simpleLogger.defaultLogLevel=trace + -javaagent:/opt/cryostat/agent.jar volumes: - ${DIR}/compose/auth_certs:/auth_certs:z diff --git a/compose/sample_apps/opensearch.yml b/compose/sample_apps/opensearch.yml index 52d971eca..e057e330d 100644 --- a/compose/sample_apps/opensearch.yml +++ b/compose/sample_apps/opensearch.yml @@ -7,7 +7,8 @@ services: discovery.type: single-node # Reference: https://opensearch.org/docs/latest/security/configuration/demo-configuration/#setting-up-a-custom-admin-password OPENSEARCH_INITIAL_ADMIN_PASSWORD: password4Opense@rch - OPENSEARCH_JAVA_OPTS: > + OPENSEARCH_JAVA_OPTS: >- + -Dio.cryostat.agent.shaded.org.slf4j.simpleLogger.defaultLogLevel=trace -Dcom.sun.management.jmxremote.autodiscovery=true -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9292 diff --git a/compose/sample_apps/quarkus-cryostat-agent.yml b/compose/sample_apps/quarkus-cryostat-agent.yml index 23e4545c8..2342abeed 100644 --- a/compose/sample_apps/quarkus-cryostat-agent.yml +++ b/compose/sample_apps/quarkus-cryostat-agent.yml @@ -12,6 +12,7 @@ services: JAVA_OPTS_APPEND: >- -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager + -Dio.cryostat.agent.shaded.org.slf4j.simpleLogger.defaultLogLevel=trace -javaagent:/deployments/app/cryostat-agent.jar -Dcom.sun.management.jmxremote.autodiscovery=false -Dcom.sun.management.jmxremote diff --git a/schema/openapi.yaml b/schema/openapi.yaml index ca6280b3a..7ccd335f9 100644 --- a/schema/openapi.yaml +++ b/schema/openapi.yaml @@ -656,6 +656,26 @@ info: version: 4.0.0-snapshot openapi: 3.0.3 paths: + /api/beta/diagnostics/targets/{targetId}/gc: + post: + parameters: + - in: path + name: targetId + required: true + schema: + format: int64 + type: integer + responses: + "201": + description: Created + "401": + description: Not Authorized + "403": + description: Not Allowed + security: + - SecurityScheme: [] + tags: + - Diagnostics /api/beta/fs/recordings: get: responses: diff --git a/src/main/java/io/cryostat/diagnostic/Diagnostics.java b/src/main/java/io/cryostat/diagnostic/Diagnostics.java new file mode 100644 index 000000000..1ae118f41 --- /dev/null +++ b/src/main/java/io/cryostat/diagnostic/Diagnostics.java @@ -0,0 +1,44 @@ +/* + * Copyright The Cryostat Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cryostat.diagnostic; + +import io.cryostat.targets.Target; +import io.cryostat.targets.TargetConnectionManager; + +import io.smallrye.common.annotation.Blocking; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import org.jboss.resteasy.reactive.RestPath; + +@Path("/api/beta/diagnostics/targets/{targetId}") +public class Diagnostics { + + @Inject TargetConnectionManager targetConnectionManager; + + @Path("/gc") + @RolesAllowed("write") + @Blocking + @POST + public void gc(@RestPath long targetId) { + targetConnectionManager.executeConnectedTask( + Target.getTargetById(targetId), + conn -> + conn.invokeMBeanOperation( + "java.lang:type=Memory", "gc", null, null, Void.class)); + } +} diff --git a/src/main/java/io/cryostat/targets/AgentClient.java b/src/main/java/io/cryostat/targets/AgentClient.java index 146d96ab9..b9e53ba05 100644 --- a/src/main/java/io/cryostat/targets/AgentClient.java +++ b/src/main/java/io/cryostat/targets/AgentClient.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.openjdk.jmc.common.unit.IConstrainedMap; @@ -108,6 +109,53 @@ Uni mbeanMetrics() { .map(Unchecked.function(s -> mapper.readValue(s, MBeanMetrics.class))); } + Uni invokeMBeanOperation( + String beanName, + String operation, + Object[] parameters, + String[] signature, + Class returnType) { + try { + var req = new MBeanInvocationRequest(beanName, operation, parameters, signature); + return invoke( + HttpMethod.POST, + "/mbean-invoke/", + Buffer.buffer(mapper.writeValueAsBytes(req)), + BodyCodec.buffer()) + .map( + Unchecked.function( + resp -> { + int statusCode = resp.statusCode(); + if (HttpStatusCodeIdentifier.isSuccessCode(statusCode)) { + return resp; + } else if (statusCode == 403) { + logger.errorv( + "invokeMBeanOperation {0} ({1}) for {2} failed:" + + " HTTP 403", + beanName, operation, getUri()); + throw new ForbiddenException( + new UnsupportedOperationException( + "startRecording")); + } else { + logger.errorv( + "invokeMBeanOperation for {0} for ({1}) {2}" + + " failed: HTTP {3}", + beanName, operation, getUri(), statusCode); + throw new AgentApiException(statusCode); + } + })) + .map(HttpResponse::bodyAsBuffer) + .map( + buff -> { + // TODO implement conditional handling based on expected returnType + return null; + }); + } catch (JsonProcessingException e) { + logger.error("invokeMBeanOperation request failed", e); + return Uni.createFrom().failure(e); + } + } + Uni startRecording(StartRecordingRequest req) { try { return invoke( @@ -568,4 +616,12 @@ public IOptionDescriptor getOptionInfo(String s) { return getOptionDescriptors().get(s); } } + + static record MBeanInvocationRequest( + String beanName, String operation, Object[] parameters, String[] signature) { + MBeanInvocationRequest { + Objects.requireNonNull(beanName); + Objects.requireNonNull(operation); + } + } } diff --git a/src/main/java/io/cryostat/targets/AgentConnection.java b/src/main/java/io/cryostat/targets/AgentConnection.java index 522c6ed82..a497e5807 100644 --- a/src/main/java/io/cryostat/targets/AgentConnection.java +++ b/src/main/java/io/cryostat/targets/AgentConnection.java @@ -106,6 +106,18 @@ public JvmIdentifier getJvmIdentifier() throws IDException, IOException { } } + @Override + public T invokeMBeanOperation( + String beanName, + String operation, + Object[] parameters, + String[] signature, + Class returnType) { + return client.invokeMBeanOperation(beanName, operation, parameters, signature, returnType) + .await() + .atMost(client.getTimeout()); + } + @Override public int getPort() { return getUri().getPort();