Move autoconfiguration implementation into the main module.

Autoconfiguration implementation really needs access to internal fields
and data structures to work properly.

Additionally introduces the concept of GlobalLogService takeover
internally, which allows existing LogService instances to become aware
of a new GlobalLogService. This is needed for
`useForAutoconfiguredLogging` to work as one would naturally expect,
where Loggers that may have already been created (explicitly or
implicitly) by library or third-party code are kept up to date when the
application explicitly configures logging.
This commit is contained in:
2025-07-07 16:29:58 -05:00
parent 269cc81c82
commit c22e7edd5d
2 changed files with 158 additions and 46 deletions

View File

@ -21,6 +21,12 @@ type
errorHandler: ErrorHandlerFunc
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
@ -123,6 +129,7 @@ type
formatter*: LogMessageFormatter
absPath*: Path
const UninitializedConfigVersion = low(int)
let JNULL = newJNull()
@ -232,6 +239,12 @@ proc ensureFreshness*(ls: var LogService) =
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:
ls.configVersion = ls.global.configVersion.load
@ -245,6 +258,28 @@ proc ensureFreshness*(ls: var LogService) =
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*(
rootLevel = lvlAll,
errorHandler = defaultErrorHandlerFunc): LogService =
@ -262,24 +297,12 @@ proc initLogService*(
## configure thresholds, and create loggers. The ref returned by this
## 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.
let global = GlobalLogService()
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[]
let global = initGlobalLogService(rootLevel, errorHandler)
result = initLogService(global)
proc threadLocalRef*(ls: LogService): ThreadLocalLogService =
result = new(LogService)
new result
result[] = ls
@ -796,6 +819,114 @@ method appendLogMessage(
"unable to append to FileLogAppender")
# -----------------------------------------------------------------------------
# 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()
when isMainModule:

View File

@ -1,4 +1,4 @@
import std/[json, options]
import std/[json, options, strutils]
from logging import Level
import ../namespaced_logging
@ -18,48 +18,29 @@ export
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
formatJsonStructuredLog,
useForAutoconfiguredLogging
proc setRootLoggingThreshold*(lvl: Level) =
setRootThreshold(getThreadLocalLogServiceRef(), lvl)
setRootThreshold(getAutoconfiguredLogService(), lvl)
proc setLoggingThreshold*(scope: string, lvl: Level) =
setThreshold(getThreadLocalLogServiceRef(), scope, lvl)
setThreshold(getAutoconfiguredLogService(), scope, lvl)
proc addLogAppender*(appender: LogAppender) =
addAppender(getThreadLocalLogServiceRef(), appender)
addAppender(getAutoconfiguredLogService(), appender)
proc clearLogAppenders*() =
clearAppenders(getAutoconfiguredLogService())
proc getLogger*(scope: string, lvl: Option[Level] = none[Level]()): Logger =
getLogger(getThreadLocalLogServiceRef(), scope, lvl)
getLogger(getAutoconfiguredLogService(), scope, lvl)
template log*(lm: LogMessage) = log(getAutoconfiguredLogger(), lm)