Initial commit

This commit is contained in:
Gustavo Cordova Avila
2024-03-20 17:15:55 -07:00
commit 05a1fa8f36
2 changed files with 340 additions and 0 deletions

335
logging.nim Normal file
View 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
View File

@@ -0,0 +1,5 @@
# just let's get these definitions out of the way
let
beVerbose* = true
beDebugging* = true
LoggingLevel* = "INFO"