From 3178c509360b7ddf552a81b3528f9b1e307a1187 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Fri, 11 Jul 2025 17:38:04 -0500 Subject: [PATCH] Remove autoconfigured (didn't work as implemented). --- .gitignore | 1 + README.md | 128 +------ namespaced_logging.nimble | 5 +- src/namespaced_logging.nim | 400 ++++++++-------------- src/namespaced_logging/autoconfigured.nim | 220 ------------ 5 files changed, 160 insertions(+), 594 deletions(-) delete mode 100644 src/namespaced_logging/autoconfigured.nim diff --git a/.gitignore b/.gitignore index dab856b..4624793 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ nimcache/ tests/tests src/namespaced_logging.out src/namespaced_logging/autoconfigured +.worktrees diff --git a/README.md b/README.md index 6faefa8..9e7d331 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ four main motivating features: - Safe and straightforward to use in multi-threaded applications. - Native support for structured logging. - Simple, autoconfigured usage pattern reminiscent of the - [std/logging][std-logging] interface. + [std/logging][std-logging] interface (*not yet implemented*) ## Getting Started @@ -21,38 +21,6 @@ nimble install https://github.com/jdbernard/nim-namespaced-logging ## Usage Patterns -### Simple, Autoconfigured Setup -```nim -import namespaced_logging/autoconfigured - -# Zero configuration of the LogService required, appender/logger configuration -# is immediately available -addLogAppender(initConsoleLogAppender()) -info("Application started") - -# Set global threshold -setRootLoggingThreshold(lvlWarn) - -# Namespaced loggers, thresholds, and appenders supported -addLogAppender(initFileLogAppender( - filePath = "/var/log/app_db.log", - formatter = formatJsonStructuredLog, # provided in namespaced_logging - namespace = "app/db", - threshold = lvlInfo)) - -# in DB code -let dbLogger = getLogger("app/db/queryplanner") -dbLogger.debug("Beginning query plan...") - -# native support for structured logs (import std/json) -dbLogger.debug(%*{ - "method": "parseParams", - "message": "unrecognized param type", - "invalidType": $params[idx].type, - "metadata": %(params.meta) -} ) -``` - ### Manual Configuration ```nim import namespaced_logging @@ -72,34 +40,6 @@ let apiLogger = localLogSvc.getLogger("api") let dbLogger = localLogSvc.getLogger("db") ``` -### Autoconfigured Multithreaded Application -```nim -import namespaced_logging/autoconfigured -import mummy, mummy/routers - -# Main thread setup -addLogAppender(initConsoleLogAppender()) - -proc createApiRouter*(apiCtx: ProbatemApiContext): Router = - # This will run on a separate thread, but the thread creation is managed by - # mummy, not us. Log functions still operate correctly and respect the - # configuration setup on the main thread - let logger = getLogger("api") - logger.trace(%*{ "method_entered": "createApiRouter" }) - - # API route setup... - - logger.debug(%*{ "method": "createApiRouter", "routes": numRoutes }) - - -let server = newServer(createApiRouter(), workerThreads = 4) -ctx.server.serve(Port(8080)) -info("Serving MyApp v1.0.0 on port 8080") - -setThreshold("api", lvlTrace) # will be picked up by loggers on worker threads -``` - - ### Manual Multithreaded Application ```nim import namespaced_logging @@ -136,43 +76,6 @@ proc configureLogging(localLogSvc: ThreadLocalLogService, verbose: bool) = # Changes automatically propagate to all threads ``` -### Autoconfigured Logging in Library Code, Falling Back to `std/logging` - -One of the primary uses-cases for the autoconfigured option is for use in -libraries or other packaged code where the main application may not be using -or even aware of namespaced\_logging, especially when paired with the -[*StdLoggingAppender*][#StandingLoggingAppender], which can be configured to -fallback to std/logging when no appenders have been configured for -namespaced\_logging. - -```nim -import namespaced_logging/autoconfigured - -# Add a StdLoggingAppender to forward logs to std/logging -addLogAppender(initStdLoggingAppender(fallbackOnly = true)) - -# will be forwarded to std/logging.debug -debug("log from library code") - -addLogAppender(initConsoleLogAppender()) - -# will no longer be forwarded to std/logging.debug -debug("log from library code") -``` - -### Providing A Custom Configuration to Replace Autoconfigured Service - -```nim -import namespace_logging - -var ls = initLogService() -ls.addAppender(initConsoleLogAppender()) - -useForAutoconfiguredLogging(ls) - -# from this point on any autoconfigured LogService or Loggers will use the -# configuration defined by ls -``` ## Loggers and Appenders The logging system is composed of two main components: loggers and appenders. @@ -320,26 +223,17 @@ behave more intuitively in a multi-threaded environment than true in environments where the logging setup code may be separated from the thread-management code (in an HTTP server, for example). -As described in the [Getting Started](#getting-started) section, you can use -the `namespaced_logging/autoconfigured` import to use a simplified interface -that more closely matches the contract of [std/logging][std-logging]. In this -case all thread and state management is done for you. The only limitation is -that you cannot create multiple global *LogService* instances. In practice this -is an uncommon need. +The *LogService* object is the main entry point for the logging system and +should be initialized on the main thread. The *LogService* contains a reference +to the "source of truth" for logging configuration and is safe to be shared +between all threads. -If you do need or want the flexibility to manage the state yourself, import -`namespaced_logging` directly. In this case, the thread which initialized -*LogService* must also be the longest-living thread that uses that *LogService* -instance. If the initializing thread terminates or the *LogService* object in -that thread goes out of scope while other threads are still running and using -the *LogService*, the global state may be harvested by the garbage collector, -leading to use-after-free errors when other threads attempt to log (likely -causing segfaults). - -When managing the state yourself, the *LogService* object is the main entry -point for the logging system and should be initialized on the main thread. The -*LogService* contains a reference to the "source of truth" for logging -configuration and is safe to be shared between all threads. +The thread which initializes a *LogService* must also be the longest-living +thread that uses that *LogService* instance. If the initializing thread +terminates or the *LogService* object in that thread goes out of scope while +other threads are still running and using the *LogService*, the global state +may be harvested by the garbage collector, leading to use-after-free errors +when other threads attempt to log (likely causing segfaults). Individual threads should use the *threadLocalRef* proc to obtain a *ThreadLocalLogService* reference that can be used to create *Logger* objects. diff --git a/namespaced_logging.nimble b/namespaced_logging.nimble index 293fa07..dd4e4dd 100644 --- a/namespaced_logging.nimble +++ b/namespaced_logging.nimble @@ -1,6 +1,6 @@ # Package -version = "2.0.1" +version = "2.0.2" author = "Jonathan Bernard" description = "Wrapper around std/logging to provide namespaced logging." license = "MIT" @@ -15,6 +15,5 @@ requires @["nim >= 2.2.0", "zero_functional"] requires "timeutils" task test, "Run unittests for the package.": - exec "nimble c src/namespaced_logging.nim src/namespaced_logging/autoconfigured.nim" + exec "nimble c src/namespaced_logging.nim" exec "src/namespaced_logging.out" - exec "src/namespaced_logging/autoconfigured" diff --git a/src/namespaced_logging.nim b/src/namespaced_logging.nim index 3f356fd..757e517 100644 --- a/src/namespaced_logging.nim +++ b/src/namespaced_logging.nim @@ -1,5 +1,5 @@ -import std/[algorithm, atomics, json, locks, options, os, paths, sequtils, - strutils, tables, times] +import std/[algorithm, atomics, exitprocs, json, locks, options, os, paths, + sequtils, strutils, tables, times] import timeutils import std/logging as stdlog @@ -16,19 +16,9 @@ type thresholds: TableRef[string, Level] rootLevel: Atomic[Level] - console: ThreadedConsoleLoggingState - file: ThreadedFileLoggingState - errorHandler: ErrorHandlerFunc errorHandlerLock: Lock - takeOverGls: Option[GlobalLogService] - # Used to direct ThreadLocalLogServices that they should switch to a new - # GlobalLogService (logging root). This is used primarily in the context of - # autoconfigured logging where we want to be able to reconfigure the GLS - # used for autologging, have existing ThreadLocalLogServices switch over - # to the newly provided GLS, and let the old GLS get garbage-collected - GlobalLogService = ref GlobalLogServiceObj @@ -94,18 +84,11 @@ type absPath: Path - ThreadedConsoleLoggingState = object + LogWriterThreadState[M] = object initialized: Atomic[bool] shutdown: Atomic[bool] - chan: Channel[ConsoleMessage] - writerThread: Thread[GlobalLogService] - - - ThreadedFileLoggingState = object - initialized: Atomic[bool] - shutdown: Atomic[bool] - chan: Channel[FileMessage] - writerThread: Thread[GlobalLogService] + chan: Channel[M] + writerThread: Thread[void] ConsoleLogAppender* = ref object of LogAppender @@ -146,6 +129,12 @@ type const UninitializedConfigVersion = low(int) let JNULL = newJNull() +var consoleLogging {.global.}: LogWriterThreadState[ConsoleMessage] +var fileLogging {.global.}: LogWriterThreadState[FileMessage] +var loggingThreadInitLock {.global.}: Lock + +initLock(loggingThreadInitLock) + proc initLogMessage*( scope: string, @@ -200,40 +189,39 @@ proc defaultErrorHandlerFunc*( stderr.flushFile() except Exception: discard # we tried... -proc shutdownThreadedConsoleLogging(gls: var GlobalLogServiceObj) = - if not gls.console.initialized.load(): return +proc shutdownThreadedConsoleLogging() = + if not consoleLogging.initialized.load(): return - gls.console.shutdown.store(true) # signal shutdown + withLock loggingThreadInitLock: + consoleLogging.shutdown.store(true) # signal shutdown - # Send sentinel values to wake up the writer thread - try: gls.console.chan.send(ConsoleMessage(message: "", useStderr: false)) - except Exception: discard + # Send sentinel values to wake up the writer thread + try: consoleLogging.chan.send(ConsoleMessage(message: "", useStderr: false)) + except Exception: discard - joinThread(gls.console.writerThread) - gls.console.chan.close() - gls.console.initialized.store(false) + joinThread(consoleLogging.writerThread) + consoleLogging.chan.close() + consoleLogging.initialized.store(false) -proc shutdownThreadedFileLogging(gls: var GlobalLogServiceObj) = - if not gls.file.initialized.load(): return +proc shutdownThreadedFileLogging() = + if not fileLogging.initialized.load(): return - gls.file.shutdown.store(true) # signal shutdown + fileLogging.shutdown.store(true) # signal shutdown - try: gls.file.chan.send(FileMessage(message: "", absPath: Path("/"))) - except Exception: discard + withLock loggingThreadInitLock: + try: fileLogging.chan.send(FileMessage(message: "", absPath: Path("/"))) + except Exception: discard - joinThread(gls.file.writerThread) - gls.file.chan.close() - gls.file.initialized.store(false) + joinThread(fileLogging.writerThread) + fileLogging.chan.close() + fileLogging.initialized.store(false) proc `=destroy`*(gls: var GlobalLogServiceObj) = # only one thread should cleanup if not gls.initialized.exchange(false): return - gls.shutdownThreadedConsoleLogging() - gls.shutdownThreadedFileLogging() - try: deinitLock(gls.lock) except Exception: discard @@ -253,12 +241,6 @@ proc ensureFreshness*(ls: var LogService) = if ls.configVersion == ls.global.configVersion.load(): return - if ls.global.takeOverGls.isSome: - let newGls = ls.global.takeOverGls.get - assert not newGls.isNil - assert newGls.initialized.load - ls.global = newGls - withLock ls.global.lock: ls.configVersion = ls.global.configVersion.load @@ -434,6 +416,15 @@ proc getLogger*( result = Logger(scope: scope, threadSvc: ls) +proc getLogger*( + ls: Option[ThreadLocalLogService], + scope: string, + lvl: Option[Level] = none[Level]()): Option[Logger] {.gcsafe.} = + return + if ls.isSome: some(getLogger(ls.get, scope, lvl)) + else: none[Logger]() + + proc addAppender*(ls: var LogService, appender: LogAppender) {.gcsafe.} = ## Add a log appender to the global log service and refresh the local thread ## state. The updated global state will trigger other threads to refresh @@ -553,6 +544,102 @@ template fatal*[L: Logger or Option[Logger], M](l: L, error: ref Exception, msg: log(l, lvlFatal, error, msg) +# ----------------------------------------------------------------------------- +# Writer Thread Implementations +# ----------------------------------------------------------------------------- + +proc consoleWriterLoop() {.thread.} = + while not consoleLogging.shutdown.load(): + var didSomething = false + + let (hasData, msg) = consoleLogging.chan.tryRecv() + if hasData and msg.message.len > 0: # Skip empty sentinel messages + try: + let output = + if msg.useStderr: stderr + else: stdout + output.write(msg.message) + output.flushFile() + didSomething = true + except IOError: + discard + + # Small delay if no work to prevent busy waiting + if not didSomething: sleep(100) + + +proc fileWriterLoop() {.thread.} = + const bufLen = 128 + var msgsByPath = newTable[Path, seq[FileMessage]]() + + while not fileLogging.shutdown.load(): + var didSomething = false + + var msgBuf: array[bufLen, FileMessage] + var recvIdx = 0 + var writeIdx = 0 + var dataAvailable = true + + while dataAvailable and recvIdx < bufLen: + # Fill our message buffer if we can + (dataAvailable, msgBuf[recvIdx]) = fileLogging.chan.tryRecv() + if dataAvailable: inc recvIdx + + # Organize messages by destination file + msgsByPath.clear() + while writeIdx < recvIdx: + let msg = msgBuf[writeIdx] + inc writeIdx + + if msg.message.len > 0: # skip empty sentinel messages + if not msgsByPath.contains(msg.absPath): msgsByPath[msg.absPath] = @[] + msgsByPath[msg.absPath].add(msg) + didSomething = true + + # Write all messages in file order to optimize file open/flush/close + for path, msgs in pairs(msgsByPath): + var f: File + + if not open(f, $path, fmAppend): + # TODO: can we do better than silently failing here? + continue + + for m in msgs: + try: writeLine(f, m.message) + except Exception: discard + flushFile(f) + close(f) + + # Wait a bit if we had no work to prevent busy waiting + if not didSomething: sleep(100) + + +proc initThreadedConsoleLogging() = + if consoleLogging.initialized.load: return + + withLock loggingThreadInitLock: + if consoleLogging.initialized.load: return + consoleLogging.chan.open() + consoleLogging.shutdown.store(false) + + # Create writer thread with reference to the service + createThread(consoleLogging.writerThread, consoleWriterLoop) + consoleLogging.initialized.store(true) + + +proc initThreadedFileLogging() = + if fileLogging.initialized.load: return + + withLock loggingThreadInitLock: + if fileLogging.initialized.load: return + fileLogging.chan.open() + fileLogging.shutdown.store(false) + + # Create writer thread with reference to the service + createThread(fileLogging.writerThread, fileWriterLoop) + fileLogging.initialized.store(true) + + # ----------------------------------------------------------------------------- # CustomLogAppender Implementation # ----------------------------------------------------------------------------- @@ -613,41 +700,6 @@ proc initConsoleLogAppender*( useStderr: useStderr) -proc consoleWriterLoop(gls: GlobalLogService) {.thread.} = - while not gls.console.shutdown.load(): - var didSomething = false - - let (hasData, msg) = gls.console.chan.tryRecv() - if hasData and msg.message.len > 0: # Skip empty sentinel messages - try: - let output = - if msg.useStderr: stderr - else: stdout - output.write(msg.message) - output.flushFile() - didSomething = true - except IOError: - discard - - # Small delay if no work to prevent busy waiting - if not didSomething: sleep(100) - - -proc initThreadedConsoleLogging(gls: GlobalLogService) = - if gls.console.initialized.load() or # don't double-init - not gls.initialized.load(): # don't init if the gls is shutting down - return - - withLock gls.lock: - if gls.console.initialized.load(): return - gls.console.chan.open() - gls.console.shutdown.store(false) - - # Create writer thread with reference to the service - createThread(gls.console.writerThread, consoleWriterLoop, gls) - gls.console.initialized.store(true) - - method clone*(cla: ConsoleLogAppender): LogAppender {.gcsafe.} = result = ConsoleLogAppender( namespace: cla.namespace, @@ -661,11 +713,11 @@ proc appendLogMessageMultiThreaded( ls: ref LogService, msg: LogMessage) {.gcsafe.} = - if not ls.global.console.initialized.load(): - ls.global.initThreadedConsoleLogging() + if not consoleLogging.initialized.load(): + initThreadedConsoleLogging() try: - ls.global.console.chan.send(ConsoleMessage( + consoleLogging.chan.send(ConsoleMessage( message: cla.formatter(msg), useStderr: cla.useStderr)) except Exception: @@ -732,67 +784,6 @@ proc initFileLogAppender*( formatter: formatter, absPath: absolutePath(Path(filePath))) - # TODO: initialize global state for the file log writer - - -proc fileWriterLoop(gls: GlobalLogService) {.thread.} = - const bufLen = 128 - var msgsByPath = newTable[Path, seq[FileMessage]]() - - while not gls.file.shutdown.load(): - var didSomething = false - - var msgBuf: array[bufLen, FileMessage] - var recvIdx = 0 - var writeIdx = 0 - var dataAvailable = true - - while dataAvailable and recvIdx < bufLen: - # Fill our message buffer if we can - (dataAvailable, msgBuf[recvIdx]) = gls.file.chan.tryRecv() - if dataAvailable: inc recvIdx - - # Organize messages by destination file - msgsByPath.clear() - while writeIdx < recvIdx: - let msg = msgBuf[writeIdx] - inc writeIdx - - if msg.message.len > 0: # skip empty sentinel messages - if not msgsByPath.contains(msg.absPath): msgsByPath[msg.absPath] = @[] - msgsByPath[msg.absPath].add(msg) - didSomething = true - - # Write all messages in file order to optimize file open/flush/close - for path, msgs in pairs(msgsByPath): - var f: File - - if not open(f, $path, fmAppend): - # TODO: can we do better than silently failing here? - continue - - for m in msgs: - try: writeLine(f, m.message) - except Exception: discard - flushFile(f) - close(f) - - # Wait a bit if we had no work to prevent busy waiting - if not didSomething: sleep(100) - - -proc initThreadedFileLogging(gls: GlobalLogService) = - if gls.file.initialized.load(): return - - withLock gls.lock: - if gls.file.initialized.load(): return - gls.file.chan.open() - gls.file.shutdown.store(false) - - # Create writer thread with reference to the service - createThread(gls.file.writerThread, fileWriterLoop, gls) - gls.file.initialized.store(true) - method clone*(fla: FileLogAppender): LogAppender {.gcsafe.} = result = FileLogAppender( @@ -807,11 +798,11 @@ proc appendLogMessageMultiThreaded( ls: ref LogService, msg: LogMessage) {.gcsafe.} = - if not ls.global.file.initialized.load(): - ls.global.initThreadedFileLogging() + if not fileLogging.initialized.load(): + initThreadedFileLogging() try: - ls.global.file.chan.send(FileMessage( + fileLogging.chan.send(FileMessage( message: fla.formatter(msg), absPath: fla.absPath)) except Exception: discard @@ -890,113 +881,14 @@ method appendLogMessage*( # ----------------------------------------------------------------------------- -# Autoconfiguration Implementation +# Cleanup # ----------------------------------------------------------------------------- -var autoGls = GlobalLogService() - # we create the global reference so that it is maintained by the thread that - # first imported this module, but leave it uninitialized until - # initAutoconfiguredLogService is actually called (when - # namespaced_logging/autoconfigured is imported) +addExitProc(proc() = + shutdownThreadedConsoleLogging() + shutdownThreadedFileLogging() +) -var autoTlls {.threadvar.}: ThreadLocalLogService -var autoLogger {.threadvar.}: Logger - - -proc initAutoconfiguredLogService*() = - ## This exists primarily for namespaced_logging/autoconfigured to call as - ## part of its setup process. This function needs to live here and be - ## exported for the autoconfigured module's visibility as many of the internal - ## fields required to properly manage the autoconfigured LogService are not - ## exported, to avoid confusion and prevent misuse of the library (from a - ## thread-safety POV). - - assert not autoGls.isNil - - let oldGls = autoGls - autoGls = initGlobalLogService() - - if oldGls.initialized.load: - # If we already have an auto-configured GLS, let's log to the existing GLS - # that we're replacing it. - - withLock oldGls.lock: - if autoTlls.isNil: - # If we somehow have an auto-configured GLS but never instantiated a - # thread-local LogService, let's do so temporarily. - autoTlls = new(LogService) - autoTlls.global = oldGls - ensureFreshness(autoTlls) - - warn( - getLogger(autoTlls, "namespaced_logging/autoconfigured"), - "initializing a new auto-configured logging service, replacing this one") - - oldGls.takeOverGls = some(autoGls) - oldGls.configVersion.atomicInc - - autoTlls = threadLocalRef(initLogService(autoGls)) - autoLogger = autoTlls.getLogger("") - - -proc getAutoconfiguredLogService*(): ThreadLocalLogService = - if autoTlls.isNil: - if not autoGls.initialized.load(): - initAutoconfiguredLogService() - assert autoGls.initialized.load() - - autoTlls = threadLocalRef(initLogService(autoGls)) - - return autoTlls - - -proc getAutoconfiguredLogger*(): Logger = - if autoLogger.isNil: - autoLogger = getLogger(getAutoconfiguredLogService(), "") - - return autoLogger - - -proc useForAutoconfiguredLogging*(ls: LogService) = - # Reconfigure the autoconfigured logging behavior to use the given LogService - # configuration instead of the existing autoconfigured configuration. This is - # useful in applications that want to control the behavior of third-party - # libraries or code that use namespaced_logging/autoconfigured. - # - # Libraries and other non-application code are suggested to use - # namespaced_logging/autoconfigured. The autoconfigured log service has no - # appenders when it is initialized which means that applications which are - # unaware of namespaced_logging are unaffected and no logs are generated. - - if ls.global == autoGls: - # As of Nim 2 `==` on `ref`s performs a referential equality check by - # default, and we don't overload `==`. Referential equality is what we're - # after here. If the reference in ls already points to the same place as - # autoGls, we have nothing to do - return - - if autoGls.initialized.load: - # if there is an existing autoGls, let's leave instructions for loggers and - # LogService instances to move to the newly provided GLS before we change - # our autoGls reference. - withLock autoGls.lock: - autoGls.takeOverGls = some(ls.global) - autoGls.configVersion.atomicInc - - autoGls = ls.global - - -proc useForAutoconfiguredLogging*(tlls: ThreadLocalLogService) = - useForAutoconfiguredLogging(tlls[]) - - -proc resetAutoconfiguredLogging*() = - ## Reset the auto-configured logging service. In general it is suggested to - # define a new LogService, configure it, and pass it to - # *useForAutoconfiguredLogging* instead. in a way that disconnects it - #from - autoGls = GlobalLogService() - initAutoconfiguredLogService() # ----------------------------------------------------------------------------- # Tests diff --git a/src/namespaced_logging/autoconfigured.nim b/src/namespaced_logging/autoconfigured.nim deleted file mode 100644 index 54286b9..0000000 --- a/src/namespaced_logging/autoconfigured.nim +++ /dev/null @@ -1,220 +0,0 @@ -import std/[json, options, strutils] -from logging import Level -import ../namespaced_logging - -export - # Types - Level, - Logger, - LogAppender, - LogMessage, - ConsoleLogAppender, - CustomLogAppender, - CustomLogAppenderFunc, - FileLogAppender, - - # Procs/Funcs - `%`, - initConsoleLogAppender, - initCustomLogAppender, - initFileLogAppender, - formatJsonStructuredLog, - useForAutoconfiguredLogging - - -proc setRootLoggingThreshold*(lvl: Level) = - setRootThreshold(getAutoconfiguredLogService(), lvl) - - -proc setLoggingThreshold*(scope: string, lvl: Level) = - setThreshold(getAutoconfiguredLogService(), scope, lvl) - - -proc addLogAppender*(appender: LogAppender) = - addAppender(getAutoconfiguredLogService(), appender) - - -proc clearLogAppenders*() = - clearAppenders(getAutoconfiguredLogService()) - - -proc getLogger*(scope: string, lvl: Option[Level] = none[Level]()): Logger = - getLogger(getAutoconfiguredLogService(), scope, lvl) - - -template log*(lm: LogMessage) = log(getAutoconfiguredLogger(), lm) - -template log*(lvl: Level, msg: untyped) = log(getAutoconfiguredLogger(), lvl, msg) - -template log*[T: ref Exception](lvl: Level, error: T, msg: untyped) = - log(getAutoconfiguredLogger(), lvl, error, msg) - -template debug*[T](msg: T) = log(lvlDebug, msg) -template info*[T](msg: T) = log(lvlInfo, msg) -template notice*[T](msg: T) = log(lvlNotice, msg) -template warn*[T](msg: T) = log(lvlWarn, msg) -template error*[T](msg: T) = log(lvlError, msg) -template error*[T](error: ref Exception, msg: T) = log(lvlError, error, msg) -template fatal*[T](msg: T) = log(lvlFatal, msg) -template fatal*[T](error: ref Exception, msg: T) = log(lvlFatal, error, msg) - -when isMainModule: - import std/unittest - import ./testutil - - suite "Automatic Global Setup": - - setup: - clearLogAppenders() - let loggedMsgs = initLoggedMessages() - addLogAppender(initTestLogAppender(loggedMsgs)) - # note that we are not resetting the global log service reference as the - # module default behavior in setting up the global log service reference - # is what we want to test - - test "simple logging works": - info("test message") - - let lm = loggedMsgs.get() - check: - lm.len == 1 - lm[0].level == lvlInfo - lm[0].message == "test message" - lm[0].scope == "" - - suite "Autoconfigured Logging": - setup: - resetAutoconfiguredLogging() - let loggedMsgs = initLoggedMessages() - addLogAppender(initTestLogAppender(loggedMsgs)) - - test "message construction is avoided if the message is not logged": - var expensiveCallCount = 0 - proc expensiveCall(): int = - inc expensiveCallCount - return expensiveCallCount - - setRootLoggingThreshold(lvlInfo) - - debug("Expensive call (" & $expensiveCall() & ")") - info("Expensive call (" & $expensiveCall() & ")") - - let lm = loggedMsgs.get() - check: - lm.len == 1 - lm[0].message.contains("Expensive call (1)") - expensiveCallCount == 1 - - test "thread local variables are cached": - # Get the service reference multiple times - should be same instance - let svc1 = getAutoconfiguredLogService() - let svc2 = getAutoconfiguredLogService() - check svc1 == svc2 - - # Default logger should also be cached - let logger1 = getAutoconfiguredLogger() - let logger2 = getAutoconfiguredLogger() - check logger1 == logger2 - - test "logging with exceptions works": - let testException = newException(ValueError, "test error") - error(testException, "Something went wrong") - - let lm = loggedMsgs.get() - check: - lm.len == 1 - lm[0].level == lvlError - lm[0].error.isSome - lm[0].error.get.msg == "test error" - - test "all convenience methods work": - debug("debug message") - info("info message") - notice("notice message") - warn("warn message") - error("error message") - fatal("fatal message") - - let lm = loggedMsgs.get() - check: - lm.len == 6 - lm[0].level == lvlDebug - lm[1].level == lvlInfo - lm[2].level == lvlNotice - lm[3].level == lvlWarn - lm[4].level == lvlError - lm[5].level == lvlFatal - - test "message construction is avoided if the message is not logged": - var expensiveCallCount = 0 - proc expensiveCall(): int = - inc expensiveCallCount - return expensiveCallCount - - setRootLoggingThreshold(lvlInfo) - - debug("Expensive call (" & $expensiveCall() & ")") - info("Expensive call (" & $expensiveCall() & ")") - - let lm = loggedMsgs.get() - check: - lm.len == 1 - lm[0].message.contains("Expensive call (1)") - expensiveCallCount == 1 - - - suite "Global Service Management": - setup: - resetAutoconfiguredLogging() - - test "useForAutoconfiguredLogging changes global service": - - let origLogs = initLoggedMessages() - let newLogs = initLoggedMessages() - - # autoconfiged first - addLogAppender(initTestLogAppender(origLogs)) - let origLogger = getAutoconfiguredLogger() - debug("msg 1") - - # Then we setup a custom service that will take over the autoconfig - var customLogService = initLogService(lvlWarn) - customLogService.addAppender(initTestLogAppender(newLogs)) - useForAutoconfiguredLogging(customLogService) - - # Subsequent calls to debug, info, etc. should use the new config - debug("msg 2 - should be filtered") - warn("msg 3 - should appear") - - # Any Loggers that are still around should also get updates - origLogger.debug("msg 4 - should be filtered") - origLogger.error("msg 5 - should appear") - - let lmOrig = origLogs.get() - let lmNew = newLogs.get() - check: - lmOrig.len == 1 - lmOrig[0].message == "msg 1" - lmNew.len == 2 - lmNew[0].message == "msg 3 - should appear" - lmNew[1].message == "msg 5 - should appear" - - test "configuration changes affect autoconfigured logging": - let loggedMsgs = initLoggedMessages() - addLogAppender(initTestLogAppender(loggedMsgs)) - - # Initially all levels should work - debug("debug message") - - # Change root threshold - setRootLoggingThreshold(lvlInfo) - - # Debug should now be filtered - debug("should be filtered") - info("should appear") - - let lm = loggedMsgs.get() - check: - lm.len == 2 # First debug + info - lm[0].level == lvlDebug - lm[1].level == lvlInfo