1 Commits
2.0.1 ... 2.0.0

Author SHA1 Message Date
3e7f3d4c1e Add version to nimble. 2025-07-07 21:49:35 -05:00
4 changed files with 248 additions and 1134 deletions

610
README.md
View File

@ -1,178 +1,38 @@
# Namespaced Logging for Nim # Namespaced Logging for Nim
`namespaced_logging` is intended to be a high-performance, thread-safe logging `namespaced_logging` provides a logging framework similar to [log4j][] or
framework similar to [std/logging][std-logging] with support for [logback][] for Nim. It has three main motivating features:
namespace-scoped logging similar to [log4j][] or [logback][] for Nim. It has
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 (old-style string logging is also
- Simple, autoconfigured usage pattern reminiscent of the supported).
[std/logging][std-logging] interface.
## Getting Started ## Getting Started
Install the package via nimble: Install the package from nimble:
```bash ```bash
# Not yet in official Nim packages. TODO once we've battle-tested it a little nimble install namespaced_logging
nimble install https://github.com/jdbernard/nim-namespaced-logging
``` ```
## Usage Patterns Then, in your application, you can use the logging system like so:
### 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 ```nim
import namespaced_logging import namespaced_logging
# Manually creating a LogService. This is an independent logging root fully # On the main thread
# isolated from subsequent LogServices initialized with initLogService let logService = initLogService()
var ls = initLogService() logService.addAppender(initConsoleAppender(LogLevel.INFO))
# Configure logging # On any thread, including the main thread
ls.addAppender(initConsoleLogAppender()) let logger = logService.getLogger("app/service/example")
ls.addAppender(initFileLogAppender("app.log")) logger.info("Log from the example service")
ls.setThreshold("api", lvlWarn)
# Create loggers # Only get logs at the WARN or higher level from the database module
let localLogSvc = threadLocalRef(ls) let logger = logService.getLogger("app/database", threshold = some(Level.lvlWarn))
let apiLogger = localLogSvc.getLogger("api") logger.error("Database connection failed")
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
# Main thread setup
var logService = initLogService()
logService.addAppender(initConsoleLogAppender())
var localLogSvc = threadLocalRef(logService) # for use on main thread
# Worker thread function
proc worker(ls: LogService) {.thread.} =
let localLogSvc = threadLocalRef(ls)
let logger = localLogSvc.getLogger("worker")
# Runtime configuration changes
localLogSvc.setThreshold("worker", lvlDebug)
logger.debug("Worker configured")
# Safe thread creation
createThread(workerThread, worker, logService)
```
### Dynamic Configuration
```nim
# Configuration can change at runtime
proc configureLogging(localLogSvc: ThreadLocalLogService, verbose: bool) =
if verbose:
localLogSvc.setRootThreshold(lvlDebug)
localLogSvc.addAppender(initFileLogAppender("debug.log"))
else:
localLogSvc.setRootThreshold(lvlInfo)
# 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 ## Loggers and Appenders
The logging system is composed of two main components: loggers and appenders. The logging system is composed of two main components: loggers and appenders.
@ -183,392 +43,140 @@ threshold, which determines which log events are acted upon by the appender,
and, optionally, a namespace filter, which determines from which loggers the and, optionally, a namespace filter, which determines from which loggers the
appender accepts log events. appender accepts log events.
### Heirarchical Logging Namespaces ### Heirarchical Logging and Namespaces
Loggers are organized hierarchically, with the hierarchy defined by the logger Loggers are organized hierarchically, with the hierarchy defined by the logger
scope. A logger with the scope `app/service/example` is conceptually a child of name. A logger with the name `app/service/example` is a child of the logger
the logger with the scope `app/service`. By default, appenders accept log with the name `app/service`. By default, appenders accept log events from all
events from all loggers, but this can be restricted by setting a namespace loggers, but this can be restricted by setting a namespace filter on the
filter on the appender. An appender with a namespace set will accept log events appender. An appender with a namespace set will accept log events from all
from all loggers with scopes that start with the namespace. For example, an loggers with names that start with the namespace. For example, an appender with
appender with the namespace `app` will accept log events from the loggers the namespace `app` will accept log events from the loggers `app`,
`app`, `app/service`, and `app/service/example`, but not from `api/service`. `app/service`, and `app/service/example`, but not from `api/service`.
The other impact of the logger heirarchy is in the effective logging level of The other impact of the logger heirarchy is in the effective logging level of
the logger. An explicit logging level threshold can be set for any scope. Any the logger. Any logger can have an explicit logging level set, but if it does
scope that does not have an explicit inherits its threshold from ancestor not, the effective logging level is inherited from ancestor loggers upwards in
loggers upwards in the scope naming heirarchy. This pattern is explained in the logger heirarchy. This pattern is explained in detail in the [logback
detail in the [logback documentation][effective logging level] and applies in documentation][effective logging level] and applies in the same manner to
the same manner to loggers in this library. loggers in this library.
### LogMessageFormater
Both the [ConsoleLogAppender](#ConsoleLogAppender) and
[FileLogAppender](#FileLogAppender) can be given a *LogMessageFormatter* to
determine how a log message is formatted before being written.
```nim
type LogMessageFormatter* = proc (msg: LogMessage): string {.gcsafe.}
```
## Available Appenders
### ConsoleLogAppender
Used for writing logs to stdout or stderr.
```nim
proc initConsoleLogAppender*(
formatter = formatSimpleTextLog,
## formatJsonStructuredLog is another useful formatter provided
## or you can write your own
useStderr = false, ## stdout is used by default
namespace = "", ## appender matches all scopes by default
threshold = lvlAll ## and accepts all message levels by default
): ConsoleLogAppender {.gcsafe.}
```
The first time a message is sent to any *ConsoleLogAppender*, we create a
writer thread which writes messages to the specified output in the order they
are received, flushing the file handle after each write to enforce an ordering.
The ConsoleLogAppender implementation uses a channel to send messages to the
writer thread.
### FileLogAppender
Used for writing logs to files.
```nim
proc initFileLogAppender*(
filePath: string,
formatter = formatSimpleTextLog,
## formatJsonStructuredLog is another useful formatter provided
## or you can write your own
namespace = "",
threshold = lvlAll
): FileLogAppender {.gcsafe.}
```
Similar to the *ConsoleLogAppender* implementation, the first time a message is
sent to any *FileLogAppender* we create a writer thread which writes messages
to files associated with the *FileLogAppender* configured for the current
*LogService*.
`namespaced_logging` does not currently have built-in logic for file
rotation, but it does play nice with external file rotation strategies. We do
not hold open file handles. The *FileLogAppender* attempts to batch messages
by destination file, opens the file with mode `fmAppend`, writes the current
batch of log messages, and then closes the file handle. Because of this, it has
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
Provides an extension point for custom logging implementations.
```nim
func initCustomLogAppender*[T](
state: T, # arbitrary state needed for the appender
doLogMessage: CustomLogAppenderFunc[T],
# custom log appender implementation
namespace = "",
threshold = lvlAll): CustomLogAppender[T] {.gcsafe.} =
```
The `state` field allows you to explicitly pass in any data that is required
for the custom functionality.
*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.*
> [!WARNING] 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
> `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
> is aware of, either on the thread where they were initialized or by
> explicitly telling the GC about the cross-thread reference *(TODO: how?)*.
See [testutil][] and the unit tests in [namespaced\_logging][nsl-unit-tests]
for an example.
## Notes on Use in Multi-Threaded Applications ## Notes on Use in Multi-Threaded Applications
The loggers and appenders in this library are thread-safe and are intended to The loggers and appenders in this library are thread-safe and behaves more
behave more intuitively in a multi-threaded environment than intuitively in a multi-threaded environment than `std/logging`, particularly in
[std/logging][std-logging] while presenting a similar API. This is particularly 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 the "source
that more closely matches the contract of [std/logging][std-logging]. In this of truth" for logging configuration and is shared between all threads.
case all thread and state management is done for you. The only limitation is Internally all access to the *LogService* is protected by a mutex.
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 Logging can be very noisy and if the *LogService* needed to be consulted for
`namespaced_logging` directly. In this case, the thread which initialized every log event, it could easily become a performance bottleneck. To avoid
*LogService* must also be the longest-living thread that uses that *LogService* this, the *getLogger* procedure makes a thread-local copy of the logging system
instance. If the initializing thread terminates or the *LogService* object in configuration (loggers defined and appenders attached).
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 **Note** that this means that the thread-local cache of the logging system
point for the logging system and should be initialized on the main thread. The configuration can become stale if the logging system configuration is changed
*LogService* contains a reference to the "source of truth" for logging after the thread-local copy is made (if another appender is added, for
configuration and is safe to be shared between all threads. example). This is a trade-off to avoid the performance penalty of consulting
the *LogService* for every log event.
Individual threads should use the *threadLocalRef* proc to obtain a
*ThreadLocalLogService* reference that can be used to create *Logger* objects.
*ThreadLocalLogService* objects cache the global *LogService* state locally to
avoid expensive locks on the shared state. Instead an atomic configuration
version number is maintained to allow the thread-local state to detect global
configuration changes via an inexpensive [load][atomic-load] call and
automatically synchronize only when necessary.
This thread-local caching mechanism is the primary advantage of this logging This thread-local caching mechanism is the primary advantage of this logging
system over std/logging in a multi-threaded environment as it means that system over `std/logging` in a multi-threaded environment as it means that
the logging system itself is responsible for making sure appenders are the logging system itself is responsible for making sure appenders are
configured for every thread where loggers are used, even if the thread configured for every thread where loggers are used, even if the thread
initialization context is separated from the logging setup code. initialization context is separated from the logging setup code.
If you find yourself needing to change the logging configuration after the
logging system has been initialized, the *reloadThreadState* procedure can be
used to update the thread-local cache of the logging system configuration, but
it must be called on the thread you wish to update.
## Architectural Design As a final note, the advice to initialize the *LogService* on the main thread
is primarily to simplify the configuration of the logging service and avoid the
need to manually reload caches on individual threads. A *LogService* reference
is required to call *getLogger*, but it can be created on any thread.
### Overview ## Custom Appender Implementations
The namespaced logging library attempts to balance performance, safety, and Due to the thread-safety of the logging system, there are a few additional
usability in multithreaded environments. The design centers on two key types: considerations when implementing custom appenders. The *LogAppender* abstract
*LogService* and *ThreadLocalLogService*. class is the base class for all appenders. To implement a custom appender, two
methods must be implemented:
#### LogService (Value Type) ### `appendLogMessage`
```nim
type LogService* = object
configVersion: int
global: GlobalLogService
appenders: seq[LogAppender]
thresholds: TableRef[string, Level]
```
The *LogService* object is intended to support uses cases such as:
- **Main thread initialization**: a mutable *LogService* supports all of the
configuration functions you would typically need when initializing logging
for an application on the main thread.
- **Cross-thread communication**: Being an `object` type, *LogService* follows
value semantics and can be safely copied between threads.
- **Service composition**: independently initialized *LogService* objects are
truly independent and multiple can be created and embedded in larger
application contexts.
> [!TIP]
> The *LogService* object is the object that is intended to be shared across
> threads.
#### ThreadLocalLogService (Reference Type)
```nim
type ThreadLocalLogService* = ref LogService
```
*ThreadLocalLogService* is a reference to a thread-local copy of a *LogService*
and can be obtained via *threadLocalRef*. We purposefully use reference
semantics within the context of a thread so that *Logger* objects created
within the same thread context share the same *ThreadLocalLogService*
reference, avoiding the need to synchronize every *Logger* individually.
*ThreadLocalLogService* is the object that users are expected to interact with
during regular operation and support both the configuration functions of
*LogService* and the creation of *Logger* objects.
> [!CAUTION]
> *ThreadLocalLogService* objects should **never** be shared outside the
> 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
#### Safe Cross-Thread Pattern
```nim
# Main thread setup
let logService = initLogService()
logService.addAppender(initConsoleLogAppender())
# Safe: value semantics allow crossing thread boundaries
proc workerThread(ls: LogService) {.thread.} =
# Convert to thread-local reference for efficient operations
let tlls = threadLocalRef(ls)
let logger = tlls.getLogger("worker")
logger.info("Worker thread started")
createThread(worker, workerThread, logService)
```
#### Unsafe Pattern (Avoided by Design)
```nim
# DON'T DO THIS - unsafe reference sharing
# ThreadLocalLogService should not be shared across threads
let tlls = threadLocalRef(initLogService())
createThread(worker, someProc, tlls) # ❌ Potential GC issues
```
### Configuration Synchronization
#### Atomic Version Checking
The library uses atomic version numbers to efficiently detect configuration
changes:
```nim ```nim
proc ensureFreshness*(ls: var LogService) = method appendLogMessage*(appender: CustomLogAppender, msg: LogMessage): void {.base, gcsafe.}
# Cheap atomic check first
if ls.configVersion == ls.global.configVersion.load():
return # No changes, return immediately
# Only acquire lock and copy if versions differ
withLock ls.global.lock:
ls.configVersion = ls.global.configVersion.load
# Sync state...
``` ```
Goals/Motivation: This is the primary appender implementation that takes a LogMessage and
- Most logging operations skip expensive synchronization so the hot path is writes it to the appender's destination. As the signature suggests, the
fast. implementation must be GC-safe. As a multi-method, the *CustomLogAppender* type
- Propogate changes automatically so all threads see configuration updates. should be replaced by the actual name of your custom appender.
- Minimize lock contention by only acquiring when configuration changes
#### Thread-Local Caching Because the *LogAppender* uses multi-methods for dynamic dispatch, the
custom appender class must also be a `ref` type.
Each thread maintains its own copy of the logging configuration in ### `initThreadCopy`
*ThreadLocalLogService*:
- **Appenders**: Thread-local copies created via `clone()` method
- **Thresholds**: Complete copy of namespace-to-level mappings
- **Version tracking**: Local version number for change detection
This caching strategy provides:
- **High performance**: No locks needed for normal logging operations
- **Consistency**: All threads eventually see the same configuration
- **Isolation**: Thread-local state prevents cross-thread interference
## Error Handling
### Overview
For errors that occur during logging operations, there is a callback-based
error handling system designed to attempt to gracefully handle such failures.
Since logging is typically a non-critical operation we prioritize application
stability over guaranteed log delivery.
### Error Handler
The library uses a callback-based error handling pattern where applications can
register custom error handlers to be notified when logging operations fail. The
error handler receives:
- `error`: The exception that caused the failure
- `msg`: A descriptive message providing context about where the error occurred
```nim ```nim
type ErrorHandlerFunc* = proc(error: ref Exception, msg: string) {.gcsafe, nimcall.} method initThreadCopy*(app: LogAppender): LogAppender {.base, gcsafe.}
``` ```
### Default Error Handler This method is used to create a thread-local copy of the appender. It is called
by the *reloadThreadState* procedure to update the thread-local cache of the
logging system configuration. The implementation will be passed the appender
instance that was provided to the *addAppender* procedure and must return a
thread-local copy of that appender.
namespaced\_logging uses the `defaultErrorHandlerFunc` if a custom error The `initThreadCopy` implementations for the built-in *ConsoleLogAppender* and
handler has not been configured. The default handler: *FileLogAppender* provide simple examples of how to implement this method by
simply copying state into the local thread, but this method can also be used
to perform any other thread-specific initialization that may be required for
the appender implementation.
1. Attempts to write to stderr, assuming it is likely to be available and monitored ### Example Custom Appender
2. Writes an error message and includes both the exception message and stack
trace (not available in release mode).
3. Fails silently if it is unable to write to to stderr.
### Configuration The following defines a simple custom appender that writes log messages to a
database table. It uses the [waterpark][] connection pooling library to manage
#### Setting Custom Error Handlers database connections as waterpark is also thread-safe and makes implementation
```nim straight-forward.
# During initialization
var logService = initLogService(errorHandler = myCustomErrorHandler)
# Or at runtime on either the LogService...
logService.setErrorHandler(myCustomErrorHandler)
# ... or on a ThreadLocalLogService
var localLogSvc = threadLocalRef(logService)
localLogSvc.setErrorHandler(myCustomErrorHandler)
```
#### Disabling Error Reporting
```nim
proc silentErrorHandler(err: ref Exception, msg: string) {.gcsafe, nimcall.} =
discard # Do nothing
logService.setErrorHandler(silentErrorHandler)
```
### Best Practices
#### Provide Fallbacks
```nim ```nim
proc robustErrorHandler(err: ref Exception, msg: string) {.gcsafe, nimcall.} = import db_connectors/db_postgres
# Primary: Send to monitoring system import namespaced_logging, waterpark, waterpark/db_postgres
if not sendToMonitoring(err, msg):
# Secondary: Write to dedicated error log type DbLogAppender = ref object of LogAppender
if not writeToErrorLog(err, msg): dbPool: PostgresPool
# Tertiary: Use stderr as last resort
try: let dbPool: PostgresPool = newPostgresPool(10, "", "", "", connectionString)
stderr.writeLine("LOGGING ERROR [" & msg & "]: " & err.msg)
stderr.flushFile() method initThreadCopy*(app: LogAppender): LogAppender =
except: discard result = DbLogAppender(dbPool: dbPool) # copy semantics as PostgresPool is an object
method appendLogMessage*(appender: DbLogAppender, msg: LogMessage): void {gcsafe.} =
appender.withConnection conn:
conn.insert(
"INSERT INTO log_events " &
" (level, scope, message, error, timestamp, custom_fields) " &
"VALUES " &
" (?, ?, ?, ?, ?, ?)",
msg.level,
msg.scope,
msg.message,
if msg.error.isSome: msg.error.msg
else: "",
msg.timestamp,
msg.additionalData)
``` ```
#### Keep Error Handlers Simple
As much as possible, avoid complex operations that might themselves fail.
Don't do heavy operations like database writes, complex network operations, or
file system operations that might fail and cause cascading errors.
[log4j]: https://logging.apache.org/log4j/2.x/ [log4j]: https://logging.apache.org/log4j/2.x/
[logback]: https://logback.qos.ch/ [logback]: https://logback.qos.ch/
[effective logging level]: https://logback.qos.ch/manual/architecture.html#effectiveLevel [effective logging level]: https://logback.qos.ch/manual/architecture.html#effectiveLevel
[atomic-load]: https://nim-lang.org/docs/atomics.html#load%2CAtomic%5BT%5D%2CMemoryOrder
[std-logging]: https://nim-lang.org/docs/logging.html
[testutil]: /blob/main/src/namespaced_logging/testutil.nim
[nsl-unit-tests]: https://github.com/jdbernard/nim-namespaced-logging/blob/main/src/namespaced_logging.nim#L904

View File

@ -1,6 +1,6 @@
# Package # Package
version = "2.0.1" version = "2.0.0"
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,8 +13,3 @@ 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 src/namespaced_logging/autoconfigured.nim"
exec "src/namespaced_logging.out"
exec "src/namespaced_logging/autoconfigured"

View File

@ -1,10 +1,9 @@
import std/[algorithm, atomics, json, locks, options, os, paths, sequtils, import std/[algorithm, atomics, json, locks, options, os, paths, sequtils,
strutils, tables, times] strutils, tables, times]
import timeutils import timeutils
import std/logging as stdlog
from std/logging import Level from logging import Level
export Level export logging.Level
type type
GlobalLogServiceObj {.acyclic.} = object GlobalLogServiceObj {.acyclic.} = object
@ -22,12 +21,6 @@ type
errorHandler: ErrorHandlerFunc errorHandler: ErrorHandlerFunc
errorHandlerLock: Lock 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 GlobalLogService = ref GlobalLogServiceObj
@ -48,7 +41,7 @@ type
ThreadLocalLogService* = ref LogService ThreadLocalLogService* = ref LogService
Logger* = ref object Logger* = object
scope*: string scope*: string
threadSvc: ThreadLocalLogService threadSvc: ThreadLocalLogService
@ -130,55 +123,6 @@ 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()
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")
@ -253,12 +197,6 @@ proc ensureFreshness*(ls: var LogService) =
if ls.configVersion == ls.global.configVersion.load(): return 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: withLock ls.global.lock:
ls.configVersion = ls.global.configVersion.load ls.configVersion = ls.global.configVersion.load
@ -272,28 +210,6 @@ 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 =
@ -311,12 +227,24 @@ 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 = initGlobalLogService(rootLevel, errorHandler) let global = GlobalLogService()
result = initLogService(global) global.configVersion.store(0)
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 =
new result result = new(LogService)
result[] = ls result[] = ls
@ -450,19 +378,6 @@ 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
@ -484,77 +399,89 @@ func getEffectiveThreshold(logger: Logger): Level {.gcsafe.} =
result = logger.threadSvc.thresholds[namespaces[0]] result = logger.threadSvc.thresholds[namespaces[0]]
proc isEnabled*(l: Logger, lvl: Level): bool {.inline,gcsafe.} = proc doLog(logger: Logger, msg: LogMessage) {.gcsafe.} =
lvl >= l.getEffectiveThreshold ensureFreshness(logger.threadSvc)
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)
template log*(l: Logger, lm: LogMessage) = proc log*(l: Logger, lvl: Level, msg: string) {.gcsafe.} =
ensureFreshness(l.threadSvc) l.doLog(LogMessage(
scope: l.scope,
if lm.level >= l.getEffectiveThreshold: level: lvl,
sendToAppenders(l, lm) error: none[ref Exception](),
timestamp: now(),
template log*(l: Logger, lvl: Level, msg: untyped) = message: msg,
ensureFreshness(l.threadSvc) additionalData: newJNull()))
if lvl >= l.getEffectiveThreshold:
sendToAppenders(l, initLogMessage(l.scope, lvl, msg))
template log*[T: ref Exception](l: Logger, lvl: Level, err: T, msg: untyped) = proc log*(
ensureFreshness(l.threadSvc) l: Logger,
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: untyped) = msg: string ) {.gcsafe.} =
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*[L: Logger or Option[Logger], M](l: L, msg: M) = template debug*[T](l: Logger, msg: T) = log(l, lvlDebug, msg)
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 info*[L: Logger or Option[Logger], M](l: L, msg: M) = template error*[T](l: Logger, msg: T) = log(l, lvlError, msg)
log(l, lvlInfo, msg) template error*(l: Logger, error: ref Exception, msg: string) =
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*[L: Logger or Option[Logger], M](l: L, msg: M) = template fatal*[T](l: Logger, msg: T) = log(l, lvlFatal, msg)
log(l, lvlFatal, msg) template fatal*(l: Logger, error: ref Exception, msg: string) =
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)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# CustomLogAppender Implementation # CustomerLogAppender Implementation
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
func initCustomLogAppender*[T]( func initCustomLogAppender*[T](
@ -564,7 +491,7 @@ func initCustomLogAppender*[T](
threshold = lvlAll): CustomLogAppender[T] {.gcsafe.} = threshold = lvlAll): CustomLogAppender[T] {.gcsafe.} =
if doLogMessage.isNil: if doLogMessage.isNil:
raise newException(ValueError, "initCustomLogAppender: doLogMessage is nil") debugEcho "initCustomLogAppender: doLogMessage is nil"
result = CustomLogAppender[T]( result = CustomLogAppender[T](
namespace: namespace, namespace: namespace,
@ -573,8 +500,8 @@ func initCustomLogAppender*[T](
state: state) state: state)
method clone*[T](cla: CustomLogAppender[T]): LogAppender {.gcsafe.} = method clone*[T](cla: CustomLogAppender[T]): LogAppender {.gcsafe.} =
assert not cla.doLogMessage.isNil, if cla.doLogMessage.isNil:
"CustomLogAppender#clone: source doLogMessage is nil" debugEcho "CustomLogAppender#clone: source doLogMessage is nil"
result = CustomLogAppender[T]( result = CustomLogAppender[T](
namespace: cla.namespace, namespace: cla.namespace,
@ -589,7 +516,7 @@ method appendLogMessage[T](
msg: LogMessage) {.gcsafe.} = msg: LogMessage) {.gcsafe.} =
try: try:
if cla.doLogMessage.isNil: if cla.doLogMessage.isNil:
raise newException(ValueError, "CustomLogAppender.appendLogMessage: doLogMessage is nil") debugEcho "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(
@ -847,165 +774,14 @@ 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))
# -----------------------------------------------------------------------------
# Autoconfiguration Implementation
# -----------------------------------------------------------------------------
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)
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
# -----------------------------------------------------------------------------
when isMainModule: when isMainModule:
import std/[files, tempfiles, unittest] import std/[tempfiles, unittest]
import ./namespaced_logging/testutil import ./namespaced_logging/testutil
# -----------------------------------------------------------------------------
# Tests
# -----------------------------------------------------------------------------
suite "GlobalLogService Initialization": suite "GlobalLogService Initialization":
@ -1129,7 +905,8 @@ when isMainModule:
setup: setup:
let ls = threadLocalRef(initLogService()) let ls = threadLocalRef(initLogService())
let loggedMsgs = initLoggedMessages() let loggedMsgs = initLoggedMessages()
ls.addAppender(initTestLogAppender(loggedMsgs)) let testAppender = 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")
@ -1139,26 +916,6 @@ 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")
@ -1207,7 +964,8 @@ when isMainModule:
setup: setup:
let ls = threadLocalRef(initLogService()) let ls = threadLocalRef(initLogService())
let loggedMsgs = initLoggedMessages() let loggedMsgs = initLoggedMessages()
ls.addAppender(initTestLogAppender(loggedMsgs)) let testAppender = initTestLogAppender(loggedMsgs)
ls.addAppender(testAppender)
test "root level filtering": test "root level filtering":
ls.setRootThreshold(lvlInfo) ls.setRootThreshold(lvlInfo)
@ -1257,25 +1015,6 @@ 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())
@ -1374,8 +1113,6 @@ 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)
@ -1384,107 +1121,3 @@ 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,4 +1,4 @@
import std/[json, options, strutils] import std/[json, options]
from logging import Level from logging import Level
import ../namespaced_logging import ../namespaced_logging
@ -10,7 +10,7 @@ export
LogMessage, LogMessage,
ConsoleLogAppender, ConsoleLogAppender,
CustomLogAppender, CustomLogAppender,
CustomLogAppenderFunc, CustomLogAppenderFunction,
FileLogAppender, FileLogAppender,
# Procs/Funcs # Procs/Funcs
@ -18,203 +18,81 @@ export
initConsoleLogAppender, initConsoleLogAppender,
initCustomLogAppender, initCustomLogAppender,
initFileLogAppender, initFileLogAppender,
formatJsonStructuredLog, formatJsonStructuredLog
useForAutoconfiguredLogging
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) = proc setRootLoggingThreshold*(lvl: Level) =
setRootThreshold(getAutoconfiguredLogService(), lvl) setRootThreshold(getThreadLocalLogServiceRef(), lvl)
proc setLoggingThreshold*(scope: string, lvl: Level) = proc setLoggingThreshold*(scope: string, lvl: Level) =
setThreshold(getAutoconfiguredLogService(), scope, lvl) setThreshold(getThreadLocalLogServiceRef(), scope, lvl)
proc addLogAppender*(appender: LogAppender) = proc addLogAppender*(appender: LogAppender) =
addAppender(getAutoconfiguredLogService(), appender) addAppender(getThreadLocalLogServiceRef(), appender)
proc clearLogAppenders*() =
clearAppenders(getAutoconfiguredLogService())
proc getLogger*(scope: string, lvl: Option[Level] = none[Level]()): Logger = proc getLogger*(scope: string, lvl: Option[Level] = none[Level]()): Logger =
getLogger(getAutoconfiguredLogService(), scope, lvl) getLogger(getThreadLocalLogServiceRef(), scope, lvl)
template log*(lm: LogMessage) = log(getAutoconfiguredLogger(), lm) proc log*(lvl: Level, msg: string) = getDefaultLogger().log(lvl, msg)
proc log*(lvl: Level, msg: JsonNode) = getDefaultLogger().log(lvl, msg)
template log*(lvl: Level, msg: untyped) = log(getAutoconfiguredLogger(), lvl, msg) proc log*(lvl: Level, error: ref Exception, msg: string) =
getDefaultLogger().log(lvl, error, 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 debug*[T](msg: T) = log(lvlDebug, msg)
template info*[T](msg: T) = log(lvlInfo, msg) template info*[T](msg: T) = log(lvlInfo, msg)
template notice*[T](msg: T) = log(lvlNotice, msg) template notice*[T](msg: T) = log(lvlNotice, msg)
template warn*[T](msg: T) = log(lvlWarn, msg) template warn*[T](msg: T) = log(lvlWarn, msg)
template error*[T](msg: T) = log(lvlError, msg) template error*[T](msg: T) = log(lvlError, msg)
template error*[T](error: ref Exception, msg: T) = log(lvlError, error, msg) template error*(error: ref Exception, msg: string) = log(lvlError, error, msg)
template fatal*[T](msg: T) = log(lvlFatal, msg) template fatal*[T](msg: T) = log(lvlFatal, msg)
template fatal*[T](error: ref Exception, msg: T) = log(lvlFatal, error, msg) template fatal*(error: ref Exception, msg: string) = log(lvlFatal, error, msg)
when isMainModule: when isMainModule:
import std/unittest import std/unittest
import ./testutil import ./testutil
suite "Automatic Global Setup": suite "Autoconfigured Logging":
setup: setup:
clearLogAppenders() globalLogServiceRef[] = initLogService()
let loggedMsgs = initLoggedMessages() let loggedMessages = initLoggedMessages()
addLogAppender(initTestLogAppender(loggedMsgs)) let testAppender = initTestLogAppender(loggedMessages)
# 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": test "simple no-config logging":
addLogAppender(testAppender)
info("test message") info("test message")
let lm = loggedMsgs.get() let lm = loggedMessages.get()
check: check:
lm.len == 1 lm.len == 1
lm[0].level == lvlInfo lm[0].level == lvlInfo
lm[0].message == "test message" 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