Initial commit
This commit is contained in:
335
logging.nim
Normal file
335
logging.nim
Normal file
@@ -0,0 +1,335 @@
|
||||
## 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
|
||||
5
properties.nim
Normal file
5
properties.nim
Normal file
@@ -0,0 +1,5 @@
|
||||
# just let's get these definitions out of the way
|
||||
let
|
||||
beVerbose* = true
|
||||
beDebugging* = true
|
||||
LoggingLevel* = "INFO"
|
||||
Reference in New Issue
Block a user