diff --git a/driver-jmx/pom.xml b/driver-jmx/pom.xml new file mode 100644 index 000000000..368a4994f --- /dev/null +++ b/driver-jmx/pom.xml @@ -0,0 +1,44 @@ + + 4.0.0 + + + + mvn-defaults + io.nosqlbench + 3.12.129-SNAPSHOT + ../mvn-defaults + + + driver-jmx + jar + ${project.artifactId} + + A JMX nosqlbench ActivityType (AT) driver module; + This provides the ability to query system via JMX + + + + + + io.nosqlbench + engine-api + 3.12.129-SNAPSHOT + + + + + + org.assertj + assertj-core + test + + + + junit + junit + test + + + + + diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXAction.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXAction.java new file mode 100644 index 000000000..fdd6d0010 --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXAction.java @@ -0,0 +1,37 @@ +package io.nosqlbench.driver.jmx; + +import io.nosqlbench.driver.jmx.ops.JmxOp; +import io.nosqlbench.engine.api.activityapi.core.SyncAction; +import io.nosqlbench.engine.api.activityapi.planning.OpSequence; +import io.nosqlbench.engine.api.activityimpl.ActivityDef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JMXAction implements SyncAction { + + private final static Logger logger = LoggerFactory.getLogger(JMXAction.class); + + private final ActivityDef activityDef; + private final int slot; + private final JMXActivity activity; + private OpSequence sequencer; + + public JMXAction(ActivityDef activityDef, int slot, JMXActivity activity) { + this.activityDef = activityDef; + this.slot = slot; + this.activity = activity; + } + + @Override + public void init() { + this.sequencer = activity.getSequencer(); + } + + @Override + public int runCycle(long value) { + ReadyJmxOp readyJmxOp = sequencer.get(value); + JmxOp jmxOp = readyJmxOp.bind(value); + jmxOp.execute(); + return 0; + } +} diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXActionDispenser.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXActionDispenser.java new file mode 100644 index 000000000..d0a2c6ac5 --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXActionDispenser.java @@ -0,0 +1,17 @@ +package io.nosqlbench.driver.jmx; + +import io.nosqlbench.engine.api.activityapi.core.Action; +import io.nosqlbench.engine.api.activityapi.core.ActionDispenser; + +public class JMXActionDispenser implements ActionDispenser { + private JMXActivity activity; + + public JMXActionDispenser(JMXActivity activity) { + this.activity = activity; + } + + @Override + public Action getAction(int slot) { + return new JMXAction(activity.getActivityDef(),slot,activity); + } +} diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXActivity.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXActivity.java new file mode 100644 index 000000000..7a039ece4 --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXActivity.java @@ -0,0 +1,25 @@ +package io.nosqlbench.driver.jmx; + +import io.nosqlbench.engine.api.activityapi.core.Activity; +import io.nosqlbench.engine.api.activityapi.planning.OpSequence; +import io.nosqlbench.engine.api.activityimpl.ActivityDef; +import io.nosqlbench.engine.api.activityimpl.SimpleActivity; + +public class JMXActivity extends SimpleActivity implements Activity { + + private OpSequence sequence; + + public JMXActivity(ActivityDef activityDef) { + super(activityDef); + } + + @Override + public void initActivity() { + super.initActivity(); + this.sequence = createOpSequenceFromCommands(ReadyJmxOp::new); + } + + public OpSequence getSequencer() { + return sequence; + } +} diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXActivityType.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXActivityType.java new file mode 100644 index 000000000..5f29622f3 --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/JMXActivityType.java @@ -0,0 +1,24 @@ +package io.nosqlbench.driver.jmx; + +import io.nosqlbench.engine.api.activityapi.core.ActionDispenser; +import io.nosqlbench.engine.api.activityapi.core.ActivityType; +import io.nosqlbench.engine.api.activityimpl.ActivityDef; +import io.nosqlbench.nb.annotations.Service; + +@Service(ActivityType.class) +public class JMXActivityType implements ActivityType { + @Override + public String getName() { + return "jmx"; + } + + @Override + public JMXActivity getActivity(ActivityDef activityDef) { + return new JMXActivity(activityDef); + } + + @Override + public ActionDispenser getActionDispenser(JMXActivity activity) { + return new JMXActionDispenser(activity); + } +} diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ReadyJmxOp.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ReadyJmxOp.java new file mode 100644 index 000000000..6edde0e8b --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ReadyJmxOp.java @@ -0,0 +1,87 @@ +package io.nosqlbench.driver.jmx; + +import io.nosqlbench.driver.jmx.ops.JMXExplainOperation; +import io.nosqlbench.driver.jmx.ops.JMXReadOperation; +import io.nosqlbench.driver.jmx.ops.JmxOp; +import io.nosqlbench.engine.api.templating.CommandTemplate; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Map; +import java.util.Optional; + +public class ReadyJmxOp { + + private final CommandTemplate command; + + public ReadyJmxOp(CommandTemplate command) { + this.command = command; + } + + public JmxOp bind(long value) { + Map cmdmap = command.getCommand(value); + JMXConnector connector = bindConnector(cmdmap); + + if (!cmdmap.containsKey("object")) { + throw new RuntimeException("You must specify an object in a jmx operation as in object=..."); + } + + ObjectName objectName = null; + try { + String object = cmdmap.get("object"); + if (object==null) { + throw new RuntimeException("You must specify an object name for any JMX operation."); + } + objectName = new ObjectName(object); + } catch (MalformedObjectNameException e) { + e.printStackTrace(); + } + + if (cmdmap.containsKey("readvar")) { + return new JMXReadOperation(connector, objectName, cmdmap.get("readvar"), cmdmap.get("as_type"),cmdmap.get("as_name")); + } else if (cmdmap.containsKey("explain")) { + return new JMXExplainOperation(connector,objectName); + } + + throw new RuntimeException("No valid form of JMX operation was determined from the provided command details:" + cmdmap.toString()); + } + + private JMXConnector bindConnector(Map cmdmap) { + + JMXConnector connector = null; + try { + JMXServiceURL url = bindJMXServiceURL(cmdmap); + connector = JMXConnectorFactory.connect(url); + } catch (IOException e) { + e.printStackTrace(); + } + return connector; + } + + private JMXServiceURL bindJMXServiceURL(Map cmdmap) { + JMXServiceURL url = null; + try { + if (cmdmap.containsKey("url")) { + url = new JMXServiceURL(cmdmap.get("url")); + } else { + if (cmdmap.containsKey("host")) { + throw new RuntimeException("You must provide at least a host if you do not provide a url."); + } + String protocol = cmdmap.get("protocol"); + String host = cmdmap.get("host"); + int port = Optional.ofNullable(cmdmap.get("port")).map(Integer::parseInt).orElse(0); + String path = cmdmap.get("path"); + url = new JMXServiceURL(protocol, host, port, path); + + } + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + return url; + } +} diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ValueConverter.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ValueConverter.java new file mode 100644 index 000000000..ee841c594 --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ValueConverter.java @@ -0,0 +1,77 @@ +package io.nosqlbench.driver.jmx; + +import java.util.function.Function; + +public class ValueConverter { + + public static Object convert(String typeName, Object o) { + + if (!typeName.contains(".")) { + if (Number.class.isAssignableFrom(o.getClass())) { + switch (typeName) { + case "String": + return o.toString(); + case "long": + case "Long": + return (((Number) o).longValue()); + case "int": + case "Integer": + return (((Number) o).intValue()); + case "double": + case "Double": + return (((Number) o).doubleValue()); + case "float": + case "Float": + return (((Number) o).floatValue()); + case "short": + case "Short": + return (((Number) o).shortValue()); + case "byte": + case "Byte": + return (((Number) o).byteValue()); + default: + throw new RuntimeException("For numeric values, you can only convert to long,int,double,float,byte,short or String"); + } + } else { + String value; + if (CharSequence.class.isAssignableFrom(o.getClass())) { + value = (String) o; + } else { + value = o.toString(); + } + switch (typeName) { + case "String": + return value; + case "long": + case "Long": + return Long.parseLong(value); + case "int": + case "Integer": + return Integer.parseInt(value); + case "double": + case "Double": + return Double.parseDouble(value); + case "float": + case "Float": + return Float.parseFloat(value); + case "short": + case "Short": + return Short.parseShort(value); + case "byte": + case "Byte": + return Byte.parseByte(value); + default: + throw new RuntimeException("For String values, you can only convert to long, int, double, float, short, byte, or String"); + } + } + } + + try { + Class aClass = Class.forName(typeName); + return aClass.cast(o); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/formats/MBeanInfoConsoleFormat.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/formats/MBeanInfoConsoleFormat.java new file mode 100644 index 000000000..c38c92cc2 --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/formats/MBeanInfoConsoleFormat.java @@ -0,0 +1,119 @@ +package io.nosqlbench.driver.jmx.formats; + +import javax.management.*; +import java.util.Map; + +public class MBeanInfoConsoleFormat { + + private static Map MbeanOpImpacts = Map.of( + MBeanOperationInfo.ACTION,"ACTION", + MBeanOperationInfo.ACTION_INFO,"ACTION_INFO", + MBeanOperationInfo.UNKNOWN,"UNKNOWN", + MBeanOperationInfo.INFO,"INFO"); + + // Not including Descriptors here + public static String formatAsText(MBeanInfo info, ObjectName objectName) { + StringBuilder sb = new StringBuilder(); + sb.append("### MBeanInfo for '").append(objectName).append("'\n"); + + String className = info.getClassName(); + sb.append("# classname: ").append(className).append("\n"); + + String description = info.getDescription(); + sb.append("# ").append(description).append("\n"); + + MBeanConstructorInfo[] constructors = info.getConstructors(); + if (constructors.length > 0) { + sb.append("## constructors:\n"); + for (MBeanConstructorInfo constructor : constructors) { + + String ctorDesc = constructor.getDescription(); + sb.append("# ").append(ctorDesc).append("\n"); + + String name = constructor.getName(); + sb.append("# ").append(name).append("("); + sb.append(pramDetail(constructor.getSignature(), " ")); + sb.append(" )\n"); +// sb.append(" [").append(descriptorDetail(constructor.getDescriptor())).append("]\n"); + } + } + + MBeanAttributeInfo[] attributes = info.getAttributes(); + if (attributes.length > 0) { + sb.append("## attributes:\n"); + + for (MBeanAttributeInfo attribute : attributes) { + String attrDesc = attribute.getDescription(); + String attrName = attribute.getName(); + String attrType = attribute.getType(); + sb.append("# ").append(attrDesc).append("\n"); + sb.append("- '").append(attrName).append("' type=").append(attrType); + sb.append("readable=").append(attribute.isReadable()).append(" writable=").append(attribute.isWritable()).append(" is_is=").append(attribute.isIs()); + sb.append("\n"); +// sb.append(" [").append(descriptorDetail(attribute.getDescriptor())).append("]\n"); + + } + } + + MBeanNotificationInfo[] notifications = info.getNotifications(); + if (notifications.length > 0) { + sb.append("## notifications:\n"); + for (MBeanNotificationInfo notification : notifications) { + String notifName = notification.getName(); + String notifDesc = notification.getDescription(); + String[] notifTypes = notification.getNotifTypes(); + Class notifClass = notification.getClass(); + sb.append("# ").append(notifDesc).append("\n"); + sb.append("- ").append(notifName).append(" [").append(descriptorDetail(notification.getDescriptor())).append("]\n"); + + if (notifTypes.length > 0) { + for (String notifType : notifTypes) { + sb.append(" - ").append(notifType).append("\n"); + } + } + } + } + + MBeanOperationInfo[] operations = info.getOperations(); + if (operations.length > 0) { + sb.append("## operations:\n"); + for (MBeanOperationInfo operation : operations) { + String opDesc = operation.getDescription(); + String opName = operation.getName(); + MBeanParameterInfo[] opSig = operation.getSignature(); + Class opClass = operation.getClass(); + + sb.append("# ").append(opDesc).append("\n"); + sb.append("- ").append(opName).append("("); + sb.append(pramDetail(operation.getSignature(), " ")); + sb.append(") -> ").append(operation.getReturnType()); + sb.append(" impact=").append(MbeanOpImpacts.get(operation.getImpact())).append("\n"); + } + } + return sb.toString(); + } + + private static String descriptorDetail(Descriptor descriptor) { + StringBuilder sb = new StringBuilder(); + sb.append("valid=").append(descriptor.isValid()); + String[] fieldNames = descriptor.getFieldNames(); + for (String field : fieldNames) { + sb.append(" ").append(field).append("=").append(descriptor.getFieldValue(field)); + } + return sb.toString(); + } + + private static String pramDetail(MBeanParameterInfo[] signature, String prefix) { + StringBuilder sb = new StringBuilder(); + + for (MBeanParameterInfo paramInfo : signature) { + String desc = paramInfo.getDescription(); + if (desc != null) { + sb.append(prefix).append(" # ").append(desc).append("\n"); + } + sb.append(prefix).append(" - ").append(paramInfo.getName()).append("\n"); + } + return sb.toString(); + + } +} diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ops/JMXExplainOperation.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ops/JMXExplainOperation.java new file mode 100644 index 000000000..8b2023fef --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ops/JMXExplainOperation.java @@ -0,0 +1,25 @@ +package io.nosqlbench.driver.jmx.ops; + +import io.nosqlbench.driver.jmx.formats.MBeanInfoConsoleFormat; + +import javax.management.*; +import javax.management.remote.JMXConnector; +import java.io.IOException; + +public class JMXExplainOperation extends JmxOp { + public JMXExplainOperation(JMXConnector connector, ObjectName objectName) { + super(connector,objectName); + } + + @Override + public void execute() { + MBeanServerConnection bean = getMBeanConnection(); + try { + MBeanInfo info = bean.getMBeanInfo(objectName); + String mbeanInfoText = MBeanInfoConsoleFormat.formatAsText(info, objectName); + System.out.println(mbeanInfoText); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ops/JMXReadOperation.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ops/JMXReadOperation.java new file mode 100644 index 000000000..9fb6df634 --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ops/JMXReadOperation.java @@ -0,0 +1,42 @@ +package io.nosqlbench.driver.jmx.ops; + +import io.nosqlbench.driver.jmx.ValueConverter; +import io.nosqlbench.virtdata.library.basics.core.threadstate.SharedState; +import org.apache.commons.math4.analysis.function.Exp; + +import javax.management.*; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXServiceURL; +import java.io.IOException; + +public class JMXReadOperation extends JmxOp { + private final String attribute; + private final String asType; + private final String asName; + + public JMXReadOperation(JMXConnector connector, ObjectName objectName, String attribute, String asType, String asName) { + super(connector, objectName); + this.attribute = attribute; + this.asType = asType; + this.asName = asName; + } + + @Override + public void execute() { + try { + Object value = getMBeanConnection().getAttribute(objectName, this.attribute); + logger.trace("read attribute '" + value +"': " + value); + + if (asType!=null) { + value = ValueConverter.convert(asType,value); + } + + String storedName = (asName==null) ? attribute : asName; + SharedState.tl_ObjectMap.get().put(storedName,value); + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + +} diff --git a/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ops/JmxOp.java b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ops/JmxOp.java new file mode 100644 index 000000000..da87578a0 --- /dev/null +++ b/driver-jmx/src/main/java/io/nosqlbench/driver/jmx/ops/JmxOp.java @@ -0,0 +1,36 @@ +package io.nosqlbench.driver.jmx.ops; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; + +/** + * All JMX Operations should built on this base type. + */ +public abstract class JmxOp { + + protected final static Logger logger = LoggerFactory.getLogger(JmxOp.class); + + protected JMXConnector connector; + protected ObjectName objectName; + + public JmxOp(JMXConnector connector, ObjectName objectName) { + this.connector = connector; + this.objectName = objectName; + } + + public MBeanServerConnection getMBeanConnection() { + MBeanServerConnection connection = null; + try { + connection = connector.getMBeanServerConnection(); + return connection; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public abstract void execute(); +} diff --git a/driver-jmx/src/main/resources/jmx.md b/driver-jmx/src/main/resources/jmx.md new file mode 100644 index 000000000..c5db06e0d --- /dev/null +++ b/driver-jmx/src/main/resources/jmx.md @@ -0,0 +1,71 @@ +# JMX Driver + +The JMX Driver allows you to do various JMX operations against a JMX service URL +and object name. + +You must specify the service URL and object name in the op template. Alternately, you can specify +the protocol, host, port and path. Each cycle, the full JMX operation is derived from the +op template, and executed. + +In the first version of this driver, only reads are supported. + +# Connection Options + +JMX transports can be configured in a myriad of ways. The options below allow you to add +connection options such as SSL and authentication. + +- **ssl** - Use SSL settings provided. Thes SSL settings are from the NoSQLBench standard + SSL support + +# Example Operations + +## readvar + +The readvar operation is used to read a named attribute of the named object and store it in the +thread local variable map. + +``` +statements: + - read1: + url: service:jmx:rmi:///jndi/rmi://dsehost:7199/jmxrmi + object: org.apache.cassandra.metrics:type=Compaction,name=PendingTasks + readvar: Value + as_type: int + as_name: pending_tasks +``` + +The `as_type` and `as_name` are optional, and if provided will set the name and data type used in +the thread local variable map. + +- *as_type* can be any of long, int, double, float, byte, short, or String. If the original type +is convertable to a number, then it will be converted to a number and then to the desired type. If it +is not, it will be converted to String form first and then to the desired type. If the type name +contains dots, as in a fully-qualified class name, then direct class casting will be used if the +types are compatible. + +A combined format is available if you don't want to put every command property on a separate line. +In this format, the first entry in the command map is taken as the command name and a set of key=value +command arguments. It is semantically equivalent to the above example, only more compact. + +``` +statements: + - read1: readvar=Value as_type=int as_name=pending_tasks + url: service:jmx:rmi:///jndi/rmi://dsehost:7199/jmxrmi + object: org.apache.cassandra.metrics:type=Compaction,name=PendingTasks +``` + +## explain + +If you want to see the details about a managed object, you can use the explain command: + +``` +statements: + - explain1: + url: service:jmx:rmi:///jndi/rmi://dsehost:7199/jmxrmi + object: org.apache.cassandra.metrics:type=Compaction,name=PendingTasks + explain: object +``` + +This will use the MBeanInfo to interrogate the named management bean and provide a summary +of it's available attriburtes, operations, notifications, and constructors to stdout. +This is not meant for bulk testing, but more for explaining and documenting JMX beans. \ No newline at end of file diff --git a/driver-jmx/src/test/java/io/nosqlbench/driver/jmx/ValueConverterTest.java b/driver-jmx/src/test/java/io/nosqlbench/driver/jmx/ValueConverterTest.java new file mode 100644 index 000000000..ac47b205e --- /dev/null +++ b/driver-jmx/src/test/java/io/nosqlbench/driver/jmx/ValueConverterTest.java @@ -0,0 +1,15 @@ +package io.nosqlbench.driver.jmx; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ValueConverterTest { + + @Test + public void testConvertStringDouble() { + String s = "3"; + double d = (double) ValueConverter.convert("double", s); + } + +} \ No newline at end of file diff --git a/driver-jmx/src/test/resources/activities/jmx-test-1.yaml b/driver-jmx/src/test/resources/activities/jmx-test-1.yaml new file mode 100644 index 000000000..1fe50283c --- /dev/null +++ b/driver-jmx/src/test/resources/activities/jmx-test-1.yaml @@ -0,0 +1,10 @@ +# (src/test/resources/activities/) jmx-test-1.yaml +statements: + - read1: + url: service:jmx:rmi:///jndi/rmi://dsehost:7199/jmxrmi + object: org.apache.cassandra.metrics:type=Compaction,name=PendingTasks + readvar: Value + as_type: int + as_name: pending_tasks + + diff --git a/driver-jmx/src/test/resources/activities/jmx-test-2.yaml b/driver-jmx/src/test/resources/activities/jmx-test-2.yaml new file mode 100644 index 000000000..b46c5232f --- /dev/null +++ b/driver-jmx/src/test/resources/activities/jmx-test-2.yaml @@ -0,0 +1,8 @@ +# (src/test/resources/activities/) jmx-test-2.yaml +statements: + - explain1: + url: service:jmx:rmi:///jndi/rmi://dsehost:7199/jmxrmi + object: org.apache.cassandra.metrics:type=Compaction,name=PendingTasks + explain: object + + diff --git a/driver-jmx/src/test/resources/activities/jmx-test-3.yaml b/driver-jmx/src/test/resources/activities/jmx-test-3.yaml new file mode 100644 index 000000000..96860076f --- /dev/null +++ b/driver-jmx/src/test/resources/activities/jmx-test-3.yaml @@ -0,0 +1,4 @@ +statements: + - read1: readvar=Value as_type=int as_name=pending_tasks + url: service:jmx:rmi:///jndi/rmi://dsehost:7199/jmxrmi + object: org.apache.cassandra.metrics:type=Compaction,name=PendingTasks diff --git a/nb/pom.xml b/nb/pom.xml index 65ed021f6..14fb46c75 100644 --- a/nb/pom.xml +++ b/nb/pom.xml @@ -86,6 +86,12 @@ 3.12.129-SNAPSHOT + + io.nosqlbench + driver-jmx + 3.12.129-SNAPSHOT + + io.nosqlbench driver-cql-shaded diff --git a/pom.xml b/pom.xml index d0511bd04..a6e40789b 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,7 @@ driver-web driver-kafka driver-mongodb + driver-jmx