diff --git a/README.md b/README.md index ca7a81a0..8606404a 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,18 @@ stopped or the host JVM shuts down. ## Configuration +### Logging + +`cryostat-agent` uses [SLF4J](https://www.slf4j.org/) for its logging. Currently it ships with `slf4j-simple` as the +log implementation, but this should change in the future to hook in to any existing SLF4J providers found on the +classpath at runtime, or fallback to `slf4j-simple` if nothing is found. + +The Agent's log levels can be controlled by setting a JVM system property on the target JVM: +`-Dio.cryostat.agent.shaded.org.slf4j.simpleLogger.defaultLogLevel=trace`, or replace `trace` with whatever level you +prefer. This can also be passed as a dynamic attachment argument when starting the Agent after the target JVM. + +### Agent Properties + `cryostat-agent` uses [smallrye-config](https://github.com/smallrye/smallrye-config) for configuration. Below is a list of configuration properties that can be used to influence how `cryostat-agent` runs and how it advertises itself to a Cryostat server instance. Properties that require configuration are indicated with a checked box. diff --git a/pom.xml b/pom.xml index 876246fe..e8251b44 100644 --- a/pom.xml +++ b/pom.xml @@ -94,7 +94,7 @@ 4.8.6.4 5.11.1 3.0 - 5.2.0 + 5.14.1 0.8.12 2.43.0 1.17.0 @@ -256,7 +256,7 @@ org.mockito - mockito-inline + mockito-core ${org.mockito.version} test diff --git a/src/main/java/io/cryostat/agent/remote/InvokeContext.java b/src/main/java/io/cryostat/agent/remote/InvokeContext.java new file mode 100644 index 00000000..6fedc664 --- /dev/null +++ b/src/main/java/io/cryostat/agent/remote/InvokeContext.java @@ -0,0 +1,117 @@ +/* + * 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.agent.remote; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.util.Objects; + +import javax.inject.Inject; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sun.net.httpserver.HttpExchange; +import org.apache.http.HttpStatus; +import org.eclipse.microprofile.config.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class InvokeContext extends MutatingRemoteContext { + + private final Logger log = LoggerFactory.getLogger(getClass()); + private final ObjectMapper mapper; + + @Inject + InvokeContext(ObjectMapper mapper, Config config) { + super(config); + this.mapper = mapper; + } + + @Override + public String path() { + return "/mbean-invoke/"; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String mtd = exchange.getRequestMethod(); + switch (mtd) { + case "POST": + try (InputStream body = exchange.getRequestBody()) { + MBeanInvocationRequest req = + mapper.readValue(body, MBeanInvocationRequest.class); + if (!req.isValid()) { + exchange.sendResponseHeaders( + HttpStatus.SC_BAD_REQUEST, BODY_LENGTH_NONE); + } + MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + Object response = + server.invoke( + ObjectName.getInstance(req.beanName), + req.operation, + req.parameters, + req.signature); + if (Objects.nonNull(response)) { + exchange.sendResponseHeaders(HttpStatus.SC_OK, BODY_LENGTH_UNKNOWN); + try (OutputStream responseStream = exchange.getResponseBody()) { + mapper.writeValue(responseStream, response); + } + } else { + exchange.sendResponseHeaders(HttpStatus.SC_ACCEPTED, BODY_LENGTH_NONE); + } + } catch (Exception e) { + log.error("mbean serialization failure", e); + exchange.sendResponseHeaders(HttpStatus.SC_BAD_GATEWAY, BODY_LENGTH_NONE); + } + break; + default: + log.warn("Unknown request method {}", mtd); + exchange.sendResponseHeaders( + HttpStatus.SC_METHOD_NOT_ALLOWED, BODY_LENGTH_NONE); + break; + } + } finally { + exchange.close(); + } + } + + static class MBeanInvocationRequest { + + public String beanName; + public String operation; + public Object[] parameters; + public String[] signature; + + public boolean isValid() { + if (this.beanName.equals(ManagementFactory.MEMORY_MXBEAN_NAME)) { + return true; + } + return false; + } + + public String getBeanName() { + return beanName; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + } +} diff --git a/src/main/java/io/cryostat/agent/remote/RemoteModule.java b/src/main/java/io/cryostat/agent/remote/RemoteModule.java index 8dd7e82d..c9554eef 100644 --- a/src/main/java/io/cryostat/agent/remote/RemoteModule.java +++ b/src/main/java/io/cryostat/agent/remote/RemoteModule.java @@ -22,6 +22,10 @@ @Module public abstract class RemoteModule { + @Binds + @IntoSet + abstract RemoteContext bindInvokeContext(InvokeContext ctx); + @Binds @IntoSet abstract RemoteContext bindMBeanContext(MBeanContext ctx);