Define explicit ownership for custom appenders and formatters

Add SharedLogState for synchronized value data and factories for
independently owned appender state and captured formatter closures.
Reject unsafe configuration state, update the test collector to value
snapshots, and document the breaking APIs in version 3.0.0.

Expand ORC ownership and sanitizer coverage and reject stale test
executables.
This commit is contained in:
jdb committed 2026-09-28 14:28:24 -05:00
1 parent 851caef706
commit b2d0e9dade
11 files changed
+808 -110

No files matched your search

+170
View File
@@ -0,0 +1,170 @@
import std/[atomics, locks, options, tables, unittest]
import ../src/namespaced_logging
type
UnsafeState = ref object
value: int
NestedUnsafeState = object
values: seq[UnsafeState]
VariantUnsafeState = object
case hasReference: bool
of true: reference: UnsafeState
of false: value: int
EmbeddedLock = object
lock: Lock
Slot = enum firstSlot, secondSlot
proc unsafeCallback(state: var UnsafeState, msg: LogMessage) {.gcsafe, noconv.} = discard
proc nestedCallback(state: var NestedUnsafeState, msg: LogMessage) {.gcsafe, noconv.} = discard
proc makePrefix(prefix: string): StatefulLogFormatterFunc =
result = proc(msg: LogMessage): string {.gcsafe.} = prefix & msg.msg
static:
doAssert not compiles(initSharedLogState(UnsafeState()))
doAssert not compiles(initSharedLogState(NestedUnsafeState()))
doAssert not compiles(initSharedLogState(VariantUnsafeState()))
doAssert not compiles(initSharedLogState(some(UnsafeState())))
doAssert not compiles(initCustomLogAppender(UnsafeState(), unsafeCallback))
doAssert not compiles(initCustomLogAppender(NestedUnsafeState(), nestedCallback))
doAssert not compiles(initConsoleLogAppender(formatter = makePrefix("unsafe")))
doAssert not compiles(initSharedLogState(makePrefix("unsafe")))
doAssert not compiles(initSharedLogState(default(Lock)))
doAssert not compiles(initSharedLogState(default(Atomic[int])))
doAssert not compiles(initSharedLogState(default(EmbeddedLock)))
doAssert compiles(initSharedLogState(default(array[Slot, string])))
doAssert compiles(initSharedLogState(initTable[string, seq[string]]()))
proc counterFormatter(prefix: string): StatefulLogFormatterFunc =
var count = 0
result = proc(msg: LogMessage): string {.gcsafe.} =
inc count
prefix & $count & ": " & msg.msg
type
ValueCounter = object
count: int
output: SharedLogState[seq[int]]
LocalCounter = ref object
count: int
output: SharedLogState[seq[int]]
proc initCounter(output: SharedLogState[seq[int]]): LocalCounter =
LocalCounter(output: output)
proc recordCounter(state: var LocalCounter, msg: LogMessage) {.gcsafe, noconv.} =
inc state.count
let count = state.count
state.output.withState(proc(values: var seq[int]) = values.add(count))
proc recordCounter(state: var ValueCounter, msg: LogMessage) {.gcsafe, noconv.} =
inc state.count
let count = state.count
state.output.withState(proc(values: var seq[int]) = values.add(count))
proc ignoreError(err: ref Exception, msg: string) {.gcsafe, nimcall.} = discard
suite "Custom state and formatter ownership":
test "shared value snapshots are independent and updates are explicit":
let state = initSharedLogState(@[@["original"]])
var first = state.snapshot()
first[0][0] = "changed"
check state.snapshot() == @[@["original"]]
state.withState(proc(value: var seq[seq[string]]) = value[0].add("second"))
check state.snapshot() == @[@["original", "second"]]
test "callback exceptions release the shared state lock":
let state = initSharedLogState(0)
expect ValueError:
state.withState(proc(value: var int) =
inc value
raise newException(ValueError, "injected failure"))
check state.snapshot() == 1
state.withState(proc(value: var int) = inc value)
check state.snapshot() == 2
test "a callback can release its caller's last handle":
var state = initSharedLogState(@["alive"])
state.withState(proc(value: var seq[string]) =
state = default(typeof(state))
value.add("still alive")
check value == @["alive", "still alive"])
expect ValueError: discard state.snapshot()
test "empty and moved handles reject access":
var original = initSharedLogState(42)
let moved = move(original)
check moved.snapshot() == 42
expect ValueError: discard original.snapshot()
expect ValueError: original.withState(proc(value: var int) = discard)
test "appender factories retain local state and reset it on clone":
let output = initSharedLogState(newSeq[int]())
var service = initLogService()
service.addAppender(initFactoryCustomLogAppender(output, initCounter, recordCounter))
let first = service.threadLocalRef().getLogger("first")
let second = service.threadLocalRef().getLogger("second")
first.info("one")
first.info("two")
second.info("one")
check output.snapshot() == @[1, 2, 1]
test "value appender state is mutable and independently copied":
let output = initSharedLogState(newSeq[int]())
var service = initLogService()
service.addAppender(initCustomLogAppender(ValueCounter(output: output), recordCounter))
let first = service.threadLocalRef().getLogger("first")
let second = service.threadLocalRef().getLogger("second")
first.info("one")
first.info("two")
second.info("one")
check output.snapshot() == @[1, 2, 1]
test "a failed appender factory is retried on the next message":
let calls = initSharedLogState(0)
proc factory(calls: SharedLogState[int]): UnsafeState =
calls.withState(proc(count: var int) = inc count)
if calls.snapshot() == 1:
raise newException(ValueError, "injected failure")
UnsafeState()
var service = initLogService(errorHandler = ignoreError)
service.addAppender(initFactoryCustomLogAppender(calls, factory, unsafeCallback))
let logger = service.threadLocalRef().getLogger("worker")
logger.info("first attempt")
logger.info("retry")
logger.info("reuse")
check calls.snapshot() == 2
test "formatter clones create independent captured state":
let original = initLogFormatter("count ", counterFormatter)
let message = LogMessage(msg: "hello")
check original.format(message) == "count 1: hello"
let copied = clone(original)
check original.format(message) == "count 2: hello"
check copied.format(message) == "count 1: hello"
test "every built-in appender clones its formatter configuration":
let formatter = initLogFormatter("count ", counterFormatter)
let message = LogMessage(msg: "hello")
let console = initConsoleLogAppender(formatter)
let file = initFileLogAppender("unused.log", formatter)
let standard = initStdLoggingAppender(formatter = formatter)
check console.formatter.format(message) == "count 1: hello"
check file.formatter.format(message) == "count 1: hello"
check standard.formatter.format(message) == "count 1: hello"
check console.formatter.format(message) == "count 2: hello"
check ConsoleLogAppender(clone(console)).formatter.format(message) == "count 1: hello"
check FileLogAppender(clone(file)).formatter.format(message) == "count 1: hello"
check StdLoggingAppender(clone(standard)).formatter.format(message) == "count 1: hello"
test "a failed formatter factory is retried":
let calls = initSharedLogState(0)
proc factory(calls: SharedLogState[int]): StatefulLogFormatterFunc =
calls.withState(proc(count: var int) = inc count)
if calls.snapshot() == 1:
raise newException(ValueError, "injected failure")
makePrefix("ready: ")
let formatter = initLogFormatter(calls, factory)
expect ValueError: discard formatter.format(LogMessage(msg: "hello"))
check formatter.format(LogMessage(msg: "hello")) == "ready: hello"
check formatter.format(LogMessage(msg: "again")) == "ready: again"
check calls.snapshot() == 2