11 Commits

Author SHA1 Message Date
3178c50936 Remove autoconfigured (didn't work as implemented). 2025-07-11 17:38:04 -05:00
1b598fb445 README typo/correction. 2025-07-11 09:27:08 -05:00
101ac8d869 Bump version. 2025-07-07 21:47:58 -05:00
a4464c7275 Add README examples for autoconfigured use-cases. 2025-07-07 21:09:38 -05:00
49755fa2af Add more unit tests. 2025-07-07 17:02:45 -05:00
b2d43d1c6d StdLoggingAppender - forward logs to std/logging
Primarily intended for use in libraries or other contexts where you want
to fall back to std/logging if the application is not using or hasn't
configured namespaced_logging.

By default the StdLoggingAppender only logs when no namespaced_logging
appenders are configured, but it can also be configured to always
forward log messages, even when namespaced_logging is configured.
2025-07-07 16:34:04 -05:00
c22e7edd5d Move autoconfiguration implementation into the main module.
Autoconfiguration implementation really needs access to internal fields
and data structures to work properly.

Additionally introduces the concept of GlobalLogService takeover
internally, which allows existing LogService instances to become aware
of a new GlobalLogService. This is needed for
`useForAutoconfiguredLogging` to work as one would naturally expect,
where Loggers that may have already been created (explicitly or
implicitly) by library or third-party code are kept up to date when the
application explicitly configures logging.
2025-07-07 16:29:58 -05:00
269cc81c82 Rewrite the log procs for better performance.
Previously, log message parsing and construction were performed before
we checked if we were actually going to log anything (based on our
thresholds). This change moves the logic for both the LogMessage
construction, as well as any logic in the calling context (string
concatenation etc.) after the threshold check.

This is to enable use-cases like this:

    logger.debug("Something interesting happened\p    context: " &
        expensiveCallToConstructFullContext())

and not have to pay the performance penalty of the string concatenation
in production settings when debug logging is turned off.
2025-07-07 16:21:59 -05:00
1884e07378 Make the README a little more concise. 2025-07-06 04:23:27 -05:00
2f761833bd Use message for structures message contents, not msg.
I like `msg`, but `message` is more common and likely to the more
expected name.
2025-07-06 04:12:47 -05:00
c4074007b5 Tweaks to README intro. 2025-07-06 04:12:23 -05:00
5 changed files with 576 additions and 462 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ nimcache/
tests/tests tests/tests
src/namespaced_logging.out src/namespaced_logging.out
src/namespaced_logging/autoconfigured src/namespaced_logging/autoconfigured
.worktrees

212
README.md
View File

@@ -1,63 +1,32 @@
# Namespaced Logging for Nim # Namespaced Logging for Nim
`namespaced_logging` provides a high-performance, thread-safe logging framework `namespaced_logging` is intended to be a high-performance, thread-safe logging
similar to [std/logging][std-logging] with support for namespace-scoped logging framework similar to [std/logging][std-logging] with support for
similar to [log4j][] or [logback][] for Nim. It has four main motivating namespace-scoped logging similar to [log4j][] or [logback][] for Nim. It has
features: four main motivating features:
- Hierarchical, namespaced logging - Hierarchical, namespaced logging
- Safe and straightforward to use in multi-threaded applications. - Safe and straightforward to use in multi-threaded applications.
- Native support for structured logging. - Native support for structured logging.
- Simple, autoconfigured usage pattern mirroring the [std/logging][std-logging] - Simple, autoconfigured usage pattern reminiscent of the
interface. [std/logging][std-logging] interface (*not yet implemented*)
## Getting Started ## Getting Started
Install the package from nimble: Install the package via nimble:
```bash ```bash
nimble install namespaced_logging # Not yet in official Nim packages. TODO once we've battle-tested it a little
nimble install https://github.com/jdbernard/nim-namespaced-logging
``` ```
## Usage Patterns ## 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 ### Manual Configuration
```nim ```nim
import namespaced_logging import namespaced_logging
# Manually creating a LogService. This is an independent logging root fully # Manually creating a LogService. This is an independent logging root fully
# isolated from subsequent LogServices initialized # isolated from subsequent LogServices initialized with initLogService
var ls = initLogService() var ls = initLogService()
# Configure logging # Configure logging
@@ -71,33 +40,6 @@ let apiLogger = localLogSvc.getLogger("api")
let dbLogger = localLogSvc.getLogger("db") 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)
info("Serving MyApp v1.0.0 on port 8080")
setThreshold("api", lvlTrace) # will be picked up by loggers on worker threads
```
### Manual Multithreaded Application ### Manual Multithreaded Application
```nim ```nim
import namespaced_logging import namespaced_logging
@@ -219,9 +161,29 @@ to files associated with the *FileLogAppender* configured for the current
`namespaced_logging` does not currently have built-in logic for file `namespaced_logging` does not currently have built-in logic for file
rotation, but it does play nice with external file rotation strategies. We do rotation, but it does play nice with external file rotation strategies. We do
not hold open file handles. The *FileLogAppender* attempts to batch messages not hold open file handles. The *FileLogAppender* attempts to batch messages
by destination file, opens the file with fmAppend, writes the current batch of by destination file, opens the file with mode `fmAppend`, writes the current
log messages, and then closes the file handle. Because of this, it has no batch of log messages, and then closes the file handle. Because of this, it has
problem if another process moves or truncates any of the target log files. no problem if another process moves or truncates any of the target log files.
### StdLoggingAppender
Provides a fallback to [std/logging][std-logging]-based logging. This is
primarily intended for use in libraries or other contexts where you want to
fall back to std/logging if the application is not using or hasn't configured
namespaced\_logging.
By default the *StdLoggingAppender* only logs when no namespaced\_logging
appenders are configured but it can also be configured to always forward log
messages regardless of whether namespaced\_logging has other appenders by
setting `fallbackOnly = false`.
```nim
func initStdLoggingAppender*(
fallbackOnly = true,
formatter = formatForwardedLog,
namespace = "",
threshold = lvlAll): StdLoggingAppender {.gcsafe.}
```
### CustomLogAppender ### CustomLogAppender
@@ -242,7 +204,7 @@ for the custom functionality.
*TODO: rethink this. I chose this to avoid GC-safety issues copying closures *TODO: rethink this. I chose this to avoid GC-safety issues copying closures
across threads, but maybe I don't need this separate, explicit state field.* across threads, but maybe I don't need this separate, explicit state field.*
> [!IMPORTANT] The `state` data type must support copy semantics on assignment. > [!CAUTION] The `state` data type must support copy semantics on assignment.
> It is possible to pass a `ref` to `state` and/or data structures that include > It is possible to pass a `ref` to `state` and/or data structures that include
> `ref`s, but **you must guarantee they remain valid**, either by allocating > `ref`s, but **you must guarantee they remain valid**, either by allocating
> shared memeory, or (preferably) keeping alive a reference to them that the GC > shared memeory, or (preferably) keeping alive a reference to them that the GC
@@ -261,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 true in environments where the logging setup code may be separated from the
thread-management code (in an HTTP server, for example). thread-management code (in an HTTP server, for example).
As described in the [Getting Started](#getting-started) section, you can use The *LogService* object is the main entry point for the logging system and
the `namespaced_logging/autoconfigured` import to use a simplified interface should be initialized on the main thread. The *LogService* contains a reference
that more closely matches the contract of [std/logging][std-logging]. In this to the "source of truth" for logging configuration and is safe to be shared
case all thread and state management is done for you. The only limitation is between all threads.
that you cannot create multiple global *LogService* instances. In practice this
is an uncommon need.
If you do need or want the flexibility to manage the state yourself, import The thread which initializes a *LogService* must also be the longest-living
`namespaced_logging` directly. In this case, the thread which initialized thread that uses that *LogService* instance. If the initializing thread
*LogService* must also be the longest-living thread that uses that *LogService* terminates or the *LogService* object in that thread goes out of scope while
instance. If the initializing thread terminates or the *LogService* object in other threads are still running and using the *LogService*, the global state
that thread goes out of scope while other threads are still running and using may be harvested by the garbage collector, leading to use-after-free errors
the *LogService*, the global state may be harvested by the garbage collector, when other threads attempt to log (likely causing segfaults).
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.
Individual threads should use the *threadLocalRef* proc to obtain a Individual threads should use the *threadLocalRef* proc to obtain a
*ThreadLocalLogService* reference that can be used to create *Logger* objects. *ThreadLocalLogService* reference that can be used to create *Logger* objects.
@@ -301,36 +254,11 @@ initialization context is separated from the logging setup code.
### Overview ### Overview
The namespaced logging library is built around a thread-safe architecture that The namespaced logging library attempts to balance performance, safety, and
attempts to balance performance, safety, and usability in multithreaded usability in multithreaded environments. The design centers on two key types:
environments. The design centers on two key types (*LogService* and *LogService* and *ThreadLocalLogService*.
*ThreadLocalLogService*) that work together to provide both thread-safe
configuration management and efficient logging operations.
### Core Architecture Components #### LogService (Value Type)
#### GlobalLogService (Internal)
At the heart of the system is the `GlobalLogService`, a heap-allocated object
that serves as the single source of truth for logging configuration. This
internal type is not exposed to library users but manages:
- **Shared configuration state**: Appenders, thresholds, and root logging level
- **Synchronization primitives**: Locks and atomic variables for thread
coordination
- **Background I/O threads**: Dedicated writer threads for console and file
output
- **Configuration versioning**: Atomic version numbers for efficient change
detection
The `GlobalLogService` ensures that configuration changes are safely propagated
across all threads while maintaining high performance for logging operations.
#### LogService vs ThreadLocalLogService
The library exposes two distinct types for different usage patterns:
##### LogService (Value Type)
```nim ```nim
type LogService* = object type LogService* = object
configVersion: int configVersion: int
@@ -353,25 +281,42 @@ The *LogService* object is intended to support uses cases such as:
> The *LogService* object is the object that is intended to be shared across > The *LogService* object is the object that is intended to be shared across
> threads. > threads.
##### ThreadLocalLogService (Reference Type) #### ThreadLocalLogService (Reference Type)
```nim ```nim
type ThreadLocalLogService* = ref LogService type ThreadLocalLogService* = ref LogService
``` ```
The *ThreadLocalLogService* is a reference to a thread-local copy of a *ThreadLocalLogService* is a reference to a thread-local copy of a *LogService*
*LogService* and can be obtained via *threadLocalRef*. We purposefully use and can be obtained via *threadLocalRef*. We purposefully use reference
reference semantics within the context of a thread so that *Logger* objects semantics within the context of a thread so that *Logger* objects created
created within the same thread context share the same *ThreadLocalLogService* within the same thread context share the same *ThreadLocalLogService*
reference, avoiding the need to synchronize every *Logger* individually. reference, avoiding the need to synchronize every *Logger* individually.
The *ThreadLocalLogService* is the object that users are expected to interact *ThreadLocalLogService* is the object that users are expected to interact with
with during regular operation and support both the configuration functions of during regular operation and support both the configuration functions of
*LogService* and the creation of *Logger* objects. *LogService* and the creation of *Logger* objects.
> [!CAUTION] > [!CAUTION]
> *ThreadLocalLogService* objects should **never** be shared outside the > *ThreadLocalLogService* objects should **never** be shared outside the
> context of the thread in which they were initialized. > context of the thread in which they were initialized.
#### GlobalLogService (Internal)
Under the hood *LogService* holds a reference to a *GlobalLogService*, a
heap-allocated object that serves as the single source of truth for logging
configuration. This internal type is not exposed to library users but manages:
- **Shared configuration state**: Appenders, thresholds, and root logging level
- **Synchronization primitives**: Locks and atomic variables for thread
coordination
- **Background I/O threads**: Dedicated writer threads for console and file
output
- **Configuration versioning**: Atomic version numbers for efficient change
detection
The `GlobalLogService` ensures that configuration changes are safely propagated
across all threads while maintaining high performance for logging operations.
### Thread Safety Model ### Thread Safety Model
#### Safe Cross-Thread Pattern #### Safe Cross-Thread Pattern
@@ -417,14 +362,16 @@ proc ensureFreshness*(ls: var LogService) =
# Sync state... # Sync state...
``` ```
This design ensures that: Goals/Motivation:
- **Hot path is fast**: Most logging operations skip expensive synchronization - Most logging operations skip expensive synchronization so the hot path is
- **Changes propagate automatically**: All threads see configuration updates fast.
- **Minimal lock contention**: Locks only acquired when configuration changes - Propogate changes automatically so all threads see configuration updates.
- Minimize lock contention by only acquiring when configuration changes
#### Thread-Local Caching #### Thread-Local Caching
Each thread maintains its own copy of the logging configuration: Each thread maintains its own copy of the logging configuration in
*ThreadLocalLogService*:
- **Appenders**: Thread-local copies created via `clone()` method - **Appenders**: Thread-local copies created via `clone()` method
- **Thresholds**: Complete copy of namespace-to-level mappings - **Thresholds**: Complete copy of namespace-to-level mappings
@@ -492,6 +439,7 @@ logService.setErrorHandler(silentErrorHandler)
### Best Practices ### Best Practices
#### Provide Fallbacks #### Provide Fallbacks
```nim ```nim
proc robustErrorHandler(err: ref Exception, msg: string) {.gcsafe, nimcall.} = proc robustErrorHandler(err: ref Exception, msg: string) {.gcsafe, nimcall.} =
# Primary: Send to monitoring system # Primary: Send to monitoring system

View File

@@ -1,6 +1,6 @@
# Package # Package
version = "1.1.0" version = "2.0.2"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Wrapper around std/logging to provide namespaced logging." description = "Wrapper around std/logging to provide namespaced logging."
license = "MIT" license = "MIT"
@@ -13,3 +13,7 @@ requires @["nim >= 2.2.0", "zero_functional"]
# from https://git.jdb-software.com/jdb/nim-packages # from https://git.jdb-software.com/jdb/nim-packages
requires "timeutils" requires "timeutils"
task test, "Run unittests for the package.":
exec "nimble c src/namespaced_logging.nim"
exec "src/namespaced_logging.out"

View File

@@ -1,9 +1,10 @@
import std/[algorithm, atomics, json, locks, options, os, paths, sequtils, import std/[algorithm, atomics, exitprocs, json, locks, options, os, paths,
strutils, tables, times] sequtils, strutils, tables, times]
import timeutils import timeutils
import std/logging as stdlog
from logging import Level from std/logging import Level
export logging.Level export Level
type type
GlobalLogServiceObj {.acyclic.} = object GlobalLogServiceObj {.acyclic.} = object
@@ -15,13 +16,9 @@ type
thresholds: TableRef[string, Level] thresholds: TableRef[string, Level]
rootLevel: Atomic[Level] rootLevel: Atomic[Level]
console: ThreadedConsoleLoggingState
file: ThreadedFileLoggingState
errorHandler: ErrorHandlerFunc errorHandler: ErrorHandlerFunc
errorHandlerLock: Lock errorHandlerLock: Lock
GlobalLogService = ref GlobalLogServiceObj GlobalLogService = ref GlobalLogServiceObj
@@ -41,7 +38,7 @@ type
ThreadLocalLogService* = ref LogService ThreadLocalLogService* = ref LogService
Logger* = object Logger* = ref object
scope*: string scope*: string
threadSvc: ThreadLocalLogService threadSvc: ThreadLocalLogService
@@ -87,18 +84,11 @@ type
absPath: Path absPath: Path
ThreadedConsoleLoggingState = object LogWriterThreadState[M] = object
initialized: Atomic[bool] initialized: Atomic[bool]
shutdown: Atomic[bool] shutdown: Atomic[bool]
chan: Channel[ConsoleMessage] chan: Channel[M]
writerThread: Thread[GlobalLogService] writerThread: Thread[void]
ThreadedFileLoggingState = object
initialized: Atomic[bool]
shutdown: Atomic[bool]
chan: Channel[FileMessage]
writerThread: Thread[GlobalLogService]
ConsoleLogAppender* = ref object of LogAppender ConsoleLogAppender* = ref object of LogAppender
@@ -123,6 +113,61 @@ type
formatter*: LogMessageFormatter formatter*: LogMessageFormatter
absPath*: Path absPath*: Path
StdLoggingAppender* = ref object of LogAppender
## Log appender that forwards log messages to the std/logging
## implementation. This is primarily intended for libraries and other
## situations where you expect that your code will be third-party to others
## and want to respect applications which use std/logging for log handlers
## and configuration.
fallbackOnly*: bool
## when true, only forward to std/logging where there are no appenders
## configured on the related LogService
formatter*: LogMessageFormatter
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,
lvl: Level,
message: string,
error: Option[ref Exception] = none[ref Exception](),
additionalData: JsonNode = JNULL): LogMessage =
LogMessage(
scope: scope,
level: lvl,
error: error,
timestamp: now(),
message: message,
additionalData: additionalData)
proc initLogMessage*(
scope: string,
lvl: Level,
msg: JsonNode,
error: Option[ref Exception] = none[ref Exception]()): LogMessage =
LogMessage(
scope: scope,
level: lvl,
error: error,
timestamp: now(),
message:
if msg.hasKey("message"): msg["message"].getStr
else: "",
additionalData: msg)
method clone*(app: LogAppender): LogAppender {.base, gcsafe.} = method clone*(app: LogAppender): LogAppender {.base, gcsafe.} =
raise newException(CatchableError, "missing concrete implementation") raise newException(CatchableError, "missing concrete implementation")
@@ -144,40 +189,39 @@ proc defaultErrorHandlerFunc*(
stderr.flushFile() stderr.flushFile()
except Exception: discard # we tried... except Exception: discard # we tried...
proc shutdownThreadedConsoleLogging(gls: var GlobalLogServiceObj) = proc shutdownThreadedConsoleLogging() =
if not gls.console.initialized.load(): return 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 # Send sentinel values to wake up the writer thread
try: gls.console.chan.send(ConsoleMessage(message: "", useStderr: false)) try: consoleLogging.chan.send(ConsoleMessage(message: "", useStderr: false))
except Exception: discard except Exception: discard
joinThread(gls.console.writerThread) joinThread(consoleLogging.writerThread)
gls.console.chan.close() consoleLogging.chan.close()
gls.console.initialized.store(false) consoleLogging.initialized.store(false)
proc shutdownThreadedFileLogging(gls: var GlobalLogServiceObj) = proc shutdownThreadedFileLogging() =
if not gls.file.initialized.load(): return 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("/"))) withLock loggingThreadInitLock:
try: fileLogging.chan.send(FileMessage(message: "", absPath: Path("/")))
except Exception: discard except Exception: discard
joinThread(gls.file.writerThread) joinThread(fileLogging.writerThread)
gls.file.chan.close() fileLogging.chan.close()
gls.file.initialized.store(false) fileLogging.initialized.store(false)
proc `=destroy`*(gls: var GlobalLogServiceObj) = proc `=destroy`*(gls: var GlobalLogServiceObj) =
# only one thread should cleanup # only one thread should cleanup
if not gls.initialized.exchange(false): return if not gls.initialized.exchange(false): return
gls.shutdownThreadedConsoleLogging()
gls.shutdownThreadedFileLogging()
try: deinitLock(gls.lock) try: deinitLock(gls.lock)
except Exception: discard except Exception: discard
@@ -210,6 +254,28 @@ proc ensureFreshness*(ls: var LogService) =
proc ensureFreshness*(ls: ThreadLocalLogService) = ensureFreshness(ls[]) proc ensureFreshness*(ls: ThreadLocalLogService) = ensureFreshness(ls[])
proc initGlobalLogService(
rootLevel = lvlAll,
errorHandler = defaultErrorHandlerFunc): GlobalLogService =
result = GlobalLogService()
result.configVersion.store(0)
initLock(result.lock)
initLock(result.errorHandlerLock)
result.appenders = @[]
result.thresholds = newTable[string, Level]()
result.rootLevel.store(rootLevel)
result.errorHandler = errorHandler
result.initialized.store(true)
proc initLogService(gls: GlobalLogService): LogService =
var lsRef: ThreadLocalLogService = ThreadLocalLogService(
configVersion: UninitializedConfigVersion, global: gls)
ensureFreshness(lsRef)
result = lsRef[]
proc initLogService*( proc initLogService*(
rootLevel = lvlAll, rootLevel = lvlAll,
errorHandler = defaultErrorHandlerFunc): LogService = errorHandler = defaultErrorHandlerFunc): LogService =
@@ -227,24 +293,12 @@ proc initLogService*(
## configure thresholds, and create loggers. The ref returned by this ## configure thresholds, and create loggers. The ref returned by this
## procedure should also be retained by the main thread so that garbage ## procedure should also be retained by the main thread so that garbage
## collection does not harvest the global state while it is still in use. ## collection does not harvest the global state while it is still in use.
let global = GlobalLogService() let global = initGlobalLogService(rootLevel, errorHandler)
global.configVersion.store(0) result = initLogService(global)
global.initialized.store(true)
initLock(global.lock)
initLock(global.errorHandlerLock)
global.appenders = @[]
global.thresholds = newTable[string, Level]()
global.rootLevel.store(rootLevel)
global.errorHandler = errorHandler
var lsRef: ThreadLocalLogService = ThreadLocalLogService(configVersion: -1, global: global)
ensureFreshness(lsRef)
result = lsRef[]
proc threadLocalRef*(ls: LogService): ThreadLocalLogService = proc threadLocalRef*(ls: LogService): ThreadLocalLogService =
result = new(LogService) new result
result[] = ls result[] = ls
@@ -362,6 +416,15 @@ proc getLogger*(
result = Logger(scope: scope, threadSvc: ls) 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.} = proc addAppender*(ls: var LogService, appender: LogAppender) {.gcsafe.} =
## Add a log appender to the global log service and refresh the local thread ## 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 ## state. The updated global state will trigger other threads to refresh
@@ -378,6 +441,19 @@ proc addAppender*(ls: ThreadLocalLogService, appender: LogAppender) {.gcsafe.} =
addAppender(ls[], appender) addAppender(ls[], appender)
proc clearAppenders*(ls: var LogService) {.gcsafe.} =
## Remove all log appenders added to the global log service and refresh the
## local thread state. The updated global state will trigger other threads to
## refresh their state as well.
withLock ls.global.lock:
ls.global.appenders = @[]
ls.global.configVersion.atomicInc
proc clearAppenders*(ls: ThreadLocalLogService) {.gcsafe.} =
clearAppenders(ls[])
func getEffectiveThreshold(logger: Logger): Level {.gcsafe.} = func getEffectiveThreshold(logger: Logger): Level {.gcsafe.} =
## Get the effective logging level threshold for a logger. This is the most ## Get the effective logging level threshold for a logger. This is the most
## specific level that is set for the logger or any of its parents. The root ## specific level that is set for the logger or any of its parents. The root
@@ -399,89 +475,173 @@ func getEffectiveThreshold(logger: Logger): Level {.gcsafe.} =
result = logger.threadSvc.thresholds[namespaces[0]] result = logger.threadSvc.thresholds[namespaces[0]]
proc doLog(logger: Logger, msg: LogMessage) {.gcsafe.} = proc isEnabled*(l: Logger, lvl: Level): bool {.inline,gcsafe.} =
ensureFreshness(logger.threadSvc) lvl >= l.getEffectiveThreshold
if msg.level < logger.getEffectiveThreshold: return
proc sendToAppenders(logger: Logger, msg: LogMessage) {.gcsafe,inline.} =
for app in logger.threadSvc.appenders: for app in logger.threadSvc.appenders:
if logger.scope.startsWith(app.namespace) and msg.level >= app.threshold: if logger.scope.startsWith(app.namespace) and msg.level >= app.threshold:
app.appendLogMessage(logger.threadSvc, msg) app.appendLogMessage(logger.threadSvc, msg)
proc log*(l: Logger, lvl: Level, msg: string) {.gcsafe.} = template log*(l: Logger, lm: LogMessage) =
l.doLog(LogMessage( ensureFreshness(l.threadSvc)
scope: l.scope,
level: lvl, if lm.level >= l.getEffectiveThreshold:
error: none[ref Exception](), sendToAppenders(l, lm)
timestamp: now(),
message: msg, template log*(l: Logger, lvl: Level, msg: untyped) =
additionalData: newJNull())) ensureFreshness(l.threadSvc)
if lvl >= l.getEffectiveThreshold:
sendToAppenders(l, initLogMessage(l.scope, lvl, msg))
proc log*( template log*[T: ref Exception](l: Logger, lvl: Level, err: T, msg: untyped) =
l: Logger, ensureFreshness(l.threadSvc)
if lvl >= l.getEffectiveThreshold:
sendToAppenders(
l,
initLogMessage(l.scope, lvl, msg, some(cast[ref Exception](err))))
template log*(l: Option[Logger], lm: LogMessage) =
if l.isSome: log(l.get, lm)
template log*(l: Option[Logger], lvl: Level, msg: untyped) =
if l.isSome: log(l.get, lvl, msg)
template log*(
l: Option[Logger],
lvl: Level, lvl: Level,
error: ref Exception, error: ref Exception,
msg: string ) {.gcsafe.} = msg: untyped) =
l.doLog(LogMessage(
scope: l.scope,
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.scope,
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) if l.isSome: log(l.get, lvl, error, msg)
template debug*[T](l: Logger, msg: T) = log(l, lvlDebug, msg) template debug*[L: Logger or Option[Logger], M](l: L, msg: M) =
template info*[T](l: Logger, msg: T) = log(l, lvlInfo, msg) log(l, lvlDebug, 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 info*[L: Logger or Option[Logger], M](l: L, msg: M) =
template error*(l: Logger, error: ref Exception, msg: string) = log(l, lvlInfo, msg)
template notice*[L: Logger or Option[Logger], M](l: L, msg: M) =
log(l, lvlNotice, msg)
template warn*[L: Logger or Option[Logger], M](l: L, msg: M) =
log(l, lvlWarn, msg)
template error*[L: Logger or Option[Logger], M](l: L, msg: M) =
log(l, lvlError, msg)
template error*[L: Logger or Option[Logger], M](l: L, error: ref Exception, msg: M) =
log(l, lvlError, error, msg) log(l, lvlError, error, msg)
template fatal*[T](l: Logger, msg: T) = log(l, lvlFatal, msg) template fatal*[L: Logger or Option[Logger], M](l: L, msg: M) =
template fatal*(l: Logger, error: ref Exception, msg: string) = log(l, lvlFatal, msg)
template fatal*[L: Logger or Option[Logger], M](l: L, error: ref Exception, msg: M) =
log(l, lvlFatal, error, msg) 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)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# CustomerLogAppender Implementation # 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
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
func initCustomLogAppender*[T]( func initCustomLogAppender*[T](
@@ -491,7 +651,7 @@ func initCustomLogAppender*[T](
threshold = lvlAll): CustomLogAppender[T] {.gcsafe.} = threshold = lvlAll): CustomLogAppender[T] {.gcsafe.} =
if doLogMessage.isNil: if doLogMessage.isNil:
debugEcho "initCustomLogAppender: doLogMessage is nil" raise newException(ValueError, "initCustomLogAppender: doLogMessage is nil")
result = CustomLogAppender[T]( result = CustomLogAppender[T](
namespace: namespace, namespace: namespace,
@@ -500,8 +660,8 @@ func initCustomLogAppender*[T](
state: state) state: state)
method clone*[T](cla: CustomLogAppender[T]): LogAppender {.gcsafe.} = method clone*[T](cla: CustomLogAppender[T]): LogAppender {.gcsafe.} =
if cla.doLogMessage.isNil: assert not cla.doLogMessage.isNil,
debugEcho "CustomLogAppender#clone: source doLogMessage is nil" "CustomLogAppender#clone: source doLogMessage is nil"
result = CustomLogAppender[T]( result = CustomLogAppender[T](
namespace: cla.namespace, namespace: cla.namespace,
@@ -516,7 +676,7 @@ method appendLogMessage[T](
msg: LogMessage) {.gcsafe.} = msg: LogMessage) {.gcsafe.} =
try: try:
if cla.doLogMessage.isNil: if cla.doLogMessage.isNil:
debugEcho "doLogMessage is nil" raise newException(ValueError, "CustomLogAppender.appendLogMessage: doLogMessage is nil")
else: cla.doLogMessage(cla.state, msg) else: cla.doLogMessage(cla.state, msg)
except Exception: except Exception:
ls.global.reportLoggingError( ls.global.reportLoggingError(
@@ -540,41 +700,6 @@ proc initConsoleLogAppender*(
useStderr: useStderr) 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.} = method clone*(cla: ConsoleLogAppender): LogAppender {.gcsafe.} =
result = ConsoleLogAppender( result = ConsoleLogAppender(
namespace: cla.namespace, namespace: cla.namespace,
@@ -588,11 +713,11 @@ proc appendLogMessageMultiThreaded(
ls: ref LogService, ls: ref LogService,
msg: LogMessage) {.gcsafe.} = msg: LogMessage) {.gcsafe.} =
if not ls.global.console.initialized.load(): if not consoleLogging.initialized.load():
ls.global.initThreadedConsoleLogging() initThreadedConsoleLogging()
try: try:
ls.global.console.chan.send(ConsoleMessage( consoleLogging.chan.send(ConsoleMessage(
message: cla.formatter(msg), message: cla.formatter(msg),
useStderr: cla.useStderr)) useStderr: cla.useStderr))
except Exception: except Exception:
@@ -659,67 +784,6 @@ proc initFileLogAppender*(
formatter: formatter, formatter: formatter,
absPath: absolutePath(Path(filePath))) 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.} = method clone*(fla: FileLogAppender): LogAppender {.gcsafe.} =
result = FileLogAppender( result = FileLogAppender(
@@ -734,11 +798,11 @@ proc appendLogMessageMultiThreaded(
ls: ref LogService, ls: ref LogService,
msg: LogMessage) {.gcsafe.} = msg: LogMessage) {.gcsafe.} =
if not ls.global.file.initialized.load(): if not fileLogging.initialized.load():
ls.global.initThreadedFileLogging() initThreadedFileLogging()
try: try:
ls.global.file.chan.send(FileMessage( fileLogging.chan.send(FileMessage(
message: fla.formatter(msg), message: fla.formatter(msg),
absPath: fla.absPath)) absPath: fla.absPath))
except Exception: discard except Exception: discard
@@ -774,15 +838,67 @@ method appendLogMessage(
"unable to append to FileLogAppender") "unable to append to FileLogAppender")
# -----------------------------------------------------------------------------
# StdLoggingAppender Implementation
# -----------------------------------------------------------------------------
func formatForwardedLog*(lm: LogMessage): string =
## Default formatter for the StdLoggingAppender that prepends the logger
## scope to the message before formatting the message via
## *formatSimpleTextLog*
"[" & lm.scope & "] " & formatSimpleTextLog(lm)
func initStdLoggingAppender*(
fallbackOnly = true,
formatter = formatForwardedLog,
namespace = "",
threshold = lvlAll): StdLoggingAppender {.gcsafe.} =
result = StdLoggingAppender(
namespace: namespace,
threshold: threshold,
fallbackOnly: fallbackOnly,
formatter: formatter)
method clone*(sla: StdLoggingAppender): LogAppender {.gcsafe.} =
result = StdLoggingAppender(
namespace: sla.namespace,
threshold: sla.threshold,
fallbackOnly: sla.fallbackOnly,
formatter: sla.formatter)
method appendLogMessage*(
sla: StdLoggingAppender,
ls: ThreadLocalLogService,
msg: LogMessage) {.gcsafe.} =
if sla.fallbackOnly and ls.appenders.len > 1: return
stdlog.log(msg.level, sla.formatter(msg))
# -----------------------------------------------------------------------------
# Cleanup
# -----------------------------------------------------------------------------
addExitProc(proc() =
shutdownThreadedConsoleLogging()
shutdownThreadedFileLogging()
)
when isMainModule:
import std/[tempfiles, unittest]
import ./namespaced_logging/testutil
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Tests # Tests
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
when isMainModule:
import std/[files, tempfiles, unittest]
import ./namespaced_logging/testutil
suite "GlobalLogService Initialization": suite "GlobalLogService Initialization":
test "initLogService creates valid service": test "initLogService creates valid service":
@@ -905,8 +1021,7 @@ when isMainModule:
setup: setup:
let ls = threadLocalRef(initLogService()) let ls = threadLocalRef(initLogService())
let loggedMsgs = initLoggedMessages() let loggedMsgs = initLoggedMessages()
let testAppender = initTestLogAppender(loggedMsgs) ls.addAppender(initTestLogAppender(loggedMsgs))
ls.addAppender(testAppender)
test "getLogger creates logger with correct scope": test "getLogger creates logger with correct scope":
let logger = ls.getLogger("api/users") let logger = ls.getLogger("api/users")
@@ -916,6 +1031,26 @@ when isMainModule:
let logger = ls.getLogger("api/users", some(lvlWarn)) let logger = ls.getLogger("api/users", some(lvlWarn))
check ls.thresholds["api/users"] == lvlWarn check ls.thresholds["api/users"] == lvlWarn
test "log methods work":
let logger = ls.getLogger("test")
logger.log(lvlDebug, "debug string msg")
logger.log(lvlInfo, %*{"message": "info json msg"})
logger.log(lvlNotice, "notice string msg")
logger.log(lvlError, newException(ValueError, "exception msg"), "error ex. msg")
let lm = loggedMsgs.get()
check:
lm.len == 4
lm[0].level == lvlDebug
lm[0].message.contains("debug string msg")
lm[1].level == lvlInfo
lm[1].message.contains("info json msg")
lm[2].level == lvlNotice
lm[2].message.contains("notice string msg")
lm[3].level == lvlError
lm[3].message.contains("error ex. msg")
test "logger convenience methods work": test "logger convenience methods work":
let logger = ls.getLogger("test") let logger = ls.getLogger("test")
@@ -964,8 +1099,7 @@ when isMainModule:
setup: setup:
let ls = threadLocalRef(initLogService()) let ls = threadLocalRef(initLogService())
let loggedMsgs = initLoggedMessages() let loggedMsgs = initLoggedMessages()
let testAppender = initTestLogAppender(loggedMsgs) ls.addAppender(initTestLogAppender(loggedMsgs))
ls.addAppender(testAppender)
test "root level filtering": test "root level filtering":
ls.setRootThreshold(lvlInfo) ls.setRootThreshold(lvlInfo)
@@ -1015,6 +1149,25 @@ when isMainModule:
lm[0].scope == "api/users/detail" lm[0].scope == "api/users/detail"
lm[0].level == lvlDebug lm[0].level == lvlDebug
test "message construction is avoided if the message is not logged":
var expensiveCallCount = 0
proc expensiveCall(): int =
inc expensiveCallCount
return expensiveCallCount
ls.setThreshold("test", lvlInfo)
let logger = ls.getLogger("test")
logger.debug("Expensive call (" & $expensiveCall() & ")")
logger.info("Expensive call (" & $expensiveCall() & ")")
let lm = loggedMsgs.get()
check:
lm.len == 1
lm[0].message.contains("Expensive call (1)")
expensiveCallCount == 1
suite "Appender Functionality": suite "Appender Functionality":
setup: setup:
let ls = threadLocalRef(initLogService()) let ls = threadLocalRef(initLogService())
@@ -1113,6 +1266,8 @@ when isMainModule:
lines.len == 1 lines.len == 1
"test message" in lines[0] "test message" in lines[0]
removeFile(pathStr)
test "file appender clone": test "file appender clone":
let original = initFileLogAppender("tempfile.log", namespace = "test") let original = initFileLogAppender("tempfile.log", namespace = "test")
let cloned = clone(original) let cloned = clone(original)
@@ -1121,3 +1276,107 @@ when isMainModule:
let clonedFile = FileLogAppender(cloned) let clonedFile = FileLogAppender(cloned)
check clonedFile.absPath == original.absPath check clonedFile.absPath == original.absPath
check clonedFile.namespace == "test" check clonedFile.namespace == "test"
suite "StdLoggingAppender":
var fileLogger: FileLogger
var tempFile: File
var tempFilename: string
setup:
let ls = threadLocalRef(initLogService())
(tempFile, tempFilename) = createTempFile("stdlog_test", ".tmp.log")
fileLogger = newFileLogger(tempFile, flushThreshold = lvlAll)
addHandler(fileLogger)
teardown:
removeHandler(fileLogger)
try: close(tempFile)
except Exception: discard
removeFile(tempFilename)
test "forwards to std logging":
ls.addAppender(initStdLoggingAppender())
let logger = ls.getLogger("test")
logger.debug("message at debug")
logger.info("message at info")
logger.error("message at error")
tempFile.flushFile()
close(tempFile)
check open(tempFile, tempFilename, fmRead)
let lines = toSeq(lines(tempFile))
check:
lines.len == 3
lines[0] == "DEBUG [test] message at debug"
lines[1] == "INFO [test] message at info"
lines[2] == "ERROR [test] message at error"
test "fallbackOnly works when on":
ls.addAppender(initStdLoggingAppender())
let logger = ls.getLogger("test")
logger.debug("message at debug")
logger.info("message at info")
logger.error("message at error")
let loggedMsgs = initLoggedMessages()
ls.addAppender(initTestLogAppender(loggedMsgs))
logger.notice("message at notice")
logger.warn("message at warn")
logger.fatal("message at fatal")
tempFile.flushFile()
close(tempFile)
check open(tempFile, tempFilename, fmRead)
let lines = toSeq(lines(tempFile))
let lm = loggedMsgs.get()
check:
lines.len == 3
lines[0] == "DEBUG [test] message at debug"
lines[1] == "INFO [test] message at info"
lines[2] == "ERROR [test] message at error"
lm.len == 3
lm[0].message.contains("message at notice")
lm[1].message.contains("message at warn")
lm[2].message.contains("message at fatal")
test "fallbackOnly works when off":
ls.addAppender(initStdLoggingAppender(fallbackOnly = false))
let logger = ls.getLogger("test")
logger.debug("message at debug")
logger.info("message at info")
logger.error("message at error")
let loggedMsgs = initLoggedMessages()
ls.addAppender(initTestLogAppender(loggedMsgs))
logger.notice("message at notice")
logger.warn("message at warn")
logger.fatal("message at fatal")
tempFile.flushFile()
close(tempFile)
check open(tempFile, tempFilename, fmRead)
let lines = toSeq(lines(tempFile))
let lm = loggedMsgs.get()
check:
lines.len == 6
lines[0] == "DEBUG [test] message at debug"
lines[1] == "INFO [test] message at info"
lines[2] == "ERROR [test] message at error"
lines[3] == "NOTICE [test] message at notice"
lines[4] == "WARN [test] message at warn"
lines[5] == "FATAL [test] message at fatal"
lm.len == 3
lm[0].message.contains("message at notice")
lm[1].message.contains("message at warn")
lm[2].message.contains("message at fatal")

View File

@@ -1,98 +0,0 @@
import std/[json, options]
from logging import Level
import ../namespaced_logging
export
# Types
Level,
Logger,
LogAppender,
LogMessage,
ConsoleLogAppender,
CustomLogAppender,
CustomLogAppenderFunction,
FileLogAppender,
# Procs/Funcs
`%`,
initConsoleLogAppender,
initCustomLogAppender,
initFileLogAppender,
formatJsonStructuredLog
var globalLogServiceRef: ThreadLocalLogService = new(LogService)
globalLogServiceRef[] = initLogService()
var threadLocalLogServiceRef {.threadvar.}: ThreadLocalLogService
var defaultLogger {.threadvar.}: Option[Logger]
proc getThreadLocalLogServiceRef(): ThreadLocalLogService {.inline.} =
if threadLocalLogServiceRef.isNil:
threadLocalLogServiceRef = new(LogService)
threadLocalLogServiceRef[] = globalLogServiceRef[]
return threadLocalLogServiceRef
proc getDefaultLogger(): Logger {.inline.} =
if defaultLogger.isNone:
defaultLogger = some(getThreadLocalLogServiceRef().getLogger(""))
return defaultLogger.get
proc useForAutoconfiguredLogging*(ls: LogService) =
globalLogServiceRef[] = ls
proc setRootLoggingThreshold*(lvl: Level) =
setRootThreshold(getThreadLocalLogServiceRef(), lvl)
proc setLoggingThreshold*(scope: string, lvl: Level) =
setThreshold(getThreadLocalLogServiceRef(), scope, lvl)
proc addLogAppender*(appender: LogAppender) =
addAppender(getThreadLocalLogServiceRef(), appender)
proc getLogger*(scope: string, lvl: Option[Level] = none[Level]()): Logger =
getLogger(getThreadLocalLogServiceRef(), scope, lvl)
proc log*(lvl: Level, msg: string) = getDefaultLogger().log(lvl, msg)
proc log*(lvl: Level, msg: JsonNode) = getDefaultLogger().log(lvl, msg)
proc log*(lvl: Level, error: ref Exception, msg: string) =
getDefaultLogger().log(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*(error: ref Exception, msg: string) = log(lvlError, error, msg)
template fatal*[T](msg: T) = log(lvlFatal, msg)
template fatal*(error: ref Exception, msg: string) = log(lvlFatal, error, msg)
when isMainModule:
import std/unittest
import ./testutil
suite "Autoconfigured Logging":
setup:
globalLogServiceRef[] = initLogService()
let loggedMessages = initLoggedMessages()
let testAppender = initTestLogAppender(loggedMessages)
test "simple no-config logging":
addLogAppender(testAppender)
info("test message")
let lm = loggedMessages.get()
check:
lm.len == 1
lm[0].level == lvlInfo
lm[0].message == "test message"