Compare commits
12 Commits
1884e07378
...
main
Author | SHA1 | Date | |
---|---|---|---|
15af03e5c5 | |||
7e1671b26e | |||
f0f0084cfd | |||
bff544ab89 | |||
3178c50936 | |||
1b598fb445 | |||
101ac8d869 | |||
a4464c7275 | |||
49755fa2af | |||
b2d43d1c6d | |||
c22e7edd5d | |||
269cc81c82 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ nimcache/
|
||||
tests/tests
|
||||
src/namespaced_logging.out
|
||||
src/namespaced_logging/autoconfigured
|
||||
.worktrees
|
||||
|
114
README.md
114
README.md
@@ -8,7 +8,7 @@ four main motivating features:
|
||||
- Safe and straightforward to use in multi-threaded applications.
|
||||
- Native support for structured logging.
|
||||
- Simple, autoconfigured usage pattern reminiscent of the
|
||||
[std/logging][std-logging] interface.
|
||||
[std/logging][std-logging] interface (*not yet implemented*)
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -21,38 +21,6 @@ nimble install https://github.com/jdbernard/nim-namespaced-logging
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Simple, Autoconfigured Setup
|
||||
```nim
|
||||
import namespaced_logging/autoconfigured
|
||||
|
||||
# Zero configuration of the LogService required, appender/logger configuration
|
||||
# is immediately available
|
||||
addLogAppender(initConsoleLogAppender())
|
||||
info("Application started")
|
||||
|
||||
# Set global threshold
|
||||
setRootLoggingThreshold(lvlWarn)
|
||||
|
||||
# Namespaced loggers, thresholds, and appenders supported
|
||||
addLogAppender(initFileLogAppender(
|
||||
filePath = "/var/log/app_db.log",
|
||||
formatter = formatJsonStructuredLog, # provided in namespaced_logging
|
||||
namespace = "app/db",
|
||||
threshold = lvlInfo))
|
||||
|
||||
# in DB code
|
||||
let dbLogger = getLogger("app/db/queryplanner")
|
||||
dbLogger.debug("Beginning query plan...")
|
||||
|
||||
# native support for structured logs (import std/json)
|
||||
dbLogger.debug(%*{
|
||||
"method": "parseParams",
|
||||
"message": "unrecognized param type",
|
||||
"invalidType": $params[idx].type,
|
||||
"metadata": %(params.meta)
|
||||
} )
|
||||
```
|
||||
|
||||
### Manual Configuration
|
||||
```nim
|
||||
import namespaced_logging
|
||||
@@ -72,34 +40,6 @@ let apiLogger = localLogSvc.getLogger("api")
|
||||
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
|
||||
@@ -225,6 +165,26 @@ 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.
|
||||
@@ -244,7 +204,7 @@ 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.
|
||||
> [!CAUTION] The `state` data type must support copy semantics on assignment.
|
||||
> It is possible to pass a `ref` to `state` and/or data structures that include
|
||||
> `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
|
||||
@@ -263,26 +223,17 @@ behave more intuitively in a multi-threaded environment than
|
||||
true in environments where the logging setup code may be separated from the
|
||||
thread-management code (in an HTTP server, for example).
|
||||
|
||||
As described in the [Getting Started](#getting-started) section, you can use
|
||||
the `namespaced_logging/autoconfigured` import to use a simplified interface
|
||||
that more closely matches the contract of [std/logging][std-logging]. In this
|
||||
case all thread and state management is done for you. The only limitation is
|
||||
that you cannot create multiple global *LogService* instances. In practice this
|
||||
is an uncommon need.
|
||||
The *LogService* object is the main entry point for the logging system and
|
||||
should be initialized on the main thread. The *LogService* contains a reference
|
||||
to the "source of truth" for logging configuration and is safe to be shared
|
||||
between all threads.
|
||||
|
||||
If you do need or want the flexibility to manage the state yourself, import
|
||||
`namespaced_logging` directly. In this case, the thread which initialized
|
||||
*LogService* must also be the longest-living thread that uses that *LogService*
|
||||
instance. If the initializing thread terminates or the *LogService* object in
|
||||
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
|
||||
point for the logging system and should be initialized on the main thread. The
|
||||
*LogService* contains a reference to the "source of truth" for logging
|
||||
configuration and is safe to be shared between all threads.
|
||||
The thread which initializes a *LogService* must also be the longest-living
|
||||
thread that uses that *LogService* instance. If the initializing thread
|
||||
terminates or the *LogService* object in 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).
|
||||
|
||||
Individual threads should use the *threadLocalRef* proc to obtain a
|
||||
*ThreadLocalLogService* reference that can be used to create *Logger* objects.
|
||||
@@ -488,6 +439,7 @@ logService.setErrorHandler(silentErrorHandler)
|
||||
### Best Practices
|
||||
|
||||
#### Provide Fallbacks
|
||||
|
||||
```nim
|
||||
proc robustErrorHandler(err: ref Exception, msg: string) {.gcsafe, nimcall.} =
|
||||
# Primary: Send to monitoring system
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Package
|
||||
|
||||
version = "1.1.0"
|
||||
version = "2.1.2"
|
||||
author = "Jonathan Bernard"
|
||||
description = "Wrapper around std/logging to provide namespaced logging."
|
||||
license = "MIT"
|
||||
@@ -13,3 +13,7 @@ requires @["nim >= 2.2.0", "zero_functional"]
|
||||
|
||||
# from https://git.jdb-software.com/jdb/nim-packages
|
||||
requires "timeutils"
|
||||
|
||||
task test, "Run unittests for the package.":
|
||||
exec "nimble c src/namespaced_logging.nim"
|
||||
exec "src/namespaced_logging.out"
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,98 +0,0 @@
|
||||
import std/[json, options]
|
||||
from logging import Level
|
||||
import ../namespaced_logging
|
||||
|
||||
export
|
||||
# Types
|
||||
Level,
|
||||
Logger,
|
||||
LogAppender,
|
||||
LogMessage,
|
||||
ConsoleLogAppender,
|
||||
CustomLogAppender,
|
||||
CustomLogAppenderFunction,
|
||||
FileLogAppender,
|
||||
|
||||
# Procs/Funcs
|
||||
`%`,
|
||||
initConsoleLogAppender,
|
||||
initCustomLogAppender,
|
||||
initFileLogAppender,
|
||||
formatJsonStructuredLog
|
||||
|
||||
var globalLogServiceRef: ThreadLocalLogService = new(LogService)
|
||||
globalLogServiceRef[] = initLogService()
|
||||
|
||||
var threadLocalLogServiceRef {.threadvar.}: ThreadLocalLogService
|
||||
var defaultLogger {.threadvar.}: Option[Logger]
|
||||
|
||||
|
||||
proc getThreadLocalLogServiceRef(): ThreadLocalLogService {.inline.} =
|
||||
if threadLocalLogServiceRef.isNil:
|
||||
threadLocalLogServiceRef = new(LogService)
|
||||
threadLocalLogServiceRef[] = globalLogServiceRef[]
|
||||
|
||||
return threadLocalLogServiceRef
|
||||
|
||||
proc getDefaultLogger(): Logger {.inline.} =
|
||||
|
||||
if defaultLogger.isNone:
|
||||
defaultLogger = some(getThreadLocalLogServiceRef().getLogger(""))
|
||||
|
||||
return defaultLogger.get
|
||||
|
||||
|
||||
proc useForAutoconfiguredLogging*(ls: LogService) =
|
||||
globalLogServiceRef[] = ls
|
||||
|
||||
|
||||
proc setRootLoggingThreshold*(lvl: Level) =
|
||||
setRootThreshold(getThreadLocalLogServiceRef(), lvl)
|
||||
|
||||
|
||||
proc setLoggingThreshold*(scope: string, lvl: Level) =
|
||||
setThreshold(getThreadLocalLogServiceRef(), scope, lvl)
|
||||
|
||||
|
||||
proc addLogAppender*(appender: LogAppender) =
|
||||
addAppender(getThreadLocalLogServiceRef(), appender)
|
||||
|
||||
|
||||
proc getLogger*(scope: string, lvl: Option[Level] = none[Level]()): Logger =
|
||||
getLogger(getThreadLocalLogServiceRef(), scope, lvl)
|
||||
|
||||
|
||||
proc log*(lvl: Level, msg: string) = getDefaultLogger().log(lvl, msg)
|
||||
proc log*(lvl: Level, msg: JsonNode) = getDefaultLogger().log(lvl, msg)
|
||||
|
||||
proc log*(lvl: Level, error: ref Exception, msg: string) =
|
||||
getDefaultLogger().log(lvl, error, msg)
|
||||
|
||||
template debug*[T](msg: T) = log(lvlDebug, msg)
|
||||
template info*[T](msg: T) = log(lvlInfo, msg)
|
||||
template notice*[T](msg: T) = log(lvlNotice, msg)
|
||||
template warn*[T](msg: T) = log(lvlWarn, msg)
|
||||
template error*[T](msg: T) = log(lvlError, msg)
|
||||
template error*(error: ref Exception, msg: string) = log(lvlError, error, msg)
|
||||
template fatal*[T](msg: T) = log(lvlFatal, msg)
|
||||
template fatal*(error: ref Exception, msg: string) = log(lvlFatal, error, msg)
|
||||
|
||||
when isMainModule:
|
||||
import std/unittest
|
||||
import ./testutil
|
||||
|
||||
suite "Autoconfigured Logging":
|
||||
setup:
|
||||
globalLogServiceRef[] = initLogService()
|
||||
let loggedMessages = initLoggedMessages()
|
||||
let testAppender = initTestLogAppender(loggedMessages)
|
||||
|
||||
test "simple no-config logging":
|
||||
addLogAppender(testAppender)
|
||||
info("test message")
|
||||
|
||||
let lm = loggedMessages.get()
|
||||
check:
|
||||
lm.len == 1
|
||||
lm[0].level == lvlInfo
|
||||
lm[0].message == "test message"
|
Reference in New Issue
Block a user