diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 64293946a1..afbe5b7416 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -39,6 +39,7 @@ + @@ -165,4 +166,4 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index f022e0e095..46e3956b41 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ buildscript { // For sharing constants between builds Properties constants = new Properties() file("$projectDir/constants.properties").withInputStream { constants.load(it) } + file("${project(':node').projectDir}/src/main/resources/build.properties").withInputStream { constants.load(it) } // Our version: bump this on release. ext.corda_release_version = "3.0-SNAPSHOT" @@ -40,7 +41,7 @@ buildscript { ext.jackson_version = '2.9.3' ext.jetty_version = '9.4.7.v20170914' ext.jersey_version = '2.25' - ext.jolokia_version = '1.3.7' + ext.jolokia_version = constants.getProperty("jolokiaAgentVersion") ext.assertj_version = '3.8.0' ext.slf4j_version = '1.7.25' ext.log4j_version = '2.9.1' @@ -71,6 +72,7 @@ buildscript { ext.docker_compose_rule_version = '0.33.0' ext.selenium_version = '3.8.1' ext.ghostdriver_version = '2.1.0' + ext.eaagentloader_version = '1.0.3' // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: ext.java8_minUpdateVersion = '131' diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 9dd048a719..3c41f75d5b 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -185,8 +185,8 @@ path to the node's base directory. :port: The port to start SSH server on -:exportJMXTo: If set to ``http``, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent. - Default Jolokia access url is http://127.0.0.1:7005/jolokia/ +:jmxMonitoringHttpPort: If set, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent on the corresponding port. + Default Jolokia access url is http://127.0.0.1:port/jolokia/ :transactionCacheSizeMegaBytes: Optionally specify how much memory should be used for caching of ledger transactions in memory. Otherwise defaults to 8MB plus 5% of all heap memory above 300MB. diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst index 2fadfccef0..4132ac9385 100644 --- a/docs/source/node-administration.rst +++ b/docs/source/node-administration.rst @@ -106,7 +106,7 @@ Here are a few ways to build dashboards and extract monitoring data for a node: It can bridge any data input to any output using their plugin system, for example, Telegraf can be configured to collect data from Jolokia and write to DataDog web api. -The Node configuration parameter `exportJMXTo` should be set to ``http`` to ensure a Jolokia agent is instrumented with +The Node configuration parameter `jmxMonitoringHttpPort` has to be present in order to ensure a Jolokia agent is instrumented with the JVM run-time. The following JMX statistics are exported: diff --git a/node/build.gradle b/node/build.gradle index 2aeb7878d3..50130933af 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -172,6 +172,9 @@ dependencies { // Jsh: Testing SSH server integrationTestCompile group: 'com.jcraft', name: 'jsch', version: '0.1.54' + // AgentLoader: dynamic loading of JVM agents + compile group: 'com.ea.agentloader', name: 'ea-agent-loader', version: "${eaagentloader_version}" + // Jetty dependencies for NetworkMapClient test. // Web stuff: for HTTP[S] servlets testCompile "org.eclipse.jetty:jetty-servlet:${jetty_version}" @@ -183,7 +186,6 @@ dependencies { testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}" testCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}" - // Jolokia JVM monitoring agent runtime "org.jolokia:jolokia-jvm:${jolokia_version}:agent" } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt index f13e73de06..39c0a8880f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -138,7 +138,7 @@ class AMQPBridgeTest { doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(artemisAddress).whenever(it).p2pAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies } artemisConfig.configureWithDevSSLCertificate() diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index 64d7c09990..8262f0b30b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -222,7 +222,7 @@ class ProtonWrapperTests { doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies } artemisConfig.configureWithDevSSLCertificate() diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index d97db49099..af380101f6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -59,6 +59,8 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor +import net.corda.node.utilities.NodeBuildProperties +import net.corda.node.utilities.JVMAgentRegistry import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -73,6 +75,7 @@ import rx.Observable import rx.Scheduler import java.io.IOException import java.lang.reflect.InvocationTargetException +import java.nio.file.Paths import java.security.KeyPair import java.security.KeyStoreException import java.security.PublicKey @@ -192,6 +195,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } log.info("Node starting up ...") initCertificate() + initialiseJVMAgents() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) val identityService = makeIdentityService(identity.certificate) @@ -749,6 +753,22 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return NodeVaultService(platformClock, keyManagementService, stateLoader, hibernateConfig) } + /** Load configured JVM agents */ + private fun initialiseJVMAgents() { + configuration.jmxMonitoringHttpPort?.let { port -> + requireNotNull(NodeBuildProperties.JOLOKIA_AGENT_VERSION) { + "'jolokiaAgentVersion' missing from build properties" + } + log.info("Starting Jolokia agent on HTTP port: $port") + val libDir = Paths.get(configuration.baseDirectory.toString(), "drivers") + val jarFilePath = JVMAgentRegistry.resolveAgentJar( + "jolokia-jvm-${NodeBuildProperties.JOLOKIA_AGENT_VERSION}-agent.jar", libDir) ?: + throw Error("Unable to locate agent jar file") + log.info("Agent jar file: $jarFilePath") + JVMAgentRegistry.attach("jolokia", "port=$port", jarFilePath) + } + } + private inner class ServiceHubInternalImpl( override val identityService: IdentityService, // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index d191af1403..a774e81b79 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -192,9 +192,9 @@ open class Node(configuration: NodeConfiguration, val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" with(rpcOptions) { rpcBroker = if (useSsl) { - ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, exportJMXto.isNotEmpty(), rpcBrokerDirectory) + ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, jmxMonitoringHttpPort != null, rpcBrokerDirectory) } else { - ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, exportJMXto.isNotEmpty(), rpcBrokerDirectory) + ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, jmxMonitoringHttpPort != null, rpcBrokerDirectory) } } return rpcBroker!!.addresses diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 450bab72a4..bdc2ec265f 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -25,7 +25,7 @@ val Int.MB: Long get() = this * 1024L * 1024L interface NodeConfiguration : NodeSSLConfiguration { val myLegalName: CordaX500Name val emailAddress: String - val exportJMXto: String + val jmxMonitoringHttpPort: Int? val dataSourceProperties: Properties val rpcUsers: List val security: SecurityConfiguration? @@ -118,6 +118,7 @@ data class NodeConfigurationImpl( /** This is not retrieved from the config file but rather from a command line argument. */ override val baseDirectory: Path, override val myLegalName: CordaX500Name, + override val jmxMonitoringHttpPort: Int? = null, override val emailAddress: String, override val keyStorePassword: String, override val trustStorePassword: String, @@ -184,7 +185,6 @@ data class NodeConfigurationImpl( return errors } - override val exportJMXto: String get() = "http" override val transactionCacheSizeBytes: Long get() = transactionCacheSizeMegaBytes?.MB ?: super.transactionCacheSizeBytes override val attachmentContentCacheSizeBytes: Long diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index f6837b80c1..ab07903a1e 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -144,7 +144,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS) // JMX enablement - if (config.exportJMXto.isNotEmpty()) { + if (config.jmxMonitoringHttpPort != null) { isJMXManagementEnabled = true isJMXUseBrokerName = true } diff --git a/node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt b/node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt new file mode 100644 index 0000000000..54850dee10 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt @@ -0,0 +1,50 @@ +package net.corda.node.utilities + +import com.ea.agentloader.AgentLoader +import net.corda.core.internal.exists +import net.corda.core.internal.isRegularFile +import java.net.URLClassLoader +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap + +/** + * Helper class for loading JVM agents dynamically + */ +object JVMAgentRegistry { + + /** + * Names and options of loaded agents + */ + val loadedAgents = ConcurrentHashMap() + + /** + * Load and attach agent located at given [jar], unless [loadedAgents] + * indicate that one of its instance has been already loaded. + */ + fun attach(agentName: String, options: String, jar: Path) { + loadedAgents.computeIfAbsent(agentName.toLowerCase()) { + AgentLoader.loadAgent(jar.toString(), options) + options + } + } + + /** + * Attempt finding location of jar for given agent by first searching into + * "drivers" directory of [nodeBaseDirectory] and then falling back to + * classpath. Returns null if no match is found. + */ + fun resolveAgentJar(jarFileName: String, driversDir: Path): Path? { + require(jarFileName.endsWith(".jar")) { "jarFileName does not have .jar suffix" } + + val path = Paths.get(driversDir.toString(), jarFileName) + return if (path.exists() && path.isRegularFile()) { + path + } else { + (this::class.java.classLoader as? URLClassLoader) + ?.urLs + ?.map { Paths.get(it.path) } + ?.firstOrNull { it.fileName.toString() == jarFileName } + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt new file mode 100644 index 0000000000..5d7d96cf56 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt @@ -0,0 +1,27 @@ +package net.corda.node.utilities + +import java.util.* + +/** + * Expose properties defined in top-level 'constants.properties' file. + */ +object NodeBuildProperties { + + // Note: initialization order is important + private val data by lazy { + Properties().apply { + NodeBuildProperties::class.java.getResourceAsStream("/build.properties") + ?.let { load(it) } + } + } + + /** + * Jolokia dependency version + */ + val JOLOKIA_AGENT_VERSION = get("jolokiaAgentVersion") + + /** + * Get property value by name + */ + fun get(key: String): String? = data.getProperty(key) +} \ No newline at end of file diff --git a/node/src/main/resources/build.properties b/node/src/main/resources/build.properties new file mode 100644 index 0000000000..72577915e7 --- /dev/null +++ b/node/src/main/resources/build.properties @@ -0,0 +1,5 @@ +# Build constants exported as resource file to make them visible in Node program +# Note: sadly, due to present limitation of IntelliJ-IDEA in processing resource files, these constants cannot be +# imported from top-level 'constants.properties' file + +jolokiaAgentVersion=1.3.7 \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt index 2f6dfc23a1..d9e9f5512e 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt @@ -67,7 +67,7 @@ class ArtemisMessagingTest { doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(NetworkHostAndPort("0.0.0.0", serverPort)).whenever(it).p2pAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies doReturn(5).whenever(it).messageRedeliveryDelaySeconds } diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 3a08b300c9..39ba0cdece 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -91,10 +91,11 @@ class DriverTests { @Test fun `monitoring mode enables jolokia exporting of JMX metrics via HTTP JSON`() { - driver(DriverParameters(jmxPolicy = JmxPolicy(true))) { + driver(DriverParameters(startNodesInProcess = false)) { // start another node so we gain access to node JMX metrics - startNode(providedName = DUMMY_REGULATOR_NAME).getOrThrow() val webAddress = NetworkHostAndPort("localhost", 7006) + startNode(providedName = DUMMY_REGULATOR_NAME, + customOverrides = mapOf("jmxMonitoringHttpPort" to webAddress.port)).getOrThrow() // request access to some JMX metrics via Jolokia HTTP/JSON val api = HttpApi.fromHostAndPort(webAddress, "/jolokia/") val versionAsJson = api.getJson("/jolokia/version/") diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index e19d571f1d..b00f4a9db7 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -469,7 +469,7 @@ private fun mockNodeConfiguration(): NodeConfiguration { doReturn(null).whenever(it).notary doReturn(DatabaseConfig()).whenever(it).database doReturn("").whenever(it).emailAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(true).whenever(it).devMode doReturn(null).whenever(it).compatibilityZoneURL doReturn(emptyList()).whenever(it).certificateChainCheckPolicies