Add a startup banner and suppress console logging unless --log-to-console is passed on the command line.

This commit is contained in:
Mike Hearn 2016-11-18 18:30:51 +01:00
parent 78e568128c
commit 235cee6727
9 changed files with 171 additions and 63 deletions

View File

@ -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']

View File

@ -4,7 +4,8 @@
<Properties>
<Property name="log-path">logs</Property>
<Property name="log-name">node-${hostName}</Property>
<Property name="archive">${log-path}/archive</Property>
<Property name="archive">${sys:log-path}/archive</Property>
<Property name="consoleLogLevel">error</Property>
</Properties>
<ThresholdFilter level="trace"/>
@ -21,7 +22,7 @@
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
those that are older than 60 days, but keep the most recent 10 GB -->
<RollingFile name="RollingFile-Appender"
fileName="${log-path}/${log-name}.log"
fileName="${sys:log-path}/${log-name}.log"
filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%-5level] %d{ISO8601}{GMT+0} [%t] %c{1} - %msg%n"/>
@ -46,14 +47,13 @@
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console-Appender"/>
<AppenderRef ref="RollingFile-Appender"/>
<Root>
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
<AppenderRef ref="RollingFile-Appender" level="info"/>
</Root>
<Logger name="net.corda" level="info" additivity="false">
<AppenderRef ref="Console-Appender"/>
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
<AppenderRef ref="RollingFile-Appender"/>
</Logger>
</Loggers>
</Configuration>

View File

@ -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<Any>()
val emojiMode = ThreadLocal<Any>()
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 <T> 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)

View File

@ -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
---------------

View File

@ -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

View File

@ -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<String>) {
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<String>, 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<String>, 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<String, String> {
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())
}

View File

@ -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<CordaPluginRegistry> by lazy {
val pluginRegistries: List<CordaPluginRegistry> 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)

View File

@ -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<ServiceInfo>, 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<Node>()
// DISCUSSION
@ -198,11 +194,11 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
httpConnector
}
server.connectors = arrayOf<Connector>(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.

View File

@ -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 {