Major refactor to better support multi-threading.
See README for details.
This commit is contained in:
@ -1,76 +1,306 @@
|
||||
import std/[logging, options, sequtils, strutils, tables]
|
||||
import std/[algorithm, json, locks, options, sequtils, strutils, tables, times]
|
||||
import timeutils, zero_functional
|
||||
|
||||
export logging
|
||||
from logging import Level
|
||||
export logging.Level
|
||||
|
||||
type
|
||||
LoggingNamespace* = ref object
|
||||
name: string
|
||||
LogService* = ptr LogServiceObj
|
||||
## Shareable pointer to the shared log service object.
|
||||
|
||||
LogServiceObj = object
|
||||
cfg*: LogServiceConfig
|
||||
lock: Lock
|
||||
|
||||
LogServiceConfig* = object
|
||||
loggers*: seq[LoggerConfig]
|
||||
appenders: seq[LogAppender]
|
||||
rootLevel*: Level
|
||||
|
||||
ThreadState = object
|
||||
cfg: LogServiceConfig
|
||||
loggers: TableRef[string, Logger]
|
||||
|
||||
LoggerConfig* = object of RootObj
|
||||
name*: string
|
||||
threshold*: Option[Level]
|
||||
|
||||
Logger* = object of LoggerConfig
|
||||
svc: LogService
|
||||
|
||||
LogAppender* = ref object of RootObj
|
||||
## Base type for log appenders.
|
||||
namespace*: string
|
||||
threshold*: Level
|
||||
|
||||
LogMessage* = object
|
||||
scope*: string
|
||||
level*: Level
|
||||
msgPrefix*: string
|
||||
error*: Option[ref Exception]
|
||||
timestamp*: DateTime
|
||||
message*: string
|
||||
additionalData*: JsonNode
|
||||
|
||||
var knownNamespacesInst {.threadvar.}: TableRef[string, LoggingNamespace]
|
||||
ConsoleLogAppender* = ref object of LogAppender
|
||||
## Log appender that writes log messages to the console. See
|
||||
## *initConsoleLogAppender* for a convenient way to create instances of
|
||||
## this appender.
|
||||
formatter*: proc (msg: LogMessage): string {.gcsafe.}
|
||||
## Formatter allows for custom formatting of log messages. The default
|
||||
## formatter uses `formatJsonStructuredLog` to format log messages as
|
||||
## JSON objects which are then stringified before being written to the
|
||||
## console.
|
||||
useStderr*: bool
|
||||
|
||||
template knownNamespaces(): TableRef[string, LoggingNamespace] =
|
||||
if knownNamespacesInst == nil:
|
||||
knownNamespacesInst = newTable[string, LoggingNamespace]()
|
||||
knownNamespacesInst
|
||||
#[
|
||||
# TODO: need to think throudh thread-safe IO for file logging
|
||||
FileLogAppender* = ref object of LogAppender
|
||||
file*: File
|
||||
formatter*: proc (msg: LogMessage): string {.gcsafe.}
|
||||
]#
|
||||
|
||||
proc initLoggingNamespace(
|
||||
name: string,
|
||||
level = lvlInfo,
|
||||
msgPrefix: string
|
||||
): LoggingNamespace {.raises: [].} =
|
||||
|
||||
result = LoggingNamespace(
|
||||
name: name,
|
||||
level: level,
|
||||
msgPrefix: msgPrefix)
|
||||
var threadState {.threadvar.}: ThreadState
|
||||
|
||||
knownNamespaces[name] = result
|
||||
|
||||
proc getLoggerForNamespace*(
|
||||
namespace: string,
|
||||
level = lvlInfo,
|
||||
msgPrefix: Option[string] = none[string]()
|
||||
): LoggingNamespace {.raises: [].} =
|
||||
## Get a LogginNamesapce for the given namespace. The first time this is
|
||||
## called for a given name space a new logger will be created. In that case,
|
||||
## the optional `level` and `msgPrefix` will be used to configure the logger.
|
||||
## In all other cases, these paratmers are ignored and the existing namespace
|
||||
## instance is returned
|
||||
method initThreadCopy*(app: LogAppender): LogAppender {.base, gcsafe.} =
|
||||
raise newException(CatchableError, "missing concrete implementation")
|
||||
|
||||
if knownNamespaces.hasKey(namespace):
|
||||
try: return knownNamespaces[namespace]
|
||||
except KeyError:
|
||||
try: error "namespaced_logging: Impossible error. " &
|
||||
"knownNamespaces contains " & namespace & " but raised a KeyError " &
|
||||
"trying to access it."
|
||||
except: discard
|
||||
|
||||
method initThreadCopy*(cla: ConsoleLogAppender): LogAppender {.gcsafe.} =
|
||||
result = ConsoleLogAppender(
|
||||
namespace: cla.namespace,
|
||||
threshold: cla.threshold,
|
||||
formatter: cla.formatter,
|
||||
useStderr: cla.useStdErr)
|
||||
|
||||
|
||||
#[
|
||||
method initThreadCopy*(fla: FileLogAppender): LogAppender {.gcsafe.} =
|
||||
result = FileLogAppender(
|
||||
namespace: fla.namespace,
|
||||
threshold: fla.threshold,
|
||||
formatter: fla.formatter,
|
||||
file: fla.file)
|
||||
]#
|
||||
|
||||
|
||||
func initLogger(svc: LogService, cfg: LoggerConfig): Logger =
|
||||
result = Logger(name: cfg.name, threshold: cfg.threshold, svc: svc)
|
||||
|
||||
|
||||
proc copyAppenders[T](s: seq[T]): seq[T] {.gcsafe.} =
|
||||
for app in s:
|
||||
result.add(initThreadCopy(app))
|
||||
|
||||
|
||||
proc reloadThreadState*(ls: LogService) {.gcsafe.} =
|
||||
## Refresh this thread's copy of the log service configuration. Note that
|
||||
## this currently loses any loggers defined on this thread since it was last
|
||||
## reloaded.
|
||||
acquire(ls.lock)
|
||||
# TODO: push loggers defined on this thread to the shared state?
|
||||
threadState.cfg = ls.cfg
|
||||
threadState.cfg.appenders = copyAppenders(ls.cfg.appenders)
|
||||
release(ls.lock)
|
||||
|
||||
let loggers = threadState.cfg.loggers --> map(initLogger(ls, it))
|
||||
threadState.loggers = newTable(loggers --> map((it.name, it)))
|
||||
|
||||
|
||||
proc getThreadState(ls: LogService): ThreadState =
|
||||
if threadState.loggers.isNil: reloadThreadState(ls)
|
||||
return threadState
|
||||
|
||||
|
||||
func fmtLevel(lvl: Level): string {.gcsafe.} =
|
||||
case lvl
|
||||
of lvlDebug: return "DEBUG"
|
||||
of lvlInfo: return "INFO"
|
||||
of lvlNotice: return "NOTICE"
|
||||
of lvlWarn: return "WARN"
|
||||
of lvlError: return "ERROR"
|
||||
of lvlFatal: return "FATAL"
|
||||
else: return "UNKNOWN"
|
||||
|
||||
|
||||
func `%`*(msg: LogMessage): JsonNode =
|
||||
result = %*{
|
||||
"scope": msg.scope,
|
||||
"level": fmtLevel(msg.level),
|
||||
"msg": msg.message,
|
||||
"ts": msg.timestamp.formatIso8601
|
||||
}
|
||||
|
||||
if msg.error.isSome:
|
||||
result["err"] = %($msg.error.get.name & ": " & msg.error.get.msg)
|
||||
result["stacktrace"] = %($msg.error.get.trace)
|
||||
|
||||
if msg.additionalData.kind == JObject:
|
||||
for (k, v) in pairs(msg.additionalData):
|
||||
if not result.hasKey(k): result[k] = v
|
||||
|
||||
|
||||
proc initLogService*(rootLevel = lvlAll): LogService =
|
||||
result = cast[LogService](allocShared0(sizeof(LogServiceObj)))
|
||||
result.cfg.rootLevel = rootLevel
|
||||
|
||||
|
||||
proc setRootLevel*(ls: LogService, lvl: Level) =
|
||||
ls.cfg.rootLevel = lvl
|
||||
|
||||
|
||||
func formatJsonStructuredLog*(msg: LogMessage): string {.gcsafe.} = return $(%msg)
|
||||
|
||||
|
||||
func initConsoleLogAppender*(
|
||||
namespace = "",
|
||||
threshold = lvlInfo,
|
||||
formatter = formatJsonStructuredLog,
|
||||
useStderr = false): ConsoleLogAppender {.gcsafe.} =
|
||||
result = ConsoleLogAppender(
|
||||
namespace: namespace,
|
||||
threshold: threshold,
|
||||
formatter: formatter,
|
||||
useStderr: useStdErr)
|
||||
|
||||
|
||||
method appendLogMessage*(appender: LogAppender, msg: LogMessage): void {.base, gcsafe.} =
|
||||
raise newException(CatchableError, "missing concrete implementation")
|
||||
|
||||
|
||||
method appendLogMessage*(cla: ConsoleLogAppender, msg: LogMessage): void {.gcsafe.} =
|
||||
if msg.level < cla.threshold: return
|
||||
|
||||
let strMsg = formatJsonStructuredLog(msg)
|
||||
if cla.useStderr:
|
||||
stderr.writeLine(strMsg)
|
||||
stderr.flushFile()
|
||||
else:
|
||||
if msgPrefix.isSome:
|
||||
return initLoggingNamespace(namespace, level, msgPrefix.get)
|
||||
else:
|
||||
return initLoggingNamespace(namespace, level, namespace & ": ")
|
||||
stdout.writeLine(strMsg)
|
||||
stdout.flushFile()
|
||||
|
||||
proc setLevelForNamespace*(namespace: string, lvl: Level, recursive = false) {.raises: [] .} =
|
||||
if recursive:
|
||||
for k, v in knownNamespaces.pairs:
|
||||
if k.startsWith(namespace):
|
||||
v.level = lvl
|
||||
else: getLoggerForNamespace(namespace).level = lvl
|
||||
|
||||
proc name*(ns: LoggingNamespace): string = ns.name
|
||||
proc log*(ns: LoggingNamespace, level: Level, args: varargs[string, `$`]) {.raises: [] .} =
|
||||
try:
|
||||
if level >= ns.level:
|
||||
if not ns.msgPrefix.isEmptyOrWhitespace:
|
||||
log(level, args.mapIt(ns.msgPrefix & it))
|
||||
else: log(level, args)
|
||||
except: discard
|
||||
proc getLogger*(
|
||||
ls: LogService,
|
||||
name: string,
|
||||
threshold = none[Level]()): Logger {.gcsafe.} =
|
||||
|
||||
proc debug*(ns: LoggingNamespace, args: varargs[string, `$`]) = log(ns, lvlDebug, args)
|
||||
proc info*(ns: LoggingNamespace, args: varargs[string, `$`]) = log(ns, lvlInfo, args)
|
||||
proc notice*(ns: LoggingNamespace, args: varargs[string, `$`]) = log(ns, lvlNotice, args)
|
||||
proc warn*(ns: LoggingNamespace, args: varargs[string, `$`]) = log(ns, lvlWarn, args)
|
||||
proc error*(ns: LoggingNamespace, args: varargs[string, `$`]) = log(ns, lvlError, args)
|
||||
proc fatal*(ns: LoggingNamespace, args: varargs[string, `$`]) = log(ns, lvlFatal, args)
|
||||
let ts = getThreadState(ls)
|
||||
if not ts.loggers.contains(name):
|
||||
ts.loggers[name] = Logger(name: name, threshold: threshold, svc: ls)
|
||||
return ts.loggers[name]
|
||||
|
||||
|
||||
proc getLogger*(
|
||||
ls: Option[LogService],
|
||||
name: string,
|
||||
threshold = none[Level]()): Option[Logger] {.gcsafe.} =
|
||||
|
||||
if ls.isNone: return none[Logger]()
|
||||
else: return some(getLogger(ls.get, name, threshold))
|
||||
|
||||
|
||||
proc addAppender*(ls: LogService, appender: LogAppender) {.gcsafe.} =
|
||||
acquire(ls.lock)
|
||||
ls.cfg.appenders.add(appender)
|
||||
release(ls.lock)
|
||||
|
||||
|
||||
func `<`(a, b: LoggerConfig): bool = a.name < b.name
|
||||
|
||||
|
||||
func getEffectiveLevel(ts: ThreadState, name: string): Level {.gcsafe.} =
|
||||
result = ts.cfg.rootLevel
|
||||
|
||||
var namespaces = toSeq(values(ts.loggers))
|
||||
namespaces = sorted(
|
||||
namespaces --> filter(name.startsWith(it.name)),
|
||||
SortOrder.Descending)
|
||||
|
||||
for n in namespaces:
|
||||
if n.threshold.isSome:
|
||||
result = n.threshold.get
|
||||
|
||||
proc doLog(logger: Logger, msg: LogMessage): void {.gcsafe.} =
|
||||
let ts = getThreadState(logger.svc)
|
||||
let threshold =
|
||||
if logger.threshold.isSome: logger.threshold.get
|
||||
else: getEffectiveLevel(ts, logger.name)
|
||||
|
||||
if msg.level < threshold: return
|
||||
|
||||
for app in ts.cfg.appenders:
|
||||
if logger.name.startsWith(app.namespace):
|
||||
appendLogMessage(app, msg)
|
||||
|
||||
|
||||
proc log*(l: Logger, lvl: Level, msg: string) {.gcsafe.} =
|
||||
l.doLog(LogMessage(
|
||||
scope: l.name,
|
||||
level: lvl,
|
||||
error: none[ref Exception](),
|
||||
timestamp: now(),
|
||||
message: msg,
|
||||
additionalData: newJNull()))
|
||||
|
||||
|
||||
proc log*(
|
||||
l: Logger,
|
||||
lvl: Level,
|
||||
error: ref Exception,
|
||||
msg: string ) {.gcsafe.} =
|
||||
l.doLog(LogMessage(
|
||||
scope: l.name,
|
||||
level: lvl,
|
||||
error: some(error),
|
||||
timestamp: now(),
|
||||
message: msg,
|
||||
additionalData: newJNull()))
|
||||
|
||||
proc log*(l: Logger, lvl: Level, msg: JsonNode) {.gcsafe.} =
|
||||
l.doLog(LogMessage(
|
||||
scope: l.name,
|
||||
level: lvl,
|
||||
error: none[ref Exception](),
|
||||
timestamp: now(),
|
||||
message:
|
||||
if msg.hasKey("msg"): msg["msg"].getStr
|
||||
else: "",
|
||||
additionalData: msg))
|
||||
|
||||
|
||||
proc log*(l: Option[Logger], lvl: Level, msg: string) {.gcsafe.} =
|
||||
if l.isSome: log(l.get, lvl, msg)
|
||||
|
||||
proc log*(l: Option[Logger], lvl: Level, msg: JsonNode) {.gcsafe.} =
|
||||
if l.isSome: log(l.get, lvl, msg)
|
||||
|
||||
proc log*(l: Option[Logger], lvl: Level, error: ref Exception, msg: string) {.gcsafe.} =
|
||||
if l.isSome: log(l.get, lvl, error, msg)
|
||||
|
||||
template debug*[T](l: Logger, msg: T) = log(l, lvlDebug, msg)
|
||||
template info*[T](l: Logger, msg: T) = log(l, lvlInfo, msg)
|
||||
template notice*[T](l: Logger, msg: T) = log(l, lvlNotice, msg)
|
||||
template warn*[T](l: Logger, msg: T) = log(l, lvlWarn, msg)
|
||||
|
||||
template error*[T](l: Logger, msg: T) = log(l, lvlError, msg)
|
||||
template error*(l: Logger, error: ref Exception, msg: string) =
|
||||
log(l, lvlError, error, msg)
|
||||
|
||||
template fatal*[T](l: Logger, msg: T) = log(l, lvlFatal, msg)
|
||||
template fatal*(l: Logger, error: ref Exception, msg: string) =
|
||||
log(l, lvlFatal, error, msg)
|
||||
|
||||
template debug*[T](l: Option[Logger], msg: T) = log(l, lvlDebug, msg)
|
||||
template info*[T](l: Option[Logger], msg: T) = log(l, lvlInfo, msg)
|
||||
template notice*[T](l: Option[Logger], msg: T) = log(l, lvlNotice, msg)
|
||||
template warn*[T](l: Option[Logger], msg: T) = log(l, lvlWarn, msg)
|
||||
|
||||
template error*[T](l: Option[Logger], msg: T) = log(l, lvlError, msg)
|
||||
template error*(l: Option[Logger], error: ref Exception, msg: string) =
|
||||
log(l, lvlError, error, msg)
|
||||
|
||||
template fatal*[T](l: Option[Logger], msg: T) = log(l, lvlFatal, msg)
|
||||
template fatal*(l: Option[Logger], error: ref Exception, msg: string) =
|
||||
log(l, lvlFatal, error, msg)
|
||||
|
Reference in New Issue
Block a user