Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
3e7f3d4c1e |
610
README.md
610
README.md
@ -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
|
|
||||||
|
@ -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"
|
|
||||||
|
@ -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")
|
|
||||||
|
@ -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
|
|
||||||
|
Reference in New Issue
Block a user