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);