Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
f80e5807db | |||
e0dba8125c | |||
0eb0d33573 | |||
92c2dec54d | |||
ab20a30434 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
.*.sw?
|
||||
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
|
||||
|
||||
version = "0.3.1"
|
||||
version = "1.1.0"
|
||||
author = "Jonathan Bernard"
|
||||
description = "Wrapper around std/logging to provide namespaced logging."
|
||||
license = "MIT"
|
||||
@ -9,4 +9,7 @@ srcDir = "src"
|
||||
|
||||
# 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