diff --git a/build.gradle b/build.gradle
index 8fc19d166c..5af04463c9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -112,11 +112,10 @@ tasks.withType(Test) {
task buildCordaJAR(type: FatCapsule, dependsOn: ['quasarScan', 'buildCertSigningRequestUtilityJAR']) {
applicationClass 'net.corda.node.MainKt'
archiveName "corda-${corda_version}.jar"
- applicationSource = files(project.tasks.findByName('jar'), 'node/build/classes/main/CordaCaplet.class')
+ applicationSource = files(project.tasks.findByName('jar'), 'node/build/classes/main/CordaCaplet.class', 'config/dev/log4j2.xml')
capsuleManifest {
appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"]
- systemProperties['log4j.configuration'] = 'log4j2.xml'
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"]
minJavaVersion = '1.8.0'
caplets = ['CordaCaplet']
diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml
index 2d293c6a89..60cebc655a 100644
--- a/config/dev/log4j2.xml
+++ b/config/dev/log4j2.xml
@@ -4,7 +4,8 @@
logs
node-${hostName}
- ${log-path}/archive
+ ${sys:log-path}/archive
+ error
@@ -21,7 +22,7 @@
@@ -46,14 +47,13 @@
-
-
-
+
+
+
-
+
-
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt
index 6fb5d96d97..3a31d5faad 100644
--- a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt
+++ b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt
@@ -4,7 +4,8 @@ package net.corda.core.utilities
* A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal.
*/
object Emoji {
- val hasEmojiTerminal by lazy { System.getenv("TERM") != null && (System.getenv("LANG")?.contains("UTF-8") == true) }
+ // Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default.
+ val hasEmojiTerminal by lazy { System.getenv("TERM_PROGRAM") == "Apple_Terminal" }
const val CODE_DIAMOND = "\ud83d\udd37"
const val CODE_BAG_OF_CASH = "\ud83d\udcb0"
@@ -13,12 +14,13 @@ object Emoji {
const val CODE_LEFT_ARROW = "\u2b05\ufe0f"
const val CODE_GREEN_TICK = "\u2705"
const val CODE_PAPERCLIP = "\ud83d\udcce"
+ const val CODE_COOL_GUY = "\ud83d\ude0e"
/**
* When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a
* sufficiently capable text surface.
*/
- private val emojiMode = ThreadLocal()
+ val emojiMode = ThreadLocal()
val diamond: String get() = if (emojiMode.get() != null) "$CODE_DIAMOND " else ""
val bagOfCash: String get() = if (emojiMode.get() != null) "$CODE_BAG_OF_CASH " else ""
@@ -26,6 +28,16 @@ object Emoji {
val rightArrow: String get() = if (emojiMode.get() != null) "$CODE_RIGHT_ARROW " else ""
val leftArrow: String get() = if (emojiMode.get() != null) "$CODE_LEFT_ARROW " else ""
val paperclip: String get() = if (emojiMode.get() != null) "$CODE_PAPERCLIP " else ""
+ val coolGuy: String get() = if (emojiMode.get() != null) "$CODE_COOL_GUY " else ""
+
+ inline fun renderIfSupported(body: () -> T): T {
+ emojiMode.set(this) // Could be any object.
+ try {
+ return body()
+ } finally {
+ emojiMode.set(null)
+ }
+ }
fun renderIfSupported(obj: Any): String {
if (!hasEmojiTerminal)
diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst
index 7cf0211341..4f2eaba3a4 100644
--- a/docs/source/node-administration.rst
+++ b/docs/source/node-administration.rst
@@ -4,6 +4,15 @@ Node administration
When a node is running, it exposes an embedded database server, an embedded web server that lets you monitor it,
you can upload and download attachments, access a REST API and so on.
+Logging
+-------
+
+Logs are stored to the logs subdirectory of the node directory and are rotated from time to time. You can
+have logging printed to the console as well by passing the ``--log-to-console`` command line flag. Corda
+uses the log4j2 framework to manage its logging, so you can also configure it in more detail by writing
+a custom logging configuration file and passing ``-Dlog4j.configurationFile=my-config-file.xml`` on the
+command line as well.
+
Database access
---------------
diff --git a/node/build.gradle b/node/build.gradle
index 0ddf64f45a..913ae7d6da 100644
--- a/node/build.gradle
+++ b/node/build.gradle
@@ -46,6 +46,11 @@ sourceSets {
srcDir "../config/test"
}
}
+ main {
+ resources {
+ srcDir "../config/dev"
+ }
+ }
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
diff --git a/node/src/main/kotlin/net/corda/node/Main.kt b/node/src/main/kotlin/net/corda/node/Main.kt
index 039ec3d92b..57c3298140 100644
--- a/node/src/main/kotlin/net/corda/node/Main.kt
+++ b/node/src/main/kotlin/net/corda/node/Main.kt
@@ -1,61 +1,81 @@
package net.corda.node
+import com.typesafe.config.ConfigException
+import joptsimple.OptionParser
+import net.corda.core.div
+import net.corda.core.randomOrNull
+import net.corda.core.rootCause
+import net.corda.core.then
+import net.corda.core.utilities.Emoji
+import net.corda.node.internal.Node
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.FullNodeConfiguration
-import joptsimple.OptionParser
+import net.corda.node.utilities.ANSIProgressObserver
+import org.fusesource.jansi.Ansi
import org.slf4j.LoggerFactory
import java.lang.management.ManagementFactory
import java.net.InetAddress
-import java.nio.file.Path
import java.nio.file.Paths
+import kotlin.system.exitProcess
-private val log = LoggerFactory.getLogger("Main")
+private var renderBasicInfoToConsole = true
-object ParamsSpec {
- val parser = OptionParser()
-
- // The intent of allowing a command line configurable directory and config path is to allow deployment flexibility.
- // Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line
- val baseDirectoryArg =
- parser.accepts("base-directory", "The directory to put all files under")
- .withOptionalArg()
- val configFileArg =
- parser.accepts("config-file", "The path to the config file")
- .withOptionalArg()
+/** Used for useful info that we always want to show, even when not logging to the console */
+fun printBasicNodeInfo(description: String, info: String? = null) {
+ if (renderBasicInfoToConsole) {
+ val msg = if (info == null) description else "${description.padEnd(40)}: $info"
+ println(msg)
+ } else {
+ val msg = if (info == null) description else "$description: $info"
+ LoggerFactory.getLogger("Main").info(msg)
+ }
}
fun main(args: Array) {
- log.info("Starting Corda Node")
+ val parser = OptionParser()
+ // The intent of allowing a command line configurable directory and config path is to allow deployment flexibility.
+ // Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line
+ val baseDirectoryArg = parser.accepts("base-directory", "The directory to put all files under").withOptionalArg()
+ val configFileArg = parser.accepts("config-file", "The path to the config file").withOptionalArg()
+ val logToConsoleArg = parser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
+ val helpArg = parser.accepts("help").forHelp()
+
val cmdlineOptions = try {
- ParamsSpec.parser.parse(*args)
+ parser.parse(*args)
} catch (ex: Exception) {
- log.error("Unable to parse args", ex)
- System.exit(1)
- return
+ println("Unknown command line arguments: ${ex.message}")
+ exitProcess(1)
}
- val baseDirectoryPath = if (cmdlineOptions.has(ParamsSpec.baseDirectoryArg)) Paths.get(cmdlineOptions.valueOf(ParamsSpec.baseDirectoryArg)) else Paths.get(".").normalize()
- val configFile = if (cmdlineOptions.has(ParamsSpec.configFileArg)) Paths.get(cmdlineOptions.valueOf(ParamsSpec.configFileArg)) else null
- val conf = FullNodeConfiguration(ConfigHelper.loadConfig(baseDirectoryPath, configFile))
+ // Maybe render command line help.
+ if (cmdlineOptions.has(helpArg)) {
+ parser.printHelpOn(System.out)
+ exitProcess(0)
+ }
+
+ // Set up logging.
+ if (cmdlineOptions.has(logToConsoleArg)) {
+ // This property is referenced from the XML config file.
+ System.setProperty("consoleLogLevel", "info")
+ renderBasicInfoToConsole = false
+ }
+
+ drawBanner()
+
+ val baseDirectoryPath = if (cmdlineOptions.has(baseDirectoryArg)) Paths.get(cmdlineOptions.valueOf(baseDirectoryArg)) else Paths.get(".").normalize()
+ System.setProperty("log-path", (baseDirectoryPath / "logs").toAbsolutePath().toString())
+
+ val log = LoggerFactory.getLogger("Main")
+ printBasicNodeInfo("Logs can be found in", System.getProperty("log-path"))
+ val configFile = if (cmdlineOptions.has(configFileArg)) Paths.get(cmdlineOptions.valueOf(configFileArg)) else null
+ val conf = try {
+ FullNodeConfiguration(ConfigHelper.loadConfig(baseDirectoryPath, configFile))
+ } catch (e: ConfigException) {
+ println("Unable to load the configuration file: ${e.rootCause.message}")
+ exitProcess(2)
+ }
val dir = conf.basedir.toAbsolutePath().normalize()
- logInfo(args, dir)
- try {
- val dirFile = dir.toFile()
- if (!dirFile.exists())
- dirFile.mkdirs()
-
- val node = conf.createNode()
- node.start()
- node.run()
- } catch (e: Exception) {
- log.error("Exception during node startup", e)
- System.exit(1)
- }
- System.exit(0)
-}
-
-private fun logInfo(args: Array, dir: Path?) {
log.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().getPath()}")
val info = ManagementFactory.getRuntimeMXBean()
log.info("CommandLine Args: ${info.getInputArguments().joinToString(" ")}")
@@ -65,5 +85,69 @@ private fun logInfo(args: Array, dir: Path?) {
log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
log.info("Machine: ${InetAddress.getLocalHost().hostName}")
log.info("Working Directory: ${dir}")
+
+ try {
+ val dirFile = dir.toFile()
+ if (!dirFile.exists())
+ dirFile.mkdirs()
+
+ val node = conf.createNode()
+ val startTime = System.currentTimeMillis()
+
+ node.start()
+ printPluginsAndServices(node)
+
+ node.networkMapRegistrationFuture.then {
+ val elapsed = (System.currentTimeMillis() - startTime) / 10 / 1000.0
+ printBasicNodeInfo("Node started up and registered in $elapsed sec")
+
+ if (renderBasicInfoToConsole)
+ ANSIProgressObserver(node.smm)
+ }
+ node.run()
+ } catch (e: Exception) {
+ log.error("Exception during node startup", e)
+ exitProcess(1)
+ }
+ exitProcess(0)
}
+private fun printPluginsAndServices(node: Node) {
+ node.configuration.extraAdvertisedServiceIds.let { if (it.isNotEmpty()) printBasicNodeInfo("Providing network services", it) }
+ val plugins = node.pluginRegistries.map { it.javaClass.name }.filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") }.map { it.replaceAfter('$', "") }
+ if (plugins.isNotEmpty())
+ printBasicNodeInfo("Loaded plugins", plugins.joinToString())
+}
+
+private fun messageOfTheDay(): Pair {
+ val messages = arrayListOf(
+ "The only distributed ledger that pays\nhomage to Pac Man in its logo.",
+ "The officially approved platform of the\nglobal capitalist lizard conspiracy™ ${Emoji.bagOfCash}",
+ "It's not who you know, it's who you know\nknows what you know you know.",
+ "It runs on the JVM because QuickBasic\nis apparently not 'professional' enough.",
+ "\"It's OK computer, I go to sleep after\ntwenty minutes of inactivity too!\"",
+ "It's kind of like a block chain but\ncords sounded healthier than chains.",
+ "Computer science and finance together.\nYou should see our crazy Christmas parties!"
+
+ )
+ if (Emoji.hasEmojiTerminal)
+ messages +=
+ "Kind of like a regular database but\nwith emojis, colours and ascii art. ${Emoji.coolGuy}"
+ val (a, b) = messages.randomOrNull()!!.split('\n')
+ return Pair(a, b)
+}
+
+private fun drawBanner() {
+ val (msg1, msg2) = Emoji.renderIfSupported { messageOfTheDay() }
+
+ println(Ansi.ansi().fgBrightRed().a(
+"""
+ ______ __
+ / ____/ _________/ /___ _
+ / / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a(
+"/ /___ /_/ / / / /_/ / /_/ / ").fgBrightBlue().a(msg2).newline().fgBrightRed().a(
+"""\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().
+a("--- DEVELOPER SNAPSHOT ------------------------------------------------------------").newline().reset())
+}
+
+
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 3186d92297..6527473eef 100644
--- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
@@ -41,7 +41,10 @@ import net.corda.node.services.statemachine.StateMachineManager
import net.corda.node.services.transactions.*
import net.corda.node.services.vault.CashBalanceAsMetricsObserver
import net.corda.node.services.vault.NodeVaultService
-import net.corda.node.utilities.*
+import net.corda.node.utilities.AddOrRemove
+import net.corda.node.utilities.AffinityExecutor
+import net.corda.node.utilities.configureDatabase
+import net.corda.node.utilities.databaseTransaction
import net.corda.protocols.CashCommand
import net.corda.protocols.CashProtocol
import net.corda.protocols.sendRequest
@@ -165,7 +168,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
get() = _networkMapRegistrationFuture
/** Fetch CordaPluginRegistry classes registered in META-INF/services/net.corda.core.node.CordaPluginRegistry files that exist in the classpath */
- protected val pluginRegistries: List by lazy {
+ val pluginRegistries: List by lazy {
ServiceLoader.load(CordaPluginRegistry::class.java).toList()
}
@@ -233,8 +236,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
buildAdvertisedServices()
// TODO: this model might change but for now it provides some de-coupling
- // Add SMM observers
- ANSIProgressObserver(smm)
// Add vault observers
CashBalanceAsMetricsObserver(services)
ScheduledActivityObserver(services)
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 9659147187..0d370f3fbd 100644
--- a/node/src/main/kotlin/net/corda/node/internal/Node.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt
@@ -9,6 +9,7 @@ import net.corda.core.node.services.ServiceType
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.then
import net.corda.core.utilities.loggerFor
+import net.corda.node.printBasicNodeInfo
import net.corda.node.serialization.NodeClock
import net.corda.node.services.RPCUserService
import net.corda.node.services.RPCUserServiceImpl
@@ -41,6 +42,7 @@ import org.jetbrains.exposed.sql.Database
import java.io.RandomAccessFile
import java.lang.management.ManagementFactory
import java.lang.reflect.InvocationTargetException
+import java.net.InetAddress
import java.nio.channels.FileLock
import java.time.Clock
import java.util.*
@@ -63,12 +65,6 @@ class ConfigurationException(message: String) : Exception(message)
*/
class Node(override val configuration: FullNodeConfiguration, networkMapAddress: SingleMessageRecipient?,
advertisedServices: Set, clock: Clock = NodeClock()) : AbstractNode(configuration, networkMapAddress, advertisedServices, clock) {
- companion object {
- /** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */
- @JvmField
- val DEFAULT_PORT = 31337
- }
-
override val log = loggerFor()
// DISCUSSION
@@ -198,11 +194,11 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
httpConnector
}
server.connectors = arrayOf(connector)
- log.info("Starting web API server on port ${connector.port}")
server.handler = handlerCollection
runOnStop += Runnable { server.stop() }
server.start()
+ printBasicNodeInfo("Embedded web server is listening on", "http://${InetAddress.getLocalHost().hostAddress}:${connector.port}/")
return server
}
@@ -295,7 +291,7 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
"-tcpDaemon",
"-key", "node", databaseName)
val url = server.start().url
- log.info("H2 JDBC url is jdbc:h2:$url/node")
+ printBasicNodeInfo("Database connection url is", "jdbc:h2:$url/node")
}
}
super.initialiseDatabasePersistence(insideTransaction)
@@ -362,7 +358,7 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
shutdownThread = null
}
}
- log.info("Shutting down ...")
+ printBasicNodeInfo("Shutting down ...")
// All the Node started subsystems were registered with the runOnStop list at creation.
// So now simply call the parent to stop everything in reverse order.
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 0a0401fefa..b6e098030e 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
@@ -7,6 +7,7 @@ import net.corda.core.div
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.utilities.loggerFor
+import net.corda.node.printBasicNodeInfo
import net.corda.node.services.RPCUserService
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.NODE_USER
@@ -170,6 +171,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
}
}
activeMQServer.start()
+ printBasicNodeInfo("Node listening on address", myHostPort.toString())
}
private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply {