Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
f80e5807db | |||
e0dba8125c | |||
0eb0d33573 | |||
92c2dec54d | |||
ab20a30434 | |||
f3dbac0896 | |||
c0f818ef30 | |||
8245cfbdf7 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
|||||||
.*.sw?
|
.*.sw?
|
||||||
nimcache/
|
nimcache/
|
||||||
|
tests/tests
|
||||||
|
src/namespaced_logging.out
|
||||||
|
src/namespaced_logging/autoconfigured
|
||||||
|
182
README.md
Normal file
182
README.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# Namespaced Logging for Nim
|
||||||
|
|
||||||
|
`namespaced_logging` provides a logging framework similar to [log4j][] or
|
||||||
|
[logback][] for Nim. It has three main motivating features:
|
||||||
|
- Hierarchical, namespaced logging
|
||||||
|
- Safe and straightforward to use in multi-threaded applications.
|
||||||
|
- Native support for structured logging (old-style string logging is also
|
||||||
|
supported).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Install the package from nimble:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nimble install namespaced_logging
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in your application, you can use the logging system like so:
|
||||||
|
|
||||||
|
```nim
|
||||||
|
import namespaced_logging
|
||||||
|
|
||||||
|
# On the main thread
|
||||||
|
let logService = initLogService()
|
||||||
|
logService.addAppender(initConsoleAppender(LogLevel.INFO))
|
||||||
|
|
||||||
|
# On any thread, including the main thread
|
||||||
|
let logger = logService.getLogger("app/service/example")
|
||||||
|
logger.info("Log from the example service")
|
||||||
|
|
||||||
|
# Only get logs at the WARN or higher level from the database module
|
||||||
|
let logger = logService.getLogger("app/database", threshold = some(Level.lvlWarn))
|
||||||
|
logger.error("Database connection failed")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loggers and Appenders
|
||||||
|
|
||||||
|
The logging system is composed of two main components: loggers and appenders.
|
||||||
|
Loggers are used to create log events, which are then passed to the appenders.
|
||||||
|
Appenders take log events and write them to some destination, such as the
|
||||||
|
console, a file, or a network socket. Appenders also have a logging level
|
||||||
|
threshold, which determines which log events are acted upon by the appender,
|
||||||
|
and, optionally, a namespace filter, which determines from which loggers the
|
||||||
|
appender accepts log events.
|
||||||
|
|
||||||
|
### Heirarchical Logging and Namespaces
|
||||||
|
|
||||||
|
Loggers are organized hierarchically, with the hierarchy defined by the logger
|
||||||
|
name. A logger with the name `app/service/example` is a child of the logger
|
||||||
|
with the name `app/service`. By default, appenders accept log events from all
|
||||||
|
loggers, but this can be restricted by setting a namespace filter on the
|
||||||
|
appender. An appender with a namespace set will accept log events from all
|
||||||
|
loggers with names that start with the namespace. For example, an appender with
|
||||||
|
the namespace `app` will accept log events from the loggers `app`,
|
||||||
|
`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 logger. Any logger can have an explicit logging level set, but if it does
|
||||||
|
not, the effective logging level is inherited from ancestor loggers upwards in
|
||||||
|
the logger heirarchy. This pattern is explained in detail in the [logback
|
||||||
|
documentation][effective logging level] and applies in the same manner to
|
||||||
|
loggers in this library.
|
||||||
|
|
||||||
|
|
||||||
|
## Notes on Use in Multi-Threaded Applications
|
||||||
|
|
||||||
|
The loggers and appenders in this library are thread-safe and behaves more
|
||||||
|
intuitively in a multi-threaded environment than `std/logging`, particularly in
|
||||||
|
environments where the logging setup code may be separated from the
|
||||||
|
thread-management code (in an HTTP server, for example).
|
||||||
|
|
||||||
|
The *LogService* object is the main entry point for the logging system and
|
||||||
|
should be initialized on the main thread. The *LogService* contains the "source
|
||||||
|
of truth" for logging configuration and is shared between all threads.
|
||||||
|
Internally all access to the *LogService* is protected by a mutex.
|
||||||
|
|
||||||
|
Logging can be very noisy and if the *LogService* needed to be consulted for
|
||||||
|
every log event, it could easily become a performance bottleneck. To avoid
|
||||||
|
this, the *getLogger* procedure makes a thread-local copy of the logging system
|
||||||
|
configuration (loggers defined and appenders attached).
|
||||||
|
|
||||||
|
**Note** that this means that the thread-local cache of the logging system
|
||||||
|
configuration can become stale if the logging system configuration is changed
|
||||||
|
after the thread-local copy is made (if another appender is added, for
|
||||||
|
example). This is a trade-off to avoid the performance penalty of consulting
|
||||||
|
the *LogService* for every log event.
|
||||||
|
|
||||||
|
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
|
||||||
|
the logging system itself is responsible for making sure appenders are
|
||||||
|
configured for every thread where loggers are used, even if the thread
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Custom Appender Implementations
|
||||||
|
|
||||||
|
Due to the thread-safety of the logging system, there are a few additional
|
||||||
|
considerations when implementing custom appenders. The *LogAppender* abstract
|
||||||
|
class is the base class for all appenders. To implement a custom appender, two
|
||||||
|
methods must be implemented:
|
||||||
|
|
||||||
|
### `appendLogMessage`
|
||||||
|
|
||||||
|
```nim
|
||||||
|
method appendLogMessage*(appender: CustomLogAppender, msg: LogMessage): void {.base, gcsafe.}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the primary appender implementation that takes a LogMessage and
|
||||||
|
writes it to the appender's destination. As the signature suggests, the
|
||||||
|
implementation must be GC-safe. As a multi-method, the *CustomLogAppender* type
|
||||||
|
should be replaced by the actual name of your custom appender.
|
||||||
|
|
||||||
|
Because the *LogAppender* uses multi-methods for dynamic dispatch, the
|
||||||
|
custom appender class must also be a `ref` type.
|
||||||
|
|
||||||
|
### `initThreadCopy`
|
||||||
|
|
||||||
|
```nim
|
||||||
|
method initThreadCopy*(app: LogAppender): LogAppender {.base, gcsafe.}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The `initThreadCopy` implementations for the built-in *ConsoleLogAppender* and
|
||||||
|
*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.
|
||||||
|
|
||||||
|
### Example Custom Appender
|
||||||
|
|
||||||
|
The following defines a simple custom appender that writes log messages to a
|
||||||
|
database table. It uses the [waterpark][] connection pooling library to manage
|
||||||
|
database connections as waterpark is also thread-safe and makes implementation
|
||||||
|
straight-forward.
|
||||||
|
|
||||||
|
```nim
|
||||||
|
import db_connectors/db_postgres
|
||||||
|
import namespaced_logging, waterpark, waterpark/db_postgres
|
||||||
|
|
||||||
|
type DbLogAppender = ref object of LogAppender
|
||||||
|
dbPool: PostgresPool
|
||||||
|
|
||||||
|
let dbPool: PostgresPool = newPostgresPool(10, "", "", "", connectionString)
|
||||||
|
|
||||||
|
method initThreadCopy*(app: LogAppender): LogAppender =
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[log4j]: https://logging.apache.org/log4j/2.x/
|
||||||
|
[logback]: https://logback.qos.ch/
|
||||||
|
[effective logging level]: https://logback.qos.ch/manual/architecture.html#effectiveLevel
|
@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.1.0"
|
version = "1.1.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"
|
||||||
@ -9,4 +9,7 @@ srcDir = "src"
|
|||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
requires "nim >= 1.0.4"
|
requires @["nim >= 2.2.0", "zero_functional"]
|
||||||
|
|
||||||
|
# from https://git.jdb-software.com/jdb/nim-packages
|
||||||
|
requires "timeutils"
|
||||||
|
File diff suppressed because it is too large
Load Diff
98
src/namespaced_logging/autoconfigured.nim
Normal file
98
src/namespaced_logging/autoconfigured.nim
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import std/[json, options]
|
||||||
|
from logging import Level
|
||||||
|
import ../namespaced_logging
|
||||||
|
|
||||||
|
export
|
||||||
|
# Types
|
||||||
|
Level,
|
||||||
|
Logger,
|
||||||
|
LogAppender,
|
||||||
|
LogMessage,
|
||||||
|
ConsoleLogAppender,
|
||||||
|
CustomLogAppender,
|
||||||
|
CustomLogAppenderFunction,
|
||||||
|
FileLogAppender,
|
||||||
|
|
||||||
|
# Procs/Funcs
|
||||||
|
`%`,
|
||||||
|
initConsoleLogAppender,
|
||||||
|
initCustomLogAppender,
|
||||||
|
initFileLogAppender,
|
||||||
|
formatJsonStructuredLog
|
||||||
|
|
||||||
|
var globalLogServiceRef: ThreadLocalLogService = new(LogService)
|
||||||
|
globalLogServiceRef[] = initLogService()
|
||||||
|
|
||||||
|
var threadLocalLogServiceRef {.threadvar.}: ThreadLocalLogService
|
||||||
|
var defaultLogger {.threadvar.}: Option[Logger]
|
||||||
|
|
||||||
|
|
||||||
|
proc getThreadLocalLogServiceRef(): ThreadLocalLogService {.inline.} =
|
||||||
|
if threadLocalLogServiceRef.isNil:
|
||||||
|
threadLocalLogServiceRef = new(LogService)
|
||||||
|
threadLocalLogServiceRef[] = globalLogServiceRef[]
|
||||||
|
|
||||||
|
return threadLocalLogServiceRef
|
||||||
|
|
||||||
|
proc getDefaultLogger(): Logger {.inline.} =
|
||||||
|
|
||||||
|
if defaultLogger.isNone:
|
||||||
|
defaultLogger = some(getThreadLocalLogServiceRef().getLogger(""))
|
||||||
|
|
||||||
|
return defaultLogger.get
|
||||||
|
|
||||||
|
|
||||||
|
proc useForAutoconfiguredLogging*(ls: LogService) =
|
||||||
|
globalLogServiceRef[] = ls
|
||||||
|
|
||||||
|
|
||||||
|
proc setRootLoggingThreshold*(lvl: Level) =
|
||||||
|
setRootThreshold(getThreadLocalLogServiceRef(), lvl)
|
||||||
|
|
||||||
|
|
||||||
|
proc setLoggingThreshold*(scope: string, lvl: Level) =
|
||||||
|
setThreshold(getThreadLocalLogServiceRef(), scope, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
proc addLogAppender*(appender: LogAppender) =
|
||||||
|
addAppender(getThreadLocalLogServiceRef(), appender)
|
||||||
|
|
||||||
|
|
||||||
|
proc getLogger*(scope: string, lvl: Option[Level] = none[Level]()): Logger =
|
||||||
|
getLogger(getThreadLocalLogServiceRef(), scope, lvl)
|
||||||
|
|
||||||
|
|
||||||
|
proc log*(lvl: Level, msg: string) = getDefaultLogger().log(lvl, msg)
|
||||||
|
proc log*(lvl: Level, msg: JsonNode) = getDefaultLogger().log(lvl, msg)
|
||||||
|
|
||||||
|
proc log*(lvl: Level, error: ref Exception, msg: string) =
|
||||||
|
getDefaultLogger().log(lvl, error, msg)
|
||||||
|
|
||||||
|
template debug*[T](msg: T) = log(lvlDebug, msg)
|
||||||
|
template info*[T](msg: T) = log(lvlInfo, msg)
|
||||||
|
template notice*[T](msg: T) = log(lvlNotice, msg)
|
||||||
|
template warn*[T](msg: T) = log(lvlWarn, msg)
|
||||||
|
template error*[T](msg: T) = log(lvlError, msg)
|
||||||
|
template error*(error: ref Exception, msg: string) = log(lvlError, error, msg)
|
||||||
|
template fatal*[T](msg: T) = log(lvlFatal, msg)
|
||||||
|
template fatal*(error: ref Exception, msg: string) = log(lvlFatal, error, msg)
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
import std/unittest
|
||||||
|
import ./testutil
|
||||||
|
|
||||||
|
suite "Autoconfigured Logging":
|
||||||
|
setup:
|
||||||
|
globalLogServiceRef[] = initLogService()
|
||||||
|
let loggedMessages = initLoggedMessages()
|
||||||
|
let testAppender = initTestLogAppender(loggedMessages)
|
||||||
|
|
||||||
|
test "simple no-config logging":
|
||||||
|
addLogAppender(testAppender)
|
||||||
|
info("test message")
|
||||||
|
|
||||||
|
let lm = loggedMessages.get()
|
||||||
|
check:
|
||||||
|
lm.len == 1
|
||||||
|
lm[0].level == lvlInfo
|
||||||
|
lm[0].message == "test message"
|
51
src/namespaced_logging/testutil.nim
Normal file
51
src/namespaced_logging/testutil.nim
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import std/[locks, sequtils, syncio, os, times]
|
||||||
|
from logging import Level
|
||||||
|
from ../namespaced_logging import CustomLogAppender, initCustomLogAppender, LogMessage
|
||||||
|
|
||||||
|
type
|
||||||
|
LoggedMessages* = ref object
|
||||||
|
messages*: seq[LogMessage]
|
||||||
|
lock: Lock
|
||||||
|
|
||||||
|
proc initLoggedMessages*(): LoggedMessages =
|
||||||
|
result = LoggedMessages(messages: @[])
|
||||||
|
initLock(result.lock)
|
||||||
|
|
||||||
|
|
||||||
|
proc add*(lm: LoggedMessages, msg: LogMessage) =
|
||||||
|
withLock lm.lock: lm.messages.add(msg)
|
||||||
|
|
||||||
|
|
||||||
|
proc clear*(lm: LoggedMessages) =
|
||||||
|
withLock lm.lock: lm.messages = @[]
|
||||||
|
|
||||||
|
|
||||||
|
proc get*(lm: LoggedMessages): seq[LogMessage] =
|
||||||
|
withLock lm.lock: return lm.messages.mapIt(it)
|
||||||
|
|
||||||
|
|
||||||
|
proc testLogAppenderProc(state: LoggedMessages, msg: LogMessage) {.gcsafe, noconv.} =
|
||||||
|
state.add(msg)
|
||||||
|
|
||||||
|
proc initTestLogAppender*(
|
||||||
|
lm: LoggedMessages,
|
||||||
|
namespace = "",
|
||||||
|
threshold = lvlAll): CustomLogAppender[LoggedMessages] =
|
||||||
|
|
||||||
|
initCustomLogAppender(
|
||||||
|
state = lm,
|
||||||
|
doLogMessage = testLogAppenderProc,
|
||||||
|
namespace = namespace,
|
||||||
|
threshold = threshold)
|
||||||
|
|
||||||
|
|
||||||
|
proc waitForFileContent*(
|
||||||
|
path: string,
|
||||||
|
expectedLines: int,
|
||||||
|
timeoutMs: int = 1000): seq[string] =
|
||||||
|
let startTime = getTime()
|
||||||
|
while (getTime() - startTime).inMilliseconds < timeoutMs:
|
||||||
|
if fileExists(path):
|
||||||
|
result = readLines(path)
|
||||||
|
if result.len >= expectedLines: break
|
||||||
|
sleep(10)
|
Reference in New Issue
Block a user