mirror of
https://github.com/waku-org/nwaku.git
synced 2025-01-15 17:35:45 +00:00
338 lines
11 KiB
Nim
338 lines
11 KiB
Nim
import
|
|
macros, strutils, strformat, sequtils, os
|
|
|
|
# The default behavior of Chronicles can be configured through a wide-range
|
|
# of compile-time -d: switches (for more information, see the README).
|
|
# This module implements the validation of all specified options and reduces
|
|
# them to a `Configuration` constant that can be accessed from the rest of
|
|
# the modules.
|
|
|
|
const
|
|
chronicles_enabled {.strdefine.} = "on"
|
|
chronicles_default_output_device* {.strdefine.} = "stdout"
|
|
|
|
chronicles_sinks* {.strdefine.} = ""
|
|
chronicles_streams* {.strdefine.} = ""
|
|
|
|
chronicles_enabled_topics {.strdefine.} = ""
|
|
chronicles_required_topics {.strdefine.} = ""
|
|
chronicles_disabled_topics {.strdefine.} = ""
|
|
chronicles_runtime_filtering {.strdefine.} = "off"
|
|
chronicles_log_level {.strdefine.} = when defined(release): "INFO"
|
|
else: "DEBUG"
|
|
|
|
chronicles_timestamps {.strdefine.} = "RfcTime"
|
|
chronicles_colors* {.strdefine.} = "NativeColors"
|
|
|
|
chronicles_indent {.intdefine.} = 2
|
|
chronicles_line_numbers {.strdefine.} = "off"
|
|
|
|
truthySwitches = ["yes", "1", "on", "true"]
|
|
falsySwitches = ["no", "0", "off", "false", "none"]
|
|
|
|
when chronicles_streams.len > 0 and chronicles_sinks.len > 0:
|
|
{.error: "Please specify only one of the options 'chronicles_streams' and 'chronicles_sinks'." }
|
|
when chronicles_enabled_topics.len > 0 and chronicles_required_topics.len > 0:
|
|
{.error: "Please specify only one of the options 'chronicles_enabled_topics' and 'chronicles_required_topics'." }
|
|
|
|
type
|
|
LogLevel* = enum
|
|
NONE,
|
|
TRACE,
|
|
DEBUG,
|
|
INFO,
|
|
NOTICE,
|
|
WARN,
|
|
ERROR,
|
|
FATAL
|
|
|
|
LogFormat* = enum
|
|
json,
|
|
textLines,
|
|
textBlocks
|
|
|
|
OutputDeviceKind* = enum
|
|
oStdOut,
|
|
oStdErr,
|
|
oFile,
|
|
oSysLog
|
|
oDynamic
|
|
|
|
LogFileMode = enum
|
|
Append,
|
|
Truncate
|
|
|
|
LogDestination* = object
|
|
case kind*: OutputDeviceKind
|
|
of oFile:
|
|
filename*: string
|
|
truncate*: bool
|
|
else:
|
|
discard
|
|
|
|
TimestampsScheme* = enum
|
|
NoTimestamps,
|
|
UnixTime,
|
|
RfcTime
|
|
|
|
ColorScheme* = enum
|
|
NoColors,
|
|
AnsiColors,
|
|
NativeColors
|
|
|
|
SinkSpec* = object
|
|
format*: LogFormat
|
|
colorScheme*: ColorScheme
|
|
timestamps*: TimestampsScheme
|
|
destinations*: seq[LogDestination]
|
|
|
|
StreamSpec* = object
|
|
name*: string
|
|
sinks*: seq[SinkSpec]
|
|
|
|
Configuration* = object
|
|
streams*: seq[StreamSpec]
|
|
|
|
EnabledTopic* = object
|
|
name*: string
|
|
logLevel*: LogLevel
|
|
|
|
const defaultChroniclesStreamName* = "defaultChroniclesStream"
|
|
|
|
proc handleYesNoOption(optName: string,
|
|
optValue: string): bool {.compileTime.} =
|
|
let canonicalValue = optValue.toLowerAscii
|
|
if canonicalValue in truthySwitches:
|
|
return true
|
|
elif canonicalValue in falsySwitches:
|
|
return false
|
|
else:
|
|
error &"A non-recognized value '{optValue}' for option '{optName}'. " &
|
|
"Please specify either 'on' or 'off'."
|
|
|
|
template handleYesNoOption(opt: untyped): bool =
|
|
handleYesNoOption(astToStr(opt), opt)
|
|
|
|
proc enumValues(E: typedesc[enum]): string =
|
|
result = mapIt(E, $it).join(", ")
|
|
|
|
proc handleEnumOption(E: typedesc[enum],
|
|
optName: string,
|
|
optValue: string): E {.compileTime.} =
|
|
try:
|
|
if optValue.toLowerAscii in falsySwitches:
|
|
type R = type(result)
|
|
return R(0)
|
|
else:
|
|
return parseEnum[E](optValue)
|
|
except: error &"'{optValue}' is not a recognized value for '{optName}'. " &
|
|
&"Allowed values are {enumValues E}"
|
|
|
|
template handleEnumOption(E, varName: untyped): auto =
|
|
handleEnumOption(E, astToStr(varName), varName)
|
|
|
|
template topicsAsSeq*(topics: string): untyped =
|
|
when topics.len > 0:
|
|
topics.split({','} + Whitespace)
|
|
else:
|
|
newSeq[string](0)
|
|
|
|
proc topicsWithLogLevelAsSeq(topics: string): seq[EnabledTopic] =
|
|
var sequence = newSeq[EnabledTopic](0)
|
|
if topics.len > 0:
|
|
for topic in split(topics, {','} + Whitespace):
|
|
var values = topic.split(':')
|
|
if values.len > 1:
|
|
if values[1].all(isDigit):
|
|
sequence.add(EnabledTopic(name: values[0],
|
|
logLevel: LogLevel(parseInt(values[1]))))
|
|
else:
|
|
sequence.add(EnabledTopic(name: values[0],
|
|
logLevel: handleEnumOption(LogLevel,
|
|
values[1])))
|
|
else:
|
|
sequence.add(EnabledTopic(name: values[0], logLevel: NONE))
|
|
return sequence
|
|
|
|
proc logFormatFromIdent(n: NimNode): LogFormat =
|
|
let format = $n
|
|
case format.toLowerAscii
|
|
of "json":
|
|
return json
|
|
of "textlines":
|
|
return textLines
|
|
of "textblocks":
|
|
return textBlocks
|
|
else:
|
|
error &"'{format}' is not a recognized output format. " &
|
|
&"Allowed values are {enumValues LogFormat}."
|
|
|
|
proc makeSinkSpec(fmt: LogFormat,
|
|
colors: ColorScheme,
|
|
timestamps: TimestampsScheme,
|
|
destinations: varargs[LogDestination]): SinkSpec =
|
|
result.format = fmt
|
|
result.colorScheme = colors
|
|
result.timestamps = timestamps
|
|
result.destinations = @destinations
|
|
|
|
func logDestinationFromStr(s: string): LogDestination {.compileTime.} =
|
|
case s.toLowerAscii
|
|
of "stdout": result.kind = oStdOut
|
|
of "stderr": result.kind = oStdErr
|
|
of "syslog": result.kind = oSysLog
|
|
of "dynamic": result.kind = oDynamic
|
|
of "file":
|
|
result.kind = oFile
|
|
result.filename = ""
|
|
result.truncate = false
|
|
else:
|
|
error &"'{s}' is not a recognized output device type. " &
|
|
"Allowed values are StdOut, StdErr, SysLog, File and Dynamic."
|
|
|
|
proc logDestinationFromNode(n: NimNode): LogDestination =
|
|
case n.kind
|
|
of nnkIdent:
|
|
result = logDestinationFromStr($n)
|
|
of nnkCall:
|
|
if n[0].kind != nnkIdent and ($n[0]).toLowerAscii != "file":
|
|
error &"Invalid log destination expression '{n.repr}'. " &
|
|
"Only 'file' destinations accept parameters."
|
|
result.kind = oFile
|
|
result.filename = n[1].repr.replace(" ", "")
|
|
if DirSep != '/': result.filename = result.filename.replace("/", $DirSep)
|
|
if n.len > 2:
|
|
result.truncate = handleEnumOption(LogFileMode, "file mode", $n[2]) == Truncate
|
|
else:
|
|
error &"Invalid log destination expression '{n.repr}'. " &
|
|
"Please refer to the documentation for the supported options."
|
|
|
|
const
|
|
defaultColorScheme = handleEnumOption(ColorScheme, chronicles_colors)
|
|
defaultTimestamsScheme = handleEnumOption(TimestampsScheme, chronicles_timestamps)
|
|
|
|
proc syntaxCheckStreamExpr*(n: NimNode) =
|
|
if n.kind != nnkBracketExpr or n[0].kind != nnkIdent:
|
|
error &"Invalid stream definition. " &
|
|
"Please use a bracket expressions such as 'stream_name[sinks_list]'."
|
|
|
|
proc sinkSpecsFromNode*(streamNode: NimNode): seq[SinkSpec] =
|
|
newSeq(result, 0)
|
|
for i in 1 ..< streamNode.len:
|
|
let n = streamNode[i]
|
|
case n.kind
|
|
of nnkIdent:
|
|
result.add makeSinkSpec(logFormatFromIdent(n),
|
|
defaultColorScheme,
|
|
defaultTimestamsScheme,
|
|
logDestinationFromStr(chronicles_default_output_device))
|
|
of nnkBracketExpr:
|
|
var spec = makeSinkSpec(logFormatFromIdent(n[0]),
|
|
defaultColorScheme,
|
|
defaultTimestamsScheme)
|
|
for i in 1 ..< n.len:
|
|
var hasExplicitColors = false
|
|
|
|
template setColors(c) =
|
|
spec.colorScheme = c
|
|
hasExplicitColors = true
|
|
continue
|
|
|
|
template setTimestamps(t) =
|
|
spec.timestamps = t
|
|
continue
|
|
|
|
let dstSpec = n[i]
|
|
if dstSpec.kind == nnkIdent:
|
|
case ($dstSpec).toLowerAscii:
|
|
of "nocolors": setColors(NoColors)
|
|
of "ansicolors": setColors(AnsiColors)
|
|
of "nativecolors": setColors(NativeColors)
|
|
of "notimestamps": setTimestamps(NoTimestamps)
|
|
of "unixtime": setTimestamps(UnixTime)
|
|
of "rfctime": setTimestamps(RfcTime)
|
|
else: discard
|
|
|
|
let dst = logDestinationFromNode(dstSpec)
|
|
if dst.kind == oSysLog and not hasExplicitColors:
|
|
spec.colorScheme = NoColors
|
|
|
|
spec.destinations.add dst
|
|
result.add spec
|
|
else:
|
|
error &"Invalid log sink expression '{n.repr}'. " &
|
|
"Please refer to the documentation for the supported options."
|
|
|
|
proc parseStreamsSpec(spec: string): Configuration {.compileTime.} =
|
|
newSeq(result.streams, 0)
|
|
var specNodes = parseExpr "(" & spec.replace("\\", "/") & ")"
|
|
for n in specNodes:
|
|
syntaxCheckStreamExpr(n)
|
|
let streamName = $n[0]
|
|
for prev in result.streams:
|
|
if prev.name == streamName:
|
|
error &"The stream name '{streamName}' appears twice in the 'chronicles_streams' definition."
|
|
|
|
result.streams.add StreamSpec(name: streamName,
|
|
sinks: sinkSpecsFromNode(n))
|
|
|
|
proc overlappingOutputsError(stream: StreamSpec, outputName: string) =
|
|
# XXX: This must be a proc until https://github.com/nim-lang/Nim/issues/7632 is fixed
|
|
error &"In the {stream.name} stream, there are multiple output formats pointed " &
|
|
&"to {outputName}. This is not a supported configuration."
|
|
|
|
for stream in mitems(result.streams):
|
|
var stdoutSinks = 0
|
|
var stderrSinks = 0
|
|
var syslogSinks = 0
|
|
for sink in mitems(stream.sinks):
|
|
for dst in mitems(sink.destinations):
|
|
case dst.kind
|
|
of oStdOut:
|
|
inc stdoutSinks
|
|
if stdoutSinks > 1: overlappingOutputsError(stream, "stdout")
|
|
of oStdErr:
|
|
inc stderrSinks
|
|
if stderrSinks > 1: overlappingOutputsError(stream, "stderr")
|
|
of oSysLog:
|
|
inc syslogSinks
|
|
if stderrSinks > 1: overlappingOutputsError(stream, "syslog")
|
|
if sink.colorScheme != NoColors:
|
|
error "Using a color scheme is not supported when logging to syslog."
|
|
when not defined(posix):
|
|
warning "Logging to syslog is available only on POSIX systems."
|
|
else: discard
|
|
|
|
proc parseSinksSpec(spec: string): Configuration {.compileTime.} =
|
|
return parseStreamsSpec(&"{defaultChroniclesStreamName}[{spec}]")
|
|
|
|
const
|
|
loggingEnabled* = handleYesNoOption chronicles_enabled
|
|
runtimeFilteringEnabled* = handleYesNoOption chronicles_runtime_filtering
|
|
|
|
enabledLogLevel* = handleEnumOption(LogLevel, chronicles_log_level)
|
|
|
|
textBlockIndent* = repeat(' ', chronicles_indent)
|
|
|
|
enabledTopics* = topicsWithLogLevelAsSeq chronicles_enabled_topics
|
|
disabledTopics* = topicsAsSeq chronicles_disabled_topics
|
|
requiredTopics* = topicsAsSeq chronicles_required_topics
|
|
lineNumbersEnabled* = handleYesNoOption chronicles_line_numbers
|
|
|
|
config* = when chronicles_streams.len > 0: parseStreamsSpec(chronicles_streams)
|
|
elif chronicles_sinks.len > 0: parseSinksSpec(chronicles_sinks)
|
|
# default is textlines because:
|
|
# * better compatibility with typical log processing tools
|
|
# like grep, logstash etc where newline delieates events or units
|
|
# * easier to match with a regex
|
|
# * good use of screen real estate
|
|
# * wins nimbus developer straw poll
|
|
# alternatively, one could prefer to use "textblocks" - it can be
|
|
# enabled by passing -d:chronicles_sinks=textblocks
|
|
# * some tools understand that indented lines following newline
|
|
# "belong" to the same logging eevent
|
|
# * wrapping more likely to happen making line hard to read on
|
|
# narrow terminals
|
|
# * properies may be easier to find
|
|
else: parseSinksSpec "textlines"
|