From 05a1fa8f3664dd03087e9ccce064f817d7713657 Mon Sep 17 00:00:00 2001 From: Gustavo Cordova Avila Date: Wed, 20 Mar 2024 17:15:55 -0700 Subject: [PATCH] Initial commit --- logging.nim | 335 +++++++++++++++++++++++++++++++++++++++++++++++++ properties.nim | 5 + 2 files changed, 340 insertions(+) create mode 100644 logging.nim create mode 100644 properties.nim diff --git a/logging.nim b/logging.nim new file mode 100644 index 0000000..e9d5720 --- /dev/null +++ b/logging.nim @@ -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 + "\x0a": "\\n", # 0x0a + "\x0b": "\\v", # 0x0b + "\x0c": "\\f", # 0x0c form-feed + "\x0d": "\\r", # 0x0d + "\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 + "\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 diff --git a/properties.nim b/properties.nim new file mode 100644 index 0000000..f1d68f6 --- /dev/null +++ b/properties.nim @@ -0,0 +1,5 @@ +# just let's get these definitions out of the way +let + beVerbose* = true + beDebugging* = true + LoggingLevel* = "INFO"