mirror of
https://github.com/corda/corda.git
synced 2025-02-03 17:50:41 +00:00
Add a startup banner and suppress console logging unless --log-to-console is passed on the command line.
This commit is contained in:
parent
78e568128c
commit
235cee6727
@ -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']
|
||||
|
@ -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>
|
@ -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)
|
||||
|
@ -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
|
||||
---------------
|
||||
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user