336 lines
11 KiB
Nim
336 lines
11 KiB
Nim
## Logging procedures and templates
|
|
## Very rudimentary, held together with twine and chewing gum.
|
|
##
|
|
## [Thu Mar 7 11:25:18 AM PST 2024]
|
|
## Changed formatting to JSON output
|
|
import std/[exitprocs, locks, strutils, terminal, times]
|
|
import ./properties
|
|
|
|
################################################################
|
|
## Log levels, and functions to convert to/from
|
|
##
|
|
type
|
|
LogLevel* = enum
|
|
ALWAYS, # Not an error, but this will always be shown
|
|
FATAL, # Application will have to abort
|
|
CRITICAL, # Critical functionality will fail
|
|
ERROR, # An error occurred
|
|
WARNING, # ditto
|
|
QUIET, # Important information
|
|
INFO, # Information
|
|
NOISY, # Maybe not-so-important information
|
|
DEBUG, # Debugging information
|
|
TRACE # Very noisy, trace-level information
|
|
|
|
proc parseLogLevel*(str: string; raiseErr: bool = false): LogLevel =
|
|
## Parse a logging level from its string representation
|
|
let ustr = str.toUpperAscii()
|
|
for i in LogLevel:
|
|
if ustr == $i:
|
|
return i
|
|
let errMsg = "Unable to identify log level '$#'" % str
|
|
if raiseErr:
|
|
raise newException(ValueError, errMsg)
|
|
stderr.writeLine(errMsg & "; default to INFO")
|
|
return INFO
|
|
|
|
|
|
################################################################
|
|
## Output type format
|
|
##
|
|
type
|
|
LogOutputFormat* = enum
|
|
JSON, # Output format is JSON compatible
|
|
COMMON, # Similar to Apache CommonLog Format
|
|
TEXT # A more verbose text logging format
|
|
|
|
proc parseLogFormat*(str: string; raiseErr: bool = false): LogOutputFormat =
|
|
## Parse a logging format name into it's value
|
|
let ustr = str.toUpperAscii()
|
|
for i in LogOutputFormat:
|
|
if ustr == $i:
|
|
return i
|
|
let errMsg = "Unable to identify the output format '$#'" % str
|
|
if raiseErr:
|
|
raise newException(ValueError, errMsg)
|
|
stderr.writeLine(errMsg & "; default to JSON")
|
|
|
|
|
|
################################################################
|
|
## Types and stuff required for all this internal machinery
|
|
##
|
|
type
|
|
KVPair* = tuple[key: string; val: string]
|
|
KVPairs* = seq[KVPair]
|
|
|
|
Logger* = ref object
|
|
name*: string
|
|
extra*: KVPairs
|
|
|
|
|
|
################################################################
|
|
## This is the logging module's internal state
|
|
##
|
|
var
|
|
logsLock: Lock
|
|
logsStream: File = stdout
|
|
logsLevel: LogLevel = properties.LoggingLevel.parseLogLevel()
|
|
logsFormat: LogOutputFormat = JSON
|
|
logsIsTTY: bool = false
|
|
|
|
|
|
################################################################
|
|
## Manipulate the module's internal state.
|
|
##
|
|
proc setLogStream*(stream: File) =
|
|
## Set the logging output stream
|
|
logsStream = stream
|
|
logsIsTTY = isatty(stream)
|
|
|
|
proc setLogLevel*(lvl: LogLevel) =
|
|
## Set the log level from a log level value
|
|
logsLevel = lvl
|
|
|
|
proc setLogLevel*(lvl: string) =
|
|
## Set the log level from its string representation
|
|
logsLevel = lvl.parseLogLevel()
|
|
|
|
proc setLogFormat*(fmt: LogOutputFormat) =
|
|
## Set the log output format to 'fmt'
|
|
logsFormat = fmt
|
|
|
|
proc setLogFormat*(fmt: string) =
|
|
## Set the log output format to 'fmt'
|
|
logsFormat = fmt.parseLogFormat()
|
|
|
|
|
|
################################################################
|
|
## Create a new logger object.
|
|
##
|
|
proc getLogger*(name: string; extra: varargs[KVPair]): Logger =
|
|
## Return a logger object
|
|
new(result)
|
|
result.name = name
|
|
result.extra.add(extra)
|
|
|
|
|
|
################################################################
|
|
## Some important constants that are used elsewhere
|
|
##
|
|
const
|
|
TROUBLE_CHARS = {
|
|
"\x00": "\\x00",
|
|
"\x01": "\\x01",
|
|
"\x02": "\\x02",
|
|
"\x03": "\\x03",
|
|
"\x04": "\\x04",
|
|
"\x05": "\\x05",
|
|
"\x06": "\\x06",
|
|
"\x07": "\\a", # 0x07
|
|
"\x08": "\\b", # 0x08
|
|
"\x09": "\\t", # 0x09 <tab>
|
|
"\x0a": "\\n", # 0x0a <nl>
|
|
"\x0b": "\\v", # 0x0b <vtab>
|
|
"\x0c": "\\f", # 0x0c form-feed
|
|
"\x0d": "\\r", # 0x0d <cr>
|
|
"\x0e": "\\x0e",
|
|
"\x0f": "\\x0f",
|
|
"\x10": "\\x10",
|
|
"\x11": "\\x11",
|
|
"\x12": "\\x12",
|
|
"\x13": "\\x13",
|
|
"\x14": "\\x14",
|
|
"\x15": "\\x15",
|
|
"\x16": "\\x16",
|
|
"\x17": "\\x17",
|
|
"\x18": "\\x18",
|
|
"\x19": "\\x19",
|
|
"\x1a": "\\x1a",
|
|
"\x1b": "\\x1b", # 0x1B <esc>
|
|
"\x1c": "\\x1c",
|
|
"\x1d": "\\x1d",
|
|
"\x1e": "\\x1e",
|
|
"\x1f": "\\x1f",
|
|
"\x7f": "\\x7f", # DEL
|
|
"\xff": "\\xff", }
|
|
|
|
proc q(s: string): string =
|
|
## Apply the trouble-chars-1 replacements
|
|
return s.multiReplace(TROUBLE_CHARS)
|
|
|
|
proc kv(fmt: LogOutputFormat; key, value: string; epilogue: string = ""): string =
|
|
## Return a properly formatted key=value pair
|
|
case fmt:
|
|
of JSON: result = "\"$#\": \"$#\"" % [key.q, value.q]
|
|
of TEXT: result = "$#=\"$#\"" % [key.q, value.q]
|
|
of COMMON: result = "$#=\"$#\"" % [key.q, value.q]
|
|
result.add epilogue
|
|
|
|
proc add(buf: var seq[string]; fmt: LogOutputFormat; kvp: openarray[KVPair]) =
|
|
## Add the kv pairs to the buffer
|
|
if kvp.len > 0:
|
|
for i in 0 ..< kvp.len:
|
|
buf.add fmt.kv(kvp[i].key, kvp[i].val)
|
|
|
|
|
|
################################################################
|
|
## Output the log messages in whatever format to the stream
|
|
##
|
|
proc fmtJSON(log: Logger; lvl: LogLevel; msg: string; extra: varargs[KVPair]): string =
|
|
## Output the content of the message as JSON
|
|
var buffer = @[
|
|
JSON.kv("@timestamp", $(now())),
|
|
JSON.kv("lvl", $lvl),
|
|
JSON.kv("name", log.name),
|
|
JSON.kv("msg", msg) ]
|
|
buffer.add(JSON, log.extra)
|
|
buffer.add(JSON, extra)
|
|
return "{$#}" % buffer.join(", ")
|
|
|
|
|
|
proc fmtTEXT(log: Logger; lvl: LogLevel; msg: string; extra: varargs[KVPair]): string =
|
|
## Output the content of the message as TEXT
|
|
## TODO: WRITE THIS METHOD
|
|
var buffer = @[ "$# [$#] $#: $#" % [$(now()), $lvl, log.name, msg.q] ]
|
|
buffer.add(TEXT, log.extra)
|
|
buffer.add(TEXT, extra)
|
|
return buffer.join("\n\t")
|
|
|
|
|
|
proc fmtCOMMON(log: Logger; lvl: LogLevel; msg: string; extra: varargs[KVPair]): string =
|
|
## Output the content of the message as CommonLogFormat
|
|
## TODO: WRITE THIS METHOD
|
|
## "${timestamp} [${level}] ${name}: ${msg} [; {key}={value}]*"
|
|
var buffer = @[ "$# [$#] $#: $#" % [$(now()), $lvl, log.name, msg.q] ]
|
|
if log.extra.len > 0 or extra.len > 0:
|
|
var buff2: seq[string]
|
|
buff2.add(COMMON, log.extra)
|
|
buff2.add(COMMON, extra)
|
|
buffer.add " { $# }" % buff2.join("; ")
|
|
return buffer.join("")
|
|
|
|
|
|
proc writeMsg*(log: Logger; lvl: LogLevel; msg: string; extra: varargs[KVPair]) =
|
|
## Write a message to the logger object if the level allows
|
|
let txt = case logsFormat:
|
|
of JSON: log.fmtJSON(lvl, msg, extra)
|
|
of COMMON: log.fmtCOMMON(lvl, msg, extra)
|
|
of TEXT: log.fmtTEXT(lvl, msg, extra)
|
|
logsLock.acquire()
|
|
try:
|
|
logsStream.writeLine(txt)
|
|
finally:
|
|
logsLock.release()
|
|
|
|
|
|
################################################################
|
|
## Shorthand for the various levels.
|
|
##
|
|
template always(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Always writes to log
|
|
logger.writeMsg(ALWAYS, msg, extra)
|
|
|
|
template fatal(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Always writes to log, and ... maybe should quit?
|
|
logger.writeMsg(FATAL, msg, extra)
|
|
|
|
template critical(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Only writes to log if logLevel allows it
|
|
if logsLevel >= CRITICAL:
|
|
logger.writeMsg(CRITICAL, msg, extra)
|
|
|
|
template error(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Only writes to log if logLevel allows it
|
|
if logsLevel >= ERROR:
|
|
logger.writeMsg(ERROR, msg, extra)
|
|
|
|
template warning(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Only writes to log if logLevel allows it
|
|
if logsLevel >= WARNING:
|
|
logger.writeMsg(WARNING, msg, extra)
|
|
|
|
template warn(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Only writes to log if logLevel allows it
|
|
if logsLevel >= WARNING:
|
|
logger.writeMsg(WARNING, msg, extra)
|
|
|
|
template quiet(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Only writes to log if logLevel allows it
|
|
if logsLevel >= QUIET:
|
|
logger.writeMsg(QUIET, msg, extra)
|
|
|
|
template info(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Only writes to log if logLevel allows it
|
|
if logsLevel >= INFO:
|
|
logger.writeMsg(INFO, msg, extra)
|
|
|
|
template noisy(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Only writes to log if logLevel allows it
|
|
if logsLevel >= NOISY:
|
|
logger.writeMsg(NOISY, msg, extra)
|
|
|
|
template debug(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Only writes to log if logLevel allows it
|
|
if logsLevel >= DEBUG:
|
|
logger.writeMsg(DEBUG, msg, extra)
|
|
|
|
template trace(logger: Logger; msg: string; extra: varargs[KVPair]) =
|
|
## Only writes to log if logLevel allows it
|
|
if logsLevel >= TRACE:
|
|
## Add to the extra context the filename and location of this call
|
|
const pos {.inject.} = instantiationInfo()
|
|
var moreExtra {.inject.}: seq[KVPair]
|
|
moreExtra.add extra
|
|
moreExtra.add {"trace.filename": pos.filename, "trace.line": $pos.line}
|
|
logger.writeMsg(TRACE, msg, moreExtra)
|
|
|
|
|
|
################################################################
|
|
## These need to be called before any logging happens, and
|
|
## just before the application exits.
|
|
##
|
|
proc initLog() =
|
|
## Initialize the logging data structures
|
|
logsLock.initLock()
|
|
|
|
proc deinitLog() =
|
|
## Free up any resources in 'log'
|
|
logsLock.deinitLock()
|
|
|
|
initLog()
|
|
setLogStream(stdout)
|
|
addExitProc(deinitLog)
|
|
|
|
|
|
################################################################
|
|
## Some simple tests
|
|
##
|
|
when isMainModule:
|
|
## Initialize a logger
|
|
let lg = getLogger("main")
|
|
|
|
proc exampleMessages() =
|
|
lg.always("This should always appear")
|
|
lg.fatal("Such a terrible thing, a FATAL error; bye?")
|
|
lg.critical("The engines are at CRITICAL level!!")
|
|
lg.error("Something went wrong, I caught an ERROR")
|
|
lg.warning("This is your first WARNING, ok?")
|
|
lg.warn("This is your second WARNING, ok?")
|
|
lg.quiet("Something important, say it QUIET")
|
|
lg.info("Some INFOrmational data")
|
|
lg.noisy("Level is low enough that it's NOISY")
|
|
lg.debug("I don't see this unless I'm DEBUGging")
|
|
lg.trace("We're TRACEing the code as we speak")
|
|
|
|
for fmt in LogOutputFormat:
|
|
echo "*** Setting log format to: ", $fmt
|
|
setLogFormat(fmt)
|
|
for ll in LogLevel:
|
|
echo "** Setting level to: ", $ll
|
|
setLogLevel(ll)
|
|
exampleMessages()
|
|
echo ""
|
|
echo ""
|
|
|
|
# fin
|