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 {