Some base picocli classes used in network services (#3706)

* Some base picocli classes used in network services

* Fix broken logger init

* Small fix
This commit is contained in:
Katarzyna Streich 2018-07-27 18:25:22 +01:00 committed by Anthony Keenan
parent f20e0e7cf1
commit df4833d448
5 changed files with 126 additions and 7 deletions

View File

@ -69,7 +69,7 @@ buildscript {
ext.snappy_version = '0.4' ext.snappy_version = '0.4'
ext.fast_classpath_scanner_version = '2.12.3' ext.fast_classpath_scanner_version = '2.12.3'
ext.jcabi_manifests_version = '1.1' ext.jcabi_manifests_version = '1.1'
ext.picocli_version = '3.0.0' ext.picocli_version = '3.3.0'
// Name of the IntelliJ SDK created for the deterministic Java rt.jar. // Name of the IntelliJ SDK created for the deterministic Java rt.jar.
// ext.deterministic_idea_sdk = '1.8 (Deterministic)' // ext.deterministic_idea_sdk = '1.8 (Deterministic)'

View File

@ -14,7 +14,7 @@
<Appenders> <Appenders>
<Console name="Console-Appender" target="SYSTEM_OUT"> <Console name="Console-Appender" target="SYSTEM_OUT">
<PatternLayout> <PatternLayout>
<ScriptPatternSelector defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n}{INFO=white,WARN=red,FATAL=bright red}"> <ScriptPatternSelector defaultPattern="%highlight{[%level{length=5}] %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n %throwable{0}}{INFO=white,WARN=red,FATAL=bright red}">
<Script name="MDCSelector" language="javascript"><![CDATA[ <Script name="MDCSelector" language="javascript"><![CDATA[
result = null; result = null;
if (!logEvent.getContextData().size() == 0) { if (!logEvent.getContextData().size() == 0) {
@ -33,7 +33,7 @@
<!-- Required for printBasicInfo --> <!-- Required for printBasicInfo -->
<Console name="Console-Appender-Println" target="SYSTEM_OUT"> <Console name="Console-Appender-Println" target="SYSTEM_OUT">
<PatternLayout pattern="%msg%n" /> <PatternLayout pattern="%msg%n %throwable{0}" />
</Console> </Console>
<!-- Will generate up to 100 log files for a given day. During every rollover it will delete <!-- Will generate up to 100 log files for a given day. During every rollover it will delete

View File

@ -166,6 +166,9 @@ dependencies {
// Apache Shiro: authentication, authorization and session management. // Apache Shiro: authentication, authorization and session management.
compile "org.apache.shiro:shiro-core:${shiro_version}" compile "org.apache.shiro:shiro-core:${shiro_version}"
//Picocli for command line interface
compile "info.picocli:picocli:$picocli_version"
// Integration test helpers // Integration test helpers
integrationTestCompile "junit:junit:$junit_version" integrationTestCompile "junit:junit:$junit_version"
integrationTestCompile "org.assertj:assertj-core:${assertj_version}" integrationTestCompile "org.assertj:assertj-core:${assertj_version}"

View File

@ -68,10 +68,7 @@ open class NodeStartup(val args: Array<String>) {
val registrationMode = checkRegistrationMode() val registrationMode = checkRegistrationMode()
val cmdlineOptions: CmdLineOptions = if (registrationMode && !args.contains("--initial-registration")) { val cmdlineOptions: CmdLineOptions = if (registrationMode && !args.contains("--initial-registration")) {
"Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.".let { println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
println(it)
logger.info(it)
}
// Pretend that the node was started with `--initial-registration` to help prevent user error. // Pretend that the node was started with `--initial-registration` to help prevent user error.
NodeArgsParser().parseOrExit(*args.plus("--initial-registration")) NodeArgsParser().parseOrExit(*args.plus("--initial-registration"))
} else { } else {

View File

@ -0,0 +1,119 @@
package net.corda.node.utilities
import com.jcabi.manifests.Manifests
import net.corda.core.internal.exists
import net.corda.core.internal.isReadable
import net.corda.core.utilities.contextLogger
import org.apache.logging.log4j.Level
import picocli.CommandLine
import picocli.CommandLine.*
import java.nio.file.Path
import kotlin.system.exitProcess
import java.util.*
/**
* Something heavily used in network services, I am not sure it's of much use in corda, but who knows. Definitely it was the key to making DevOps happy.
* Add it as
* `@CommandLine.Mixin
* lateinit var configParser: ConfigFilePathArgsParser`
*
* in your command class and then validate()
*/
@Command(description = ["Parse configuration file. Checks if given configuration file exists"])
class ConfigFilePathArgsParser : Validated {
@Option(names = ["--config-file", "-f"], required = true, paramLabel = "FILE", description = ["The path to the config file"])
lateinit var configFile: Path
override fun validator(): List<String> {
val res = mutableListOf<String>()
if(!configFile.exists()) res += "Config file ${configFile.toAbsolutePath().normalize()} does not exist!"
if(!configFile.isReadable) res += "Config file ${configFile.toAbsolutePath().normalize()} is not readable"
return res
}
}
/**
* Simple version printing when command is called with --version or -V flag. Assuming that we reuse Corda-Release-Version and Corda-Revision
* in the manifest file.
*/
class CordaVersionProvider : IVersionProvider {
override fun getVersion(): Array<String> {
return if (Manifests.exists("Corda-Release-Version") && Manifests.exists("Corda-Revision")) {
arrayOf("Version: ${Manifests.read("Corda-Release-Version")}", "Revision: ${Manifests.read("Corda-Revision")}")
} else {
arrayOf("No version data is available in the MANIFEST file.")
}
}
}
/**
* Usually when we have errors in some command line flags that are not handled by picocli (e.g. non existing file). Error is thrown
* and no CommandLine help afterwards. This can be called from run() method.
*/
interface Validated {
companion object {
val logger = contextLogger()
const val RED = "\u001B[31m"
const val RESET = "\u001B[0m"
}
/**
* Check that provided command line parameters are valid, e.g. check file existence. Return list of error strings.
*/
fun validator(): List<String>
/**
* Function that provides nice error handing of command line validation.
*/
fun validate() {
val errors = validator()
if (errors.isNotEmpty()) {
logger.error(RED + "Exceptions when parsing command line arguments:")
logger.error(errors.joinToString("\n") + RESET)
CommandLine(this).usage(System.err)
exitProcess(1)
}
}
}
/**
* Simple base class for handling help, version, verbose and logging-level commands.
* As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node)
* Picocli will prioritise versionProvider from the `@Command` annotation on the subclass, see: https://picocli.info/#_reuse_combinations
*/
@Command(mixinStandardHelpOptions = true,
versionProvider = CordaVersionProvider::class,
sortOptions = false,
synopsisHeading = "%n@|bold,underline Usage|@:%n%n",
descriptionHeading = "%n@|bold,underline Description|@:%n%n",
parameterListHeading = "%n@|bold,underline Parameters|@:%n%n",
optionListHeading = "%n@|bold,underline Options|@:%n%n",
commandListHeading = "%n@|bold,underline Commands|@:%n%n")
abstract class ArgsParser {
@Option(names = [ "-v", "--verbose" ], description = ["If set, prints logging to the console as well as to a file."])
var verbose: Boolean = false
@Option(names = ["--logging-level"],
// TODO For some reason I couldn't make picocli COMPLETION-CANDIDATES work
description = ["Enable logging at this level and higher. Defaults to INFO. Possible values: OFF, INFO, WARN, TRACE, DEBUG, ERROR, FATAL, ALL"],
converter = [LoggingLevelConverter::class])
var loggingLevel: Level = Level.INFO
// This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before).
// Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used.
protected open fun initLogging() {
val loggingLevel = loggingLevel.name().toLowerCase(Locale.ENGLISH)
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
if (verbose) {
System.setProperty("consoleLogLevel", loggingLevel)
}
}
}
/**
* Converter from String to log4j logging Level.
*/
class LoggingLevelConverter : ITypeConverter<Level> {
override fun convert(value: String?): Level {
return value?.let { Level.getLevel(it) } ?: throw TypeConversionException("Unknown option for --logging-level: $value")
}
}