Files
BitLogger/src-async/BitLoggerAsync_test.mbt
T
Nanaloveyuki 0cbe25a551 πŸ”Š Update 1.0.0
2026-06-27 10:45:36 +08:00

5073 lines
166 KiB
MoonBit

///|
suberror TestFlushError {
TestFlushError(String)
}
///|
async test "shutdown drains pending records" {
inspect(
async_runtime_mode_label(async_runtime_mode()) == "native_worker" ||
async_runtime_mode_label(async_runtime_mode()) == "compatibility",
content="true",
)
let written : Ref[Array[String]] = Ref([])
let flushes : Ref[Int] = Ref(0)
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) { written.val.push(rec.message) }),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
max_batch=4,
linger_ms=10,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Info,
target="async.test",
flush=fn(_) {
flushes.val += 1
1
},
)
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("one")
logger.info("two")
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.has_failed(), content="false")
inspect(logger.pending_count(), content="0")
inspect(
match logger.flush_policy() {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Batch",
)
inspect(written.val.length(), content="2")
inspect(written.val[0], content="one")
inspect(written.val[1], content="two")
inspect(flushes.val, content="1")
}
///|
async test "close clear counts abandoned records as dropped" {
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
),
min_level=@bitlogger.Level::Info,
target="async.clear",
)
logger.info("one")
logger.info("two")
inspect(logger.pending_count(), content="2")
inspect(logger.dropped_count(), content="0")
logger.close(clear=true)
inspect(logger.is_closed(), content="true")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="2")
}
///|
async test "shutdown clear closes without worker startup" {
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
),
min_level=@bitlogger.Level::Info,
target="async.noworker",
)
logger.info("one")
logger.shutdown(clear=true)
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
}
///|
async test "closed blocking logger does not add pending count on later log attempts" {
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
),
min_level=@bitlogger.Level::Info,
target="async.closed.blocking",
)
logger.info("one")
logger.close(clear=true)
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
logger.info("late")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
}
///|
async test "closed logger runtime determines whether later log reaches patch path" {
let patched : Ref[Int] = Ref(0)
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
),
min_level=@bitlogger.Level::Info,
target="async.closed.patch",
).with_patch(fn(rec) {
patched.val += 1
rec
})
logger.info("one")
inspect(patched.val, content="1")
logger.close(clear=true)
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
logger.info("late")
inspect(
patched.val,
content=if async_runtime_supports_background_worker() { "2" } else { "1" },
)
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
}
///|
async test "async logger with_filter composes and leaves base logger unchanged" {
let written_targets : Ref[Array[String]] = Ref([])
let written_messages : Ref[Array[String]] = Ref([])
let base = async_logger(
@bitlogger.callback_sink(fn(rec) {
written_targets.val.push(rec.target)
written_messages.val.push(rec.message)
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Trace,
target="service",
)
let filtered = base.with_filter(
@bitlogger.all_of([
@bitlogger.target_has_prefix("service.api"),
@bitlogger.level_at_least(@bitlogger.Level::Info),
@bitlogger.message_contains("visible"),
]),
)
@async.with_task_group(group => {
group.spawn_bg(() => filtered.run())
filtered.debug("visible debug")
filtered.info("hidden info")
filtered.child("api").info("visible info")
filtered.shutdown()
})
inspect(written_messages.val.length(), content="1")
inspect(written_messages.val[0], content="visible info")
inspect(written_targets.val[0], content="service.api")
let base_written : Ref[Array[String]] = Ref([])
let untouched = async_logger(
@bitlogger.callback_sink(fn(rec) { base_written.val.push(rec.message) }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="base",
)
ignore(untouched.with_filter(fn(_) { false }))
@async.with_task_group(group => {
group.spawn_bg(() => untouched.run())
untouched.info("base still writes")
untouched.shutdown()
})
inspect(base_written.val.length(), content="1")
inspect(base_written.val[0], content="base still writes")
}
///|
async test "async logger with_patch composes before filter and does not mutate base logger" {
let captured_target : Ref[String] = Ref("")
let captured_message : Ref[String] = Ref("")
let captured_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) {
captured_target.val = rec.target
captured_message.val = rec.message
captured_fields.val = rec.fields
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="auth",
)
.with_patch(
@bitlogger.compose_patches([
@bitlogger.set_target("audit.auth"),
@bitlogger.prefix_message("[safe] "),
@bitlogger.redact_field("token"),
@bitlogger.append_fields([@bitlogger.field("service", "bitlogger")]),
]),
)
.with_filter(@bitlogger.target_is("audit.auth"))
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("login", fields=[
@bitlogger.field("token", "secret"),
@bitlogger.field("user", "alice"),
])
logger.shutdown()
})
inspect(captured_target.val, content="audit.auth")
inspect(captured_message.val, content="[safe] login")
inspect(captured_fields.val.length(), content="3")
inspect(captured_fields.val[0].key, content="token")
inspect(captured_fields.val[0].value, content="***")
inspect(captured_fields.val[1].key, content="user")
inspect(captured_fields.val[1].value, content="alice")
inspect(captured_fields.val[2].key, content="service")
inspect(captured_fields.val[2].value, content="bitlogger")
let base_message : Ref[String] = Ref("")
let base_target : Ref[String] = Ref("")
let base_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let base = async_logger(
@bitlogger.callback_sink(fn(rec) {
base_message.val = rec.message
base_target.val = rec.target
base_fields.val = rec.fields
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="auth",
)
ignore(base.with_patch(@bitlogger.prefix_message("[safe] ")))
@async.with_task_group(group => {
group.spawn_bg(() => base.run())
base.info("plain", fields=[@bitlogger.field("token", "secret")])
base.shutdown()
})
inspect(base_target.val, content="auth")
inspect(base_message.val, content="plain")
inspect(base_fields.val.length(), content="1")
inspect(base_fields.val[0].value, content="secret")
}
///|
async test "async logger with_min_level only affects derived logger threshold" {
let base_written : Ref[Array[String]] = Ref([])
let derived_written : Ref[Array[String]] = Ref([])
let base = async_logger(
@bitlogger.callback_sink(fn(rec) { base_written.val.push(rec.message) }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="level.base",
)
let derived = async_logger(
@bitlogger.callback_sink(fn(rec) { derived_written.val.push(rec.message) }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="level.derived",
).with_min_level(@bitlogger.Level::Error)
inspect(base.is_enabled(@bitlogger.Level::Info), content="true")
inspect(derived.is_enabled(@bitlogger.Level::Info), content="false")
inspect(derived.is_enabled(@bitlogger.Level::Error), content="true")
@async.with_task_group(group => {
group.spawn_bg(() => base.run())
group.spawn_bg(() => derived.run())
base.info("base info")
derived.info("derived info skipped")
derived.error("derived error kept")
base.shutdown()
derived.shutdown()
})
inspect(base_written.val.length(), content="1")
inspect(base_written.val[0], content="base info")
inspect(derived_written.val.length(), content="1")
inspect(derived_written.val[0], content="derived error kept")
}
///|
async test "async logger with_target replaces default target without mutating base logger" {
let written_targets : Ref[Array[String]] = Ref([])
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) { written_targets.val.push(rec.target) }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.base",
).with_timestamp()
let retargeted = logger.with_target("async.retarget")
inspect(logger.target, content="async.base")
inspect(retargeted.target, content="async.retarget")
inspect(logger.timestamp, content="true")
inspect(retargeted.timestamp, content="true")
@async.with_task_group(group => {
group.spawn_bg(() => retargeted.run())
retargeted.error("retargeted")
retargeted.shutdown()
})
inspect(written_targets.val.length(), content="1")
inspect(written_targets.val[0], content="async.retarget")
let base_targets : Ref[Array[String]] = Ref([])
let base = async_logger(
@bitlogger.callback_sink(fn(rec) { base_targets.val.push(rec.target) }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.base",
)
ignore(base.with_target("async.retarget"))
@async.with_task_group(group => {
group.spawn_bg(() => base.run())
base.error("base")
base.shutdown()
})
inspect(base_targets.val.length(), content="1")
inspect(base_targets.val[0], content="async.base")
}
///|
async test "async logger with_context_fields prepends shared fields without mutating base logger" {
let written_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) { written_fields.val = rec.fields }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.ctx",
)
let contextual = logger.with_context_fields([
@bitlogger.field("service", "bitlogger"),
@bitlogger.field("scope", "sdk"),
])
inspect(logger.context_fields.length(), content="0")
inspect(contextual.context_fields.length(), content="2")
@async.with_task_group(group => {
group.spawn_bg(() => contextual.run())
contextual.error("ctx", fields=[@bitlogger.field("mode", "test")])
contextual.shutdown()
})
inspect(written_fields.val.length(), content="3")
inspect(written_fields.val[0].key, content="service")
inspect(written_fields.val[0].value, content="bitlogger")
inspect(written_fields.val[1].key, content="scope")
inspect(written_fields.val[1].value, content="sdk")
inspect(written_fields.val[2].key, content="mode")
inspect(written_fields.val[2].value, content="test")
}
///|
async test "async logger child composes target and preserves other state" {
let written_target : Ref[String] = Ref("")
let written_timestamp : Ref[UInt64] = Ref(0UL)
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) {
written_target.val = rec.target
written_timestamp.val = rec.timestamp_ms
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="sdk",
).with_timestamp()
let child = logger.child("worker")
inspect(logger.target, content="sdk")
inspect(child.target, content="sdk.worker")
inspect(child.timestamp, content="true")
inspect(child.is_enabled(@bitlogger.Level::Error), content="true")
inspect(child.is_enabled(@bitlogger.Level::Info), content="false")
@async.with_task_group(group => {
group.spawn_bg(() => child.run())
child.error("child")
child.shutdown()
})
inspect(written_target.val, content="sdk.worker")
inspect(written_timestamp.val > 0UL, content="true")
let root_child = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="",
).child("worker")
inspect(root_child.target, content="worker")
let keep_parent = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="sdk",
).child("")
inspect(keep_parent.target, content="sdk")
}
///|
async test "async logger with_timestamp only affects derived logger timestamp flag" {
let base_timestamp : Ref[UInt64] = Ref(0UL)
let derived_timestamp : Ref[UInt64] = Ref(0UL)
let base = async_logger(
@bitlogger.callback_sink(fn(rec) { base_timestamp.val = rec.timestamp_ms }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.ts.base",
)
let derived = async_logger(
@bitlogger.callback_sink(fn(rec) {
derived_timestamp.val = rec.timestamp_ms
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.ts.derived",
).with_timestamp()
inspect(base.timestamp, content="false")
inspect(derived.timestamp, content="true")
@async.with_task_group(group => {
group.spawn_bg(() => base.run())
group.spawn_bg(() => derived.run())
base.error("base")
derived.error("derived")
base.shutdown()
derived.shutdown()
})
inspect(base_timestamp.val, content="0")
inspect(derived_timestamp.val > 0UL, content="true")
}
///|
async test "async logger log supports per-call target override" {
let written_target : Ref[String] = Ref("")
let written_message : Ref[String] = Ref("")
let written_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) {
written_target.val = rec.target
written_message.val = rec.message
written_fields.val = rec.fields
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.default",
).with_context_fields([@bitlogger.field("service", "bitlogger")])
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
fields=[@bitlogger.field("mode", "test")],
target="async.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.override")
inspect(written_message.val, content="override")
inspect(written_fields.val.length(), content="2")
inspect(written_fields.val[0].key, content="service")
inspect(written_fields.val[0].value, content="bitlogger")
inspect(written_fields.val[1].key, content="mode")
inspect(written_fields.val[1].value, content="test")
let second_target : Ref[String] = Ref("")
let second_message : Ref[String] = Ref("")
let second_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let second = async_logger(
@bitlogger.callback_sink(fn(rec) {
second_target.val = rec.target
second_message.val = rec.message
second_fields.val = rec.fields
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.default",
).with_context_fields([@bitlogger.field("service", "bitlogger")])
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.log(@bitlogger.Level::Error, "default")
second.shutdown()
})
inspect(second_target.val, content="async.default")
inspect(second_message.val, content="default")
inspect(second_fields.val.length(), content="1")
inspect(second_fields.val[0].key, content="service")
}
///|
async test "async logger severity helpers use stored target without override" {
let written_levels : Ref[Array[String]] = Ref([])
let written_targets : Ref[Array[String]] = Ref([])
let written_fields : Ref[Array[Array[@bitlogger.Field]]] = Ref([])
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) {
written_levels.val.push(rec.level.label())
written_targets.val.push(rec.target)
written_fields.val.push(rec.fields)
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="async.helpers",
).with_context_fields([@bitlogger.field("service", "bitlogger")])
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("info", fields=[@bitlogger.field("mode", "info")])
logger.warn("warn", fields=[@bitlogger.field("mode", "warn")])
logger.error("error", fields=[@bitlogger.field("mode", "error")])
logger.shutdown()
})
inspect(written_levels.val.length(), content="3")
inspect(written_levels.val[0], content="INFO")
inspect(written_levels.val[1], content="WARN")
inspect(written_levels.val[2], content="ERROR")
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.helpers")
inspect(written_targets.val[1], content="async.helpers")
inspect(written_targets.val[2], content="async.helpers")
inspect(written_fields.val[0].length(), content="2")
inspect(written_fields.val[1].length(), content="2")
inspect(written_fields.val[2].length(), content="2")
inspect(written_fields.val[0][0].key, content="service")
inspect(written_fields.val[0][1].value, content="info")
inspect(written_fields.val[1][0].key, content="service")
inspect(written_fields.val[1][1].value, content="warn")
inspect(written_fields.val[2][0].key, content="service")
inspect(written_fields.val[2][1].value, content="error")
}
///|
test "async logger config stringify roundtrips stable fields" {
let text = stringify_async_logger_config(
AsyncLoggerConfig::new(
max_pending=8,
overflow=AsyncOverflowPolicy::DropOldest,
max_batch=3,
linger_ms=25,
flush=AsyncFlushPolicy::Batch,
),
)
let config = parse_async_logger_config_text(text)
inspect(config.max_pending, content="8")
inspect(config.max_batch, content="3")
inspect(config.linger_ms, content="25")
inspect(
match config.overflow {
AsyncOverflowPolicy::Blocking => "Blocking"
AsyncOverflowPolicy::DropOldest => "DropOldest"
AsyncOverflowPolicy::DropNewest => "DropNewest"
},
content="DropOldest",
)
inspect(
match config.flush {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Batch",
)
}
///|
test "async logger config parser accepts compatibility aliases" {
let config = parse_async_logger_config_text(
"{\"overflow\":\"DropLatest\",\"flush\":\"None\",\"max_batch\":0,\"linger_ms\":-2}",
)
inspect(config.max_batch, content="1")
inspect(config.linger_ms, content="0")
inspect(
match config.overflow {
AsyncOverflowPolicy::Blocking => "Blocking"
AsyncOverflowPolicy::DropOldest => "DropOldest"
AsyncOverflowPolicy::DropNewest => "DropNewest"
},
content="DropNewest",
)
inspect(
match config.flush {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Never",
)
}
///|
async test "async logger config constructor normalizes batch and linger but preserves pending input" {
let config = AsyncLoggerConfig::new(
max_pending=-3,
overflow=AsyncOverflowPolicy::DropNewest,
max_batch=0,
linger_ms=-5,
flush=AsyncFlushPolicy::Batch,
)
inspect(config.max_pending, content="-3")
inspect(config.max_batch, content="1")
inspect(config.linger_ms, content="0")
inspect(
match config.overflow {
AsyncOverflowPolicy::Blocking => "Blocking"
AsyncOverflowPolicy::DropOldest => "DropOldest"
AsyncOverflowPolicy::DropNewest => "DropNewest"
},
content="DropNewest",
)
inspect(
match config.flush {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Batch",
)
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config~,
min_level=@bitlogger.Level::Info,
target="async.config.normalized",
)
logger.info("one")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
}
///|
test "async build config stringify roundtrips nested logger and async fields" {
let text = stringify_async_logger_build_config(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.roundtrip",
timestamp=true,
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::TextConsole),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropNewest,
max_batch=5,
linger_ms=40,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
let config = parse_async_logger_build_config_text(text)
inspect(config.logger.min_level.label(), content="WARN")
inspect(config.logger.target, content="async.roundtrip")
inspect(config.logger.timestamp, content="true")
inspect(config.async_config.max_pending, content="2")
inspect(config.async_config.max_batch, content="5")
inspect(config.async_config.linger_ms, content="40")
inspect(
match config.async_config.overflow {
AsyncOverflowPolicy::Blocking => "Blocking"
AsyncOverflowPolicy::DropOldest => "DropOldest"
AsyncOverflowPolicy::DropNewest => "DropNewest"
},
content="DropNewest",
)
inspect(
match config.async_config.flush {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
}
///|
test "async build config parser fills omitted sections from defaults" {
let config = parse_async_logger_build_config_text("{}")
inspect(config.logger.min_level.label(), content="INFO")
inspect(config.logger.target, content="")
inspect(config.logger.timestamp, content="false")
inspect(
match config.logger.sink.kind {
@bitlogger.SinkKind::Console => "Console"
@bitlogger.SinkKind::JsonConsole => "JsonConsole"
@bitlogger.SinkKind::TextConsole => "TextConsole"
@bitlogger.SinkKind::File => "File"
},
content="Console",
)
inspect(config.logger.queue is None, content="true")
inspect(config.async_config.max_pending, content="0")
inspect(config.async_config.max_batch, content="1")
inspect(config.async_config.linger_ms, content="0")
inspect(
match config.async_config.overflow {
AsyncOverflowPolicy::Blocking => "Blocking"
AsyncOverflowPolicy::DropOldest => "DropOldest"
AsyncOverflowPolicy::DropNewest => "DropNewest"
},
content="Blocking",
)
inspect(
match config.async_config.flush {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Never",
)
}
///|
test "async json helpers export stable structured shapes" {
let config_json = async_logger_config_to_json(
AsyncLoggerConfig::new(
max_pending=8,
overflow=AsyncOverflowPolicy::DropOldest,
max_batch=3,
linger_ms=25,
flush=AsyncFlushPolicy::Batch,
),
)
let config_obj = config_json.as_object().unwrap()
inspect(
@json_parser.stringify(config_json),
content="{\"max_pending\":8,\"max_batch\":3,\"linger_ms\":25,\"overflow\":\"DropOldest\",\"flush\":\"Batch\"}",
)
inspect(
config_obj.get("max_pending").unwrap().as_number().unwrap().to_int(),
content="8",
)
inspect(
config_obj.get("max_batch").unwrap().as_number().unwrap().to_int(),
content="3",
)
inspect(
config_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(),
content="25",
)
inspect(
config_obj.get("overflow").unwrap().as_string().unwrap(),
content="DropOldest",
)
inspect(
config_obj.get("flush").unwrap().as_string().unwrap(),
content="Batch",
)
let build_json = async_logger_build_config_to_json(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.roundtrip",
timestamp=true,
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::TextConsole),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropNewest,
max_batch=5,
linger_ms=40,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
let build_obj = build_json.as_object().unwrap()
let logger_obj = build_obj.get("logger").unwrap().as_object().unwrap()
let sink_obj = logger_obj.get("sink").unwrap().as_object().unwrap()
let formatter_obj = sink_obj
.get("text_formatter")
.unwrap()
.as_object()
.unwrap()
let async_obj = build_obj.get("async_config").unwrap().as_object().unwrap()
inspect(
logger_obj.get("min_level").unwrap().as_string().unwrap(),
content="WARN",
)
inspect(
logger_obj.get("target").unwrap().as_string().unwrap(),
content="async.roundtrip",
)
inspect(
logger_obj.get("timestamp").unwrap().as_bool().unwrap(),
content="true",
)
inspect(logger_obj.get("queue") is None, content="true")
inspect(
sink_obj.get("kind").unwrap().as_string().unwrap(),
content="text_console",
)
inspect(sink_obj.get("path").unwrap().as_string().unwrap(), content="")
inspect(sink_obj.get("append").unwrap().as_bool().unwrap(), content="true")
inspect(
sink_obj.get("auto_flush").unwrap().as_bool().unwrap(),
content="true",
)
inspect(
formatter_obj.get("show_timestamp").unwrap().as_bool().unwrap(),
content="true",
)
inspect(
formatter_obj.get("separator").unwrap().as_string().unwrap(),
content=" ",
)
inspect(
formatter_obj.get("field_separator").unwrap().as_string().unwrap(),
content=" ",
)
inspect(
formatter_obj.get("template").unwrap().as_string().unwrap(),
content="",
)
inspect(
formatter_obj.get("color_mode").unwrap().as_string().unwrap(),
content="never",
)
inspect(
formatter_obj.get("color_support").unwrap().as_string().unwrap(),
content="truecolor",
)
inspect(
formatter_obj.get("style_markup").unwrap().as_string().unwrap(),
content="full",
)
inspect(
async_obj.get("max_pending").unwrap().as_number().unwrap().to_int(),
content="2",
)
inspect(
async_obj.get("max_batch").unwrap().as_number().unwrap().to_int(),
content="5",
)
inspect(
async_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(),
content="40",
)
inspect(
async_obj.get("overflow").unwrap().as_string().unwrap(),
content="DropNewest",
)
inspect(
async_obj.get("flush").unwrap().as_string().unwrap(),
content="Shutdown",
)
}
///|
test "async build config json export materializes parsed omitted defaults" {
let parsed = parse_async_logger_build_config_text("{}")
let build_json = async_logger_build_config_to_json(parsed)
let build_obj = build_json.as_object().unwrap()
let logger_obj = build_obj.get("logger").unwrap().as_object().unwrap()
let sink_obj = logger_obj.get("sink").unwrap().as_object().unwrap()
let formatter_obj = sink_obj
.get("text_formatter")
.unwrap()
.as_object()
.unwrap()
let async_obj = build_obj.get("async_config").unwrap().as_object().unwrap()
inspect(
@json_parser.stringify(build_json),
content="{\"logger\":{\"min_level\":\"INFO\",\"target\":\"\",\"timestamp\":false,\"sink\":{\"kind\":\"console\",\"path\":\"\",\"append\":true,\"auto_flush\":true,\"text_formatter\":{\"show_timestamp\":true,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"never\",\"color_support\":\"truecolor\",\"style_markup\":\"full\",\"target_style_markup\":\"disabled\",\"fields_style_markup\":\"disabled\"}}},\"async_config\":{\"max_pending\":0,\"max_batch\":1,\"linger_ms\":0,\"overflow\":\"Blocking\",\"flush\":\"Never\"}}",
)
inspect(
logger_obj.get("min_level").unwrap().as_string().unwrap(),
content="INFO",
)
inspect(logger_obj.get("target").unwrap().as_string().unwrap(), content="")
inspect(
logger_obj.get("timestamp").unwrap().as_bool().unwrap(),
content="false",
)
inspect(logger_obj.get("queue") is None, content="true")
inspect(sink_obj.get("kind").unwrap().as_string().unwrap(), content="console")
inspect(sink_obj.get("path").unwrap().as_string().unwrap(), content="")
inspect(sink_obj.get("append").unwrap().as_bool().unwrap(), content="true")
inspect(
sink_obj.get("auto_flush").unwrap().as_bool().unwrap(),
content="true",
)
inspect(
formatter_obj.get("show_timestamp").unwrap().as_bool().unwrap(),
content="true",
)
inspect(
formatter_obj.get("show_level").unwrap().as_bool().unwrap(),
content="true",
)
inspect(
formatter_obj.get("show_target").unwrap().as_bool().unwrap(),
content="true",
)
inspect(
formatter_obj.get("show_fields").unwrap().as_bool().unwrap(),
content="true",
)
inspect(
formatter_obj.get("separator").unwrap().as_string().unwrap(),
content=" ",
)
inspect(
formatter_obj.get("field_separator").unwrap().as_string().unwrap(),
content=" ",
)
inspect(
formatter_obj.get("template").unwrap().as_string().unwrap(),
content="",
)
inspect(
formatter_obj.get("color_mode").unwrap().as_string().unwrap(),
content="never",
)
inspect(
formatter_obj.get("color_support").unwrap().as_string().unwrap(),
content="truecolor",
)
inspect(
formatter_obj.get("style_markup").unwrap().as_string().unwrap(),
content="full",
)
inspect(
formatter_obj.get("target_style_markup").unwrap().as_string().unwrap(),
content="disabled",
)
inspect(
formatter_obj.get("fields_style_markup").unwrap().as_string().unwrap(),
content="disabled",
)
inspect(formatter_obj.get("style_tags") is None, content="true")
inspect(
async_obj.get("max_pending").unwrap().as_number().unwrap().to_int(),
content="0",
)
inspect(
async_obj.get("max_batch").unwrap().as_number().unwrap().to_int(),
content="1",
)
inspect(
async_obj.get("linger_ms").unwrap().as_number().unwrap().to_int(),
content="0",
)
inspect(
async_obj.get("overflow").unwrap().as_string().unwrap(),
content="Blocking",
)
inspect(async_obj.get("flush").unwrap().as_string().unwrap(), content="Never")
}
///|
test "async config parsers reject malformed input" {
let invalid_json_error = (fn() -> String raise {
ignore(parse_async_logger_config_text("{"))
"no error"
})() catch {
err => err.to_string()
}
inspect(invalid_json_error.contains("UnexpectedToken"), content="true")
let wrong_type_error = (fn() -> String raise {
ignore(parse_async_logger_config_text("{\"max_pending\":\"many\"}"))
"no error"
})() catch {
err => err.to_string()
}
inspect(
wrong_type_error.contains("Expected number at async_config.max_pending"),
content="true",
)
let invalid_enum_error = (fn() -> String raise {
ignore(parse_async_logger_config_text("{\"overflow\":\"Burst\"}"))
"no error"
})() catch {
err => err.to_string()
}
inspect(
invalid_enum_error.contains("Unsupported async overflow policy: Burst"),
content="true",
)
let invalid_build_root_error = (fn() -> String raise {
ignore(parse_async_logger_build_config_text("[]"))
"no error"
})() catch {
err => err.to_string()
}
inspect(
invalid_build_root_error.contains(
"Expected object at async logger build config root",
),
content="true",
)
let nested_sync_error = (fn() -> String raise {
ignore(
parse_async_logger_build_config_text(
"{\"logger\":{\"timestamp\":\"true\"}}",
),
)
"no error"
})() catch {
err => err.to_string()
}
inspect(
nested_sync_error.contains("ConfigError.InvalidConfig"),
content="true",
)
}
///|
test "async runtime capability helpers stay consistent" {
let mode = async_runtime_mode()
let state = async_runtime_state()
let worker_supported = match mode {
AsyncRuntimeMode::NativeWorker => true
AsyncRuntimeMode::Compatibility => false
}
inspect(
async_runtime_mode_label(mode) == "native_worker" ||
async_runtime_mode_label(mode) == "compatibility",
content="true",
)
inspect(
async_runtime_supports_background_worker() == worker_supported,
content="true",
)
inspect(
async_runtime_mode_label(state.mode) == async_runtime_mode_label(mode),
content="true",
)
inspect(state.background_worker == worker_supported, content="true")
inspect(
@json_parser.stringify(async_runtime_state_to_json(state)),
content=if worker_supported {
"{\"mode\":\"native_worker\",\"background_worker\":true}"
} else {
"{\"mode\":\"compatibility\",\"background_worker\":false}"
},
)
inspect(
stringify_async_runtime_state(state),
content=if worker_supported {
"{\"mode\":\"native_worker\",\"background_worker\":true}"
} else {
"{\"mode\":\"compatibility\",\"background_worker\":false}"
},
)
}
///|
test "async runtime mode and worker flag encode target contract" {
let mode_label = async_runtime_mode_label(async_runtime_mode())
let worker = async_runtime_supports_background_worker()
inspect(
mode_label == "native_worker" || mode_label == "compatibility",
content="true",
)
inspect((mode_label == "native_worker") == worker, content="true")
inspect((mode_label == "compatibility") == !worker, content="true")
}
///|
test "async logger state snapshot reflects current counters and runtime" {
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(
max_pending=3,
overflow=AsyncOverflowPolicy::DropNewest,
flush=AsyncFlushPolicy::Shutdown,
),
min_level=@bitlogger.Level::Info,
target="async.state",
)
let state = logger.state()
inspect(
async_runtime_mode_label(state.runtime.mode) ==
async_runtime_mode_label(async_runtime_mode()),
content="true",
)
inspect(
state.runtime.background_worker ==
async_runtime_supports_background_worker(),
content="true",
)
inspect(state.pending_count, content="0")
inspect(state.dropped_count, content="0")
inspect(state.is_closed, content="false")
inspect(state.is_running, content="false")
inspect(state.has_failed, content="false")
inspect(state.last_error, content="")
inspect(
match state.flush_policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
inspect(
@json_parser.stringify(async_logger_state_to_json(state)),
content=if async_runtime_supports_background_worker() {
"{\"runtime\":{\"mode\":\"native_worker\",\"background_worker\":true},\"pending_count\":0,\"dropped_count\":0,\"is_closed\":false,\"is_running\":false,\"has_failed\":false,\"last_error\":\"\",\"flush_policy\":\"Shutdown\"}"
} else {
"{\"runtime\":{\"mode\":\"compatibility\",\"background_worker\":false},\"pending_count\":0,\"dropped_count\":0,\"is_closed\":false,\"is_running\":false,\"has_failed\":false,\"last_error\":\"\",\"flush_policy\":\"Shutdown\"}"
},
)
inspect(
stringify_async_logger_state(state),
content=if async_runtime_supports_background_worker() {
"{\"runtime\":{\"mode\":\"native_worker\",\"background_worker\":true},\"pending_count\":0,\"dropped_count\":0,\"is_closed\":false,\"is_running\":false,\"has_failed\":false,\"last_error\":\"\",\"flush_policy\":\"Shutdown\"}"
} else {
"{\"runtime\":{\"mode\":\"compatibility\",\"background_worker\":false},\"pending_count\":0,\"dropped_count\":0,\"is_closed\":false,\"is_running\":false,\"has_failed\":false,\"last_error\":\"\",\"flush_policy\":\"Shutdown\"}"
},
)
}
///|
async test "run drains queued records in compatibility backends too" {
let written : Ref[Array[String]] = Ref([])
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) { written.val.push(rec.message) }),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::DropNewest,
max_batch=2,
linger_ms=5,
flush=AsyncFlushPolicy::Never,
),
min_level=@bitlogger.Level::Info,
target="async.compat",
)
@async.with_task_group(group => {
logger.info("one")
logger.info("two")
inspect(logger.pending_count(), content="2")
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.pending_count(), content="0")
inspect(written.val.length(), content="2")
inspect(written.val[0], content="one")
inspect(written.val[1], content="two")
}
///|
async test "async logger records worker failures and wait_idle stops early" {
let writes : Ref[Int] = Ref(0)
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { writes.val += 1 }),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Info,
target="async.failure",
flush=fn(_) -> Int raise { raise TestFlushError("flush exploded") },
)
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
logger.info("ok")
logger.info("flush-now")
logger.wait_idle()
inspect(logger.has_failed(), content="true")
inspect(logger.pending_count(), content="1")
logger.close(clear=true)
})
inspect(writes.val, content="1")
inspect(logger.has_failed(), content="true")
inspect(logger.last_error().contains("TestFlushError"), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
}
///|
async test "later started run resets async failure state before draining remaining backlog" {
let writes : Ref[Int] = Ref(0)
let flushes : Ref[Int] = Ref(0)
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { writes.val += 1 }),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Info,
target="async.retry",
flush=fn(_) -> Int raise {
flushes.val += 1
if flushes.val == 1 {
raise TestFlushError("flush exploded once")
}
1
},
)
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
logger.info("one")
logger.info("two")
logger.wait_idle()
inspect(logger.has_failed(), content="true")
inspect(logger.last_error().contains("TestFlushError"), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="1")
group.spawn_bg(() => logger.run())
while !logger.is_running() {
@async.pause()
}
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
logger.shutdown()
})
inspect(writes.val, content="2")
inspect(flushes.val, content="2")
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
inspect(logger.is_running(), content="false")
inspect(logger.is_closed(), content="true")
inspect(logger.pending_count(), content="0")
}
///|
async test "shutdown after worker failure uses runtime-specific pending cleanup" {
let writes : Ref[Int] = Ref(0)
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { writes.val += 1 }),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Info,
target="async.failure.shutdown",
flush=fn(_) -> Int raise {
raise TestFlushError("flush exploded on shutdown path")
},
)
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
logger.info("one")
logger.info("two")
logger.wait_idle()
inspect(logger.has_failed(), content="true")
inspect(logger.pending_count(), content="1")
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.has_failed(), content="true")
inspect(logger.last_error().contains("TestFlushError"), content="true")
inspect(logger.is_running(), content="false")
inspect(writes.val, content="1")
inspect(
logger.pending_count(),
content=if async_runtime_supports_background_worker() { "0" } else { "1" },
)
inspect(
logger.dropped_count(),
content=if async_runtime_supports_background_worker() { "1" } else { "0" },
)
}
///|
async test "library async logger keeps a smaller async facade" {
let written_targets : Ref[Array[String]] = Ref([])
let written_messages : Ref[Array[String]] = Ref([])
let written_field_counts : Ref[Array[Int]] = Ref([])
let logger = LibraryAsyncLogger::new(
@bitlogger.callback_sink(fn(rec) {
written_targets.val.push(rec.target)
written_messages.val.push(rec.message)
written_field_counts.val.push(rec.fields.length())
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="async.lib",
)
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.child("worker")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("ready", fields=[@bitlogger.field("mode", "test")])
logger.shutdown()
})
inspect(written_targets.val.length(), content="1")
inspect(written_targets.val[0], content="async.lib.worker")
inspect(written_messages.val[0], content="ready")
inspect(written_field_counts.val[0], content="2")
}
///|
async test "library async new preserves config and flush failure contract" {
let writes : Ref[Int] = Ref(0)
let flushes : Ref[Int] = Ref(0)
let logger = LibraryAsyncLogger::new(
@bitlogger.callback_sink(fn(_) { writes.val += 1 }),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
max_batch=3,
linger_ms=7,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Warn,
target="async.lib.new",
flush=fn(_) -> Int raise {
flushes.val += 1
raise TestFlushError("library new flush exploded")
},
)
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.new")
inspect(
match full.flush_policy() {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Batch",
)
inspect(full.pending_count(), content="0")
inspect(full.has_failed(), content="false")
inspect(full.last_error(), content="")
logger.info("skip")
inspect(full.pending_count(), content="0")
logger.error("one")
logger.error("two")
inspect(full.pending_count(), content="2")
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
full.wait_idle()
while !full.has_failed() && full.is_running() {
@async.pause()
}
inspect(full.has_failed(), content="true")
inspect(full.last_error().contains("TestFlushError"), content="true")
inspect(full.is_running(), content="false")
inspect(full.pending_count(), content="0")
logger.shutdown(clear=true)
})
inspect(writes.val, content="2")
inspect(flushes.val, content="1")
inspect(full.is_closed(), content="true")
}
///|
async test "library async context binding replaces stored field set" {
let written_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let logger = LibraryAsyncLogger::new(
@bitlogger.callback_sink(fn(rec) { written_fields.val = rec.fields }),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.lib.ctx",
)
.with_context_fields([@bitlogger.field("old", "gone")])
.with_context_fields([
@bitlogger.field("service", "bitlogger"),
@bitlogger.field("scope", "sdk"),
])
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.ctx")
inspect(full.timestamp, content="false")
inspect(full.context_fields.length(), content="2")
inspect(full.context_fields[0].key, content="service")
inspect(full.context_fields[0].value, content="bitlogger")
inspect(full.context_fields[1].key, content="scope")
inspect(full.context_fields[1].value, content="sdk")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.error("ctx", fields=[@bitlogger.field("mode", "test")])
logger.shutdown()
})
inspect(written_fields.val.length(), content="3")
inspect(written_fields.val[0].key, content="service")
inspect(written_fields.val[0].value, content="bitlogger")
inspect(written_fields.val[1].key, content="scope")
inspect(written_fields.val[1].value, content="sdk")
inspect(written_fields.val[2].key, content="mode")
inspect(written_fields.val[2].value, content="test")
}
///|
async test "library async bind matches context facade contract" {
let written_target : Ref[String] = Ref("")
let written_timestamp : Ref[UInt64] = Ref(0UL)
let written_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) {
written_target.val = rec.target
written_timestamp.val = rec.timestamp_ms
written_fields.val = rec.fields
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.lib.bind",
)
.with_timestamp()
.to_library_async_logger()
.bind([@bitlogger.field("service", "bitlogger")])
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.bind")
inspect(full.timestamp, content="true")
inspect(full.context_fields.length(), content="1")
inspect(full.context_fields[0].key, content="service")
inspect(full.context_fields[0].value, content="bitlogger")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.error("bound", fields=[@bitlogger.field("mode", "test")])
logger.shutdown()
})
inspect(written_target.val, content="async.lib.bind")
inspect(written_timestamp.val > 0UL, content="true")
inspect(written_fields.val.length(), content="2")
inspect(written_fields.val[0].key, content="service")
inspect(written_fields.val[0].value, content="bitlogger")
inspect(written_fields.val[1].key, content="mode")
inspect(written_fields.val[1].value, content="test")
}
///|
async test "library async logger with_target preserves facade state while replacing target" {
let written_target : Ref[String] = Ref("")
let written_timestamp : Ref[UInt64] = Ref(0UL)
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) {
written_target.val = rec.target
written_timestamp.val = rec.timestamp_ms
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.lib.base",
)
.with_timestamp()
.to_library_async_logger()
.with_target("async.lib.retarget")
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.retarget")
inspect(full.timestamp, content="true")
inspect(full.pending_count(), content="0")
inspect(full.dropped_count(), content="0")
inspect(full.is_closed(), content="false")
inspect(full.is_running(), content="false")
inspect(full.has_failed(), content="false")
inspect(full.last_error(), content="")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.error("retargeted")
logger.shutdown()
})
inspect(written_target.val, content="async.lib.retarget")
inspect(written_timestamp.val > 0UL, content="true")
}
///|
async test "library async logger child composes target through facade" {
let written_target : Ref[String] = Ref("")
let written_timestamp : Ref[UInt64] = Ref(0UL)
let logger = async_logger(
@bitlogger.callback_sink(fn(rec) {
written_target.val = rec.target
written_timestamp.val = rec.timestamp_ms
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="sdk",
)
.with_timestamp()
.to_library_async_logger()
.child("worker")
let full = logger.to_async_logger()
inspect(full.target, content="sdk.worker")
inspect(full.timestamp, content="true")
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.error("child")
logger.shutdown()
})
inspect(written_target.val, content="sdk.worker")
inspect(written_timestamp.val > 0UL, content="true")
let root_child = LibraryAsyncLogger::new(@bitlogger.callback_sink(fn(_) { })).child(
"worker",
)
inspect(root_child.to_async_logger().target, content="worker")
let keep_parent = LibraryAsyncLogger::new(
@bitlogger.callback_sink(fn(_) { }),
target="sdk",
).child("")
inspect(keep_parent.to_async_logger().target, content="sdk")
}
///|
async test "library async logger log supports per-call target override" {
let written_target : Ref[String] = Ref("")
let written_message : Ref[String] = Ref("")
let written_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let logger = LibraryAsyncLogger::new(
@bitlogger.callback_sink(fn(rec) {
written_target.val = rec.target
written_message.val = rec.message
written_fields.val = rec.fields
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.lib.default",
).with_context_fields([@bitlogger.field("service", "bitlogger")])
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
fields=[@bitlogger.field("mode", "test")],
target="async.lib.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.lib.override")
inspect(written_message.val, content="override")
inspect(written_fields.val.length(), content="2")
inspect(written_fields.val[0].key, content="service")
inspect(written_fields.val[0].value, content="bitlogger")
inspect(written_fields.val[1].key, content="mode")
inspect(written_fields.val[1].value, content="test")
let second_target : Ref[String] = Ref("")
let second_message : Ref[String] = Ref("")
let second_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let second = LibraryAsyncLogger::new(
@bitlogger.callback_sink(fn(rec) {
second_target.val = rec.target
second_message.val = rec.message
second_fields.val = rec.fields
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Warn,
target="async.lib.default",
).with_context_fields([@bitlogger.field("service", "bitlogger")])
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.log(@bitlogger.Level::Error, "default")
second.shutdown()
})
inspect(second_target.val, content="async.lib.default")
inspect(second_message.val, content="default")
inspect(second_fields.val.length(), content="1")
inspect(second_fields.val[0].key, content="service")
}
///|
async test "library async severity helpers use stored target without override" {
let written_levels : Ref[Array[String]] = Ref([])
let written_targets : Ref[Array[String]] = Ref([])
let written_fields : Ref[Array[Array[@bitlogger.Field]]] = Ref([])
let logger = LibraryAsyncLogger::new(
@bitlogger.callback_sink(fn(rec) {
written_levels.val.push(rec.level.label())
written_targets.val.push(rec.target)
written_fields.val.push(rec.fields)
}),
config=AsyncLoggerConfig::new(max_pending=4),
min_level=@bitlogger.Level::Info,
target="async.lib.helpers",
).bind([@bitlogger.field("service", "bitlogger")])
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("info", fields=[@bitlogger.field("mode", "info")])
logger.warn("warn", fields=[@bitlogger.field("mode", "warn")])
logger.error("error", fields=[@bitlogger.field("mode", "error")])
logger.shutdown()
})
inspect(written_levels.val.length(), content="3")
inspect(written_levels.val[0], content="INFO")
inspect(written_levels.val[1], content="WARN")
inspect(written_levels.val[2], content="ERROR")
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.lib.helpers")
inspect(written_targets.val[1], content="async.lib.helpers")
inspect(written_targets.val[2], content="async.lib.helpers")
inspect(written_fields.val[0].length(), content="2")
inspect(written_fields.val[1].length(), content="2")
inspect(written_fields.val[2].length(), content="2")
inspect(written_fields.val[0][0].key, content="service")
inspect(written_fields.val[0][1].value, content="info")
inspect(written_fields.val[1][0].key, content="service")
inspect(written_fields.val[1][1].value, content="warn")
inspect(written_fields.val[2][0].key, content="service")
inspect(written_fields.val[2][1].value, content="error")
}
///|
async test "library async shutdown preserves wrapped failure cleanup semantics" {
let writes : Ref[Int] = Ref(0)
let logger = LibraryAsyncLogger::new(
@bitlogger.callback_sink(fn(_) { writes.val += 1 }),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Info,
target="async.lib.failure",
flush=fn(_) -> Int raise {
raise TestFlushError("library facade flush exploded")
},
)
let full = logger.to_async_logger()
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
logger.info("one")
logger.info("two")
full.wait_idle()
inspect(full.has_failed(), content="true")
inspect(full.pending_count(), content="1")
logger.shutdown()
})
inspect(full.is_closed(), content="true")
inspect(full.has_failed(), content="true")
inspect(full.last_error().contains("TestFlushError"), content="true")
inspect(full.is_running(), content="false")
inspect(writes.val, content="1")
inspect(
full.pending_count(),
content=if async_runtime_supports_background_worker() { "0" } else { "1" },
)
inspect(
full.dropped_count(),
content=if async_runtime_supports_background_worker() { "1" } else { "0" },
)
}
///|
async test "library async shutdown clear abandons pending records through facade" {
let logger = LibraryAsyncLogger::new(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
),
min_level=@bitlogger.Level::Info,
target="async.lib.clear",
)
let full = logger.to_async_logger()
logger.info("one")
inspect(full.pending_count(), content="1")
inspect(full.dropped_count(), content="0")
logger.shutdown(clear=true)
inspect(full.is_closed(), content="true")
inspect(full.is_running(), content="false")
inspect(full.pending_count(), content="0")
inspect(full.dropped_count(), content="1")
}
///|
async test "library async logger can be built from config" {
let logger = parse_and_build_library_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.config\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.config")
}
///|
async test "library async builder unwrap matches direct async builder behavior" {
let config = AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.lib.same-build",
timestamp=true,
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropNewest,
),
),
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
)
let logger = build_library_async_logger(config)
let full = logger.to_async_logger()
let direct = build_async_logger(config)
let full_state = full.state()
let direct_state = direct.state()
inspect(full.target, content=direct.target)
inspect(full.timestamp == direct.timestamp, content="true")
inspect(
logger.is_enabled(@bitlogger.Level::Error) ==
direct.is_enabled(@bitlogger.Level::Error),
content="true",
)
inspect(
logger.is_enabled(@bitlogger.Level::Info) ==
direct.is_enabled(@bitlogger.Level::Info),
content="true",
)
inspect(
stringify_async_logger_state(full_state) ==
stringify_async_logger_state(direct_state),
content="true",
)
let full_sink_kind = match full.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
@bitlogger.RuntimeSink::Console(_) => "Console"
@bitlogger.RuntimeSink::JsonConsole(_) => "JsonConsole"
@bitlogger.RuntimeSink::TextConsole(_) => "TextConsole"
@bitlogger.RuntimeSink::File(_) => "File"
}
let direct_sink_kind = match direct.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
@bitlogger.RuntimeSink::Console(_) => "Console"
@bitlogger.RuntimeSink::JsonConsole(_) => "JsonConsole"
@bitlogger.RuntimeSink::TextConsole(_) => "TextConsole"
@bitlogger.RuntimeSink::File(_) => "File"
}
inspect(full_sink_kind == direct_sink_kind, content="true")
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
direct.info("skip")
direct.error("one")
direct.error("two")
direct.error("three")
inspect(full.pending_count() == direct.pending_count(), content="true")
inspect(full.dropped_count() == direct.dropped_count(), content="true")
inspect(full.is_closed() == direct.is_closed(), content="true")
inspect(full.is_running() == direct.is_running(), content="true")
inspect(
full.sink.pending_count() == direct.sink.pending_count(),
content="true",
)
inspect(
full.sink.dropped_count() == direct.sink.dropped_count(),
content="true",
)
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
group.spawn_bg(() => direct.run())
logger.shutdown()
direct.shutdown()
})
inspect(full.is_closed() == direct.is_closed(), content="true")
inspect(full.is_running() == direct.is_running(), content="true")
inspect(full.pending_count() == direct.pending_count(), content="true")
inspect(full.dropped_count() == direct.dropped_count(), content="true")
inspect(full.has_failed() == direct.has_failed(), content="true")
inspect(full.last_error() == direct.last_error(), content="true")
}
///|
async test "library async builder log supports per-call target override through facade" {
let written_target : Ref[String] = Ref("")
let logger = build_library_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.lib.builder.default",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
)
.to_async_logger()
.with_patch(fn(rec) {
written_target.val = rec.target
rec
})
.to_library_async_logger()
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
target="async.lib.builder.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.lib.builder.override")
let second_target : Ref[String] = Ref("")
let second = build_library_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.lib.builder.default",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
)
.to_async_logger()
.with_patch(fn(rec) {
second_target.val = rec.target
rec
})
.to_library_async_logger()
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.error("default")
second.shutdown()
})
inspect(second_target.val, content="async.lib.builder.default")
}
///|
async test "library async builder severity helpers use stored target without override" {
let written_targets : Ref[Array[String]] = Ref([])
let logger = build_library_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Trace,
target="async.lib.builder.helpers",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
)
.to_async_logger()
.with_patch(fn(rec) {
written_targets.val.push(rec.target)
rec
})
.to_library_async_logger()
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("first")
logger.warn("second")
logger.error("third")
logger.shutdown()
})
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.lib.builder.helpers")
inspect(written_targets.val[1], content="async.lib.builder.helpers")
inspect(written_targets.val[2], content="async.lib.builder.helpers")
}
///|
async test "library async builder shutdown clear abandons pending records through facade" {
let logger = build_library_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Info,
target="async.lib.builder.clear",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
),
),
)
let full = logger.to_async_logger()
logger.info("one")
inspect(full.pending_count(), content="1")
inspect(full.dropped_count(), content="0")
logger.shutdown(clear=true)
inspect(full.is_closed(), content="true")
inspect(full.is_running(), content="false")
inspect(full.pending_count(), content="0")
inspect(full.dropped_count(), content="1")
}
///|
async test "parsed library async logger unwrap keeps async helper surface" {
let logger = parse_and_build_library_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.config.helpers\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}",
)
let full = logger.to_async_logger()
let state = full.state()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.config.helpers")
inspect(state.pending_count, content="0")
inspect(state.dropped_count, content="0")
inspect(state.is_closed, content="false")
inspect(state.is_running, content="false")
inspect(state.has_failed, content="false")
inspect(
match state.flush_policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
inspect(full.pending_count(), content="2")
inspect(full.dropped_count(), content="1")
inspect(full.is_closed(), content="false")
inspect(full.is_running(), content="false")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(full.is_closed(), content="true")
inspect(full.is_running(), content="false")
inspect(full.pending_count(), content="0")
inspect(full.dropped_count(), content="1")
inspect(full.has_failed(), content="false")
inspect(full.last_error(), content="")
}
///|
async test "parsed library async logger shutdown clear abandons pending records through facade" {
let logger = parse_and_build_library_async_logger(
"{\"logger\":{\"min_level\":\"info\",\"target\":\"async.lib.json.clear\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
let full = logger.to_async_logger()
logger.info("one")
inspect(full.pending_count(), content="1")
inspect(full.dropped_count(), content="0")
logger.shutdown(clear=true)
inspect(full.is_closed(), content="true")
inspect(full.is_running(), content="false")
inspect(full.pending_count(), content="0")
inspect(full.dropped_count(), content="1")
}
///|
async test "parsed library async logger log supports per-call target override through facade" {
let written_target : Ref[String] = Ref("")
let logger = parse_and_build_library_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
.to_async_logger()
.with_patch(fn(rec) {
written_target.val = rec.target
rec
})
.to_library_async_logger()
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
target="async.lib.json.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.lib.json.override")
let second_target : Ref[String] = Ref("")
let second = parse_and_build_library_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
.to_async_logger()
.with_patch(fn(rec) {
second_target.val = rec.target
rec
})
.to_library_async_logger()
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.error("default")
second.shutdown()
})
inspect(second_target.val, content="async.lib.json.default")
}
///|
async test "parsed library async severity helpers use stored target without override" {
let written_targets : Ref[Array[String]] = Ref([])
let logger = parse_and_build_library_async_logger(
"{\"logger\":{\"min_level\":\"trace\",\"target\":\"async.lib.json.helpers.target\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":4,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
.to_async_logger()
.with_patch(fn(rec) {
written_targets.val.push(rec.target)
rec
})
.to_library_async_logger()
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("first")
logger.warn("second")
logger.error("third")
logger.shutdown()
})
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.lib.json.helpers.target")
inspect(written_targets.val[1], content="async.lib.json.helpers.target")
inspect(written_targets.val[2], content="async.lib.json.helpers.target")
}
///|
async test "library async parse-build unwrap matches parsed direct async builder behavior" {
let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.same-build\",\"timestamp\":true,\"queue\":{\"max_pending\":3,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}"
let logger = parse_and_build_library_async_logger(raw)
let full = logger.to_async_logger()
let direct = build_async_logger(parse_async_logger_build_config_text(raw))
let full_state = full.state()
let direct_state = direct.state()
inspect(full.target, content=direct.target)
inspect(full.timestamp == direct.timestamp, content="true")
inspect(
logger.is_enabled(@bitlogger.Level::Error) ==
direct.is_enabled(@bitlogger.Level::Error),
content="true",
)
inspect(
logger.is_enabled(@bitlogger.Level::Info) ==
direct.is_enabled(@bitlogger.Level::Info),
content="true",
)
inspect(
stringify_async_logger_state(full_state) ==
stringify_async_logger_state(direct_state),
content="true",
)
let full_sink_kind = match full.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
@bitlogger.RuntimeSink::Console(_) => "Console"
@bitlogger.RuntimeSink::JsonConsole(_) => "JsonConsole"
@bitlogger.RuntimeSink::TextConsole(_) => "TextConsole"
@bitlogger.RuntimeSink::File(_) => "File"
}
let direct_sink_kind = match direct.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
@bitlogger.RuntimeSink::Console(_) => "Console"
@bitlogger.RuntimeSink::JsonConsole(_) => "JsonConsole"
@bitlogger.RuntimeSink::TextConsole(_) => "TextConsole"
@bitlogger.RuntimeSink::File(_) => "File"
}
inspect(full_sink_kind == direct_sink_kind, content="true")
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
direct.info("skip")
direct.error("one")
direct.error("two")
direct.error("three")
inspect(full.pending_count() == direct.pending_count(), content="true")
inspect(full.dropped_count() == direct.dropped_count(), content="true")
inspect(full.is_closed() == direct.is_closed(), content="true")
inspect(full.is_running() == direct.is_running(), content="true")
inspect(
full.sink.pending_count() == direct.sink.pending_count(),
content="true",
)
inspect(
full.sink.dropped_count() == direct.sink.dropped_count(),
content="true",
)
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
group.spawn_bg(() => direct.run())
logger.shutdown()
direct.shutdown()
})
inspect(full.is_closed() == direct.is_closed(), content="true")
inspect(full.is_running() == direct.is_running(), content="true")
inspect(full.pending_count() == direct.pending_count(), content="true")
inspect(full.dropped_count() == direct.dropped_count(), content="true")
inspect(full.has_failed() == direct.has_failed(), content="true")
inspect(full.last_error() == direct.last_error(), content="true")
}
///|
async test "library async parse-build unwrap preserves file-backed runtime helpers" {
let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"async-lib-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}"
let logger = parse_and_build_library_async_logger(raw)
let full = logger.to_async_logger()
let direct = build_async_logger(parse_async_logger_build_config_text(raw))
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
direct.info("skip")
direct.error("one")
direct.error("two")
direct.error("three")
inspect(full.pending_count() == direct.pending_count(), content="true")
inspect(full.dropped_count() == direct.dropped_count(), content="true")
inspect(
full.sink.pending_count() == direct.sink.pending_count(),
content="true",
)
inspect(
full.sink.dropped_count() == direct.sink.dropped_count(),
content="true",
)
inspect(
full.sink.file_available() == direct.sink.file_available(),
content="true",
)
inspect(
full.sink.file_append_mode() == direct.sink.file_append_mode(),
content="true",
)
inspect(
full.sink.file_auto_flush() == direct.sink.file_auto_flush(),
content="true",
)
let full_state = full.sink.file_state()
let direct_state = direct.sink.file_state()
inspect(full_state.path == direct_state.path, content="true")
inspect(full_state.available == direct_state.available, content="true")
inspect(full_state.append == direct_state.append, content="true")
inspect(full_state.auto_flush == direct_state.auto_flush, content="true")
inspect(
full_state.open_failures == direct_state.open_failures,
content="true",
)
inspect(
full_state.write_failures == direct_state.write_failures,
content="true",
)
inspect(
full_state.flush_failures == direct_state.flush_failures,
content="true",
)
inspect(
full_state.rotation_failures == direct_state.rotation_failures,
content="true",
)
match (full.sink.file_runtime_state(), direct.sink.file_runtime_state()) {
(Some(snapshot), Some(other)) => {
inspect(snapshot.file.path == other.file.path, content="true")
inspect(snapshot.file.available == other.file.available, content="true")
inspect(snapshot.queued == other.queued, content="true")
inspect(snapshot.pending_count == other.pending_count, content="true")
inspect(snapshot.dropped_count == other.dropped_count, content="true")
}
_ => inspect(false, content="true")
}
inspect(
full.sink.file_set_append_mode(false) ==
direct.sink.file_set_append_mode(false),
content="true",
)
inspect(
full.sink.file_append_mode() == direct.sink.file_append_mode(),
content="true",
)
inspect(
full.sink.file_set_auto_flush(false) ==
direct.sink.file_set_auto_flush(false),
content="true",
)
inspect(
full.sink.file_auto_flush() == direct.sink.file_auto_flush(),
content="true",
)
inspect(
full.sink.file_set_rotation(
Some(@bitlogger.file_rotation(36, max_backups=2)),
) ==
direct.sink.file_set_rotation(
Some(@bitlogger.file_rotation(36, max_backups=2)),
),
content="true",
)
inspect(
full.sink.file_rotation_enabled() == direct.sink.file_rotation_enabled(),
content="true",
)
inspect(
full.sink.file_reopen_append() == direct.sink.file_reopen_append(),
content="true",
)
inspect(
full.sink.file_append_mode() == direct.sink.file_append_mode(),
content="true",
)
inspect(
full.sink.file_clear_rotation() == direct.sink.file_clear_rotation(),
content="true",
)
inspect(
full.sink.file_reset_policy() == direct.sink.file_reset_policy(),
content="true",
)
inspect(
full.sink.file_policy_matches_default() ==
direct.sink.file_policy_matches_default(),
content="true",
)
inspect(
full.sink.file_reset_failure_counters() ==
direct.sink.file_reset_failure_counters(),
content="true",
)
inspect(full.sink.file_flush() == direct.sink.file_flush(), content="true")
inspect(full.pending_count() == direct.pending_count(), content="true")
inspect(full.sink.file_close() == direct.sink.file_close(), content="true")
}
///|
test "library async logger parse-and-build preserves parsed sync queue layer" {
let logger = parse_and_build_library_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.lib.config.queued\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.config.queued")
inspect(
match full.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
_ => "Other"
},
content="QueuedConsole",
)
}
///|
test "library async text logger can be built from typed config" {
let logger = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.lib.text",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.text")
}
///|
async test "library async text builder unwrap matches direct text builder behavior" {
let config = AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.lib.text.same-build",
timestamp=true,
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropNewest,
),
),
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
template="TEXT:{target}:{message}",
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
)
let logger = build_library_async_text_logger(config)
let full = logger.to_async_logger()
let direct = build_async_text_logger(config)
let full_state = full.state()
let direct_state = direct.state()
inspect(full.target, content=direct.target)
inspect(full.timestamp == direct.timestamp, content="true")
inspect(
logger.is_enabled(@bitlogger.Level::Error) ==
direct.is_enabled(@bitlogger.Level::Error),
content="true",
)
inspect(
logger.is_enabled(@bitlogger.Level::Info) ==
direct.is_enabled(@bitlogger.Level::Info),
content="true",
)
inspect(
stringify_async_logger_state(full_state) ==
stringify_async_logger_state(direct_state),
content="true",
)
let record = @bitlogger.Record::new(
@bitlogger.Level::Error,
"boom",
target="async.lib.text.same-build",
)
inspect(
(full.sink.formatter)(record) == (direct.sink.formatter)(record),
content="true",
)
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
direct.info("skip")
direct.error("one")
direct.error("two")
direct.error("three")
inspect(full.pending_count() == direct.pending_count(), content="true")
inspect(full.dropped_count() == direct.dropped_count(), content="true")
inspect(full.is_closed() == direct.is_closed(), content="true")
inspect(full.is_running() == direct.is_running(), content="true")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
group.spawn_bg(() => direct.run())
logger.shutdown()
direct.shutdown()
})
inspect(full.is_closed() == direct.is_closed(), content="true")
inspect(full.is_running() == direct.is_running(), content="true")
inspect(full.pending_count() == direct.pending_count(), content="true")
inspect(full.dropped_count() == direct.dropped_count(), content="true")
inspect(full.has_failed() == direct.has_failed(), content="true")
inspect(full.last_error() == direct.last_error(), content="true")
}
///|
async test "library async text builder log supports per-call target override through facade" {
let written_target : Ref[String] = Ref("")
let logger = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.lib.text.builder.default",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
)
.to_async_logger()
.with_patch(fn(rec) {
written_target.val = rec.target
rec
})
.to_library_async_logger()
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
target="async.lib.text.builder.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.lib.text.builder.override")
let second_target : Ref[String] = Ref("")
let second = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.lib.text.builder.default",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
)
.to_async_logger()
.with_patch(fn(rec) {
second_target.val = rec.target
rec
})
.to_library_async_logger()
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.error("default")
second.shutdown()
})
inspect(second_target.val, content="async.lib.text.builder.default")
}
///|
async test "library async text builder severity helpers use stored target without override" {
let written_targets : Ref[Array[String]] = Ref([])
let logger = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Info,
target="async.lib.text.builder.helpers",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
)
.to_async_logger()
.with_patch(fn(rec) {
written_targets.val.push(rec.target)
rec
})
.to_library_async_logger()
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("first")
logger.warn("second")
logger.error("third")
logger.shutdown()
})
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.lib.text.builder.helpers")
inspect(written_targets.val[1], content="async.lib.text.builder.helpers")
inspect(written_targets.val[2], content="async.lib.text.builder.helpers")
}
///|
async test "library async text logger unwrap keeps async helper surface" {
let logger = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.lib.text.helpers",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
let full = logger.to_async_logger()
let state = full.state()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.text.helpers")
inspect(state.pending_count, content="0")
inspect(state.dropped_count, content="0")
inspect(state.is_closed, content="false")
inspect(state.is_running, content="false")
inspect(state.has_failed, content="false")
inspect(
match state.flush_policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
inspect(full.pending_count(), content="2")
inspect(full.dropped_count(), content="1")
inspect(full.is_closed(), content="false")
inspect(full.is_running(), content="false")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(full.is_closed(), content="true")
inspect(full.is_running(), content="false")
inspect(full.pending_count(), content="0")
inspect(full.dropped_count(), content="1")
inspect(full.has_failed(), content="false")
inspect(full.last_error(), content="")
}
///|
async test "library async text logger shutdown clear abandons pending records through facade" {
let logger = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Info,
target="async.lib.text.clear",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
),
),
)
let full = logger.to_async_logger()
logger.info("one")
inspect(full.pending_count(), content="1")
inspect(full.dropped_count(), content="0")
logger.shutdown(clear=true)
inspect(full.is_closed(), content="true")
inspect(full.is_running(), content="false")
inspect(full.pending_count(), content="0")
inspect(full.dropped_count(), content="1")
}
///|
test "library async text logger builder ignores sync queue layer" {
let logger = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.lib.text.queued",
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropNewest,
),
),
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.text.queued")
inspect(full.pending_count(), content="0")
inspect(full.dropped_count(), content="0")
}
///|
test "library async text logger ignores sink kind and still uses text formatter" {
let logger = build_library_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.lib.text.kind",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
template="TEXT:{target}:{message}",
),
),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
let full = logger.to_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.lib.text.kind")
let rendered = (full.sink.formatter)(
@bitlogger.Record::new(
@bitlogger.Level::Error,
"boom",
target="async.lib.text.kind",
),
)
inspect(rendered, content="TEXT:async.lib.text.kind:boom")
}
///|
async test "async logger can project to library async logger" {
let logger = build_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
target="async.projected",
min_level=@bitlogger.Level::Warn,
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
).to_library_async_logger()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.to_async_logger().target, content="async.projected")
}
///|
async test "build async logger keeps direct helper surface and runtime sink path" {
let logger = build_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.direct.helpers",
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropNewest,
),
),
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
let state = logger.state()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.direct.helpers")
inspect(state.pending_count, content="0")
inspect(state.dropped_count, content="0")
inspect(state.is_closed, content="false")
inspect(state.is_running, content="false")
inspect(state.has_failed, content="false")
inspect(
match state.flush_policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
inspect(
match logger.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
_ => "Other"
},
content="QueuedConsole",
)
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
inspect(logger.pending_count(), content="2")
inspect(logger.dropped_count(), content="1")
inspect(logger.is_closed(), content="false")
inspect(logger.is_running(), content="false")
inspect(logger.sink.pending_count(), content="0")
inspect(logger.sink.dropped_count(), content="0")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
}
///|
async test "build async logger log supports per-call target override" {
let written_target : Ref[String] = Ref("")
let logger = build_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.builder.default",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
).with_patch(fn(rec) {
written_target.val = rec.target
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
target="async.builder.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.builder.override")
let second_target : Ref[String] = Ref("")
let second = build_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.builder.default",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
).with_patch(fn(rec) {
second_target.val = rec.target
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.error("default")
second.shutdown()
})
inspect(second_target.val, content="async.builder.default")
}
///|
async test "build async logger severity helpers use stored target without override" {
let written_targets : Ref[Array[String]] = Ref([])
let logger = build_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Trace,
target="async.builder.helpers",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
).with_patch(fn(rec) {
written_targets.val.push(rec.target)
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.debug("first")
logger.info("second")
logger.error("third")
logger.shutdown()
})
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.builder.helpers")
inspect(written_targets.val[1], content="async.builder.helpers")
inspect(written_targets.val[2], content="async.builder.helpers")
}
///|
async test "async logger projection preserves async queue and failure state" {
let logger = async_logger(
@bitlogger.callback_sink(fn(_) { }),
config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Batch,
),
min_level=@bitlogger.Level::Info,
target="async.projected.state",
flush=fn(_) -> Int raise {
raise TestFlushError("projected facade flush exploded")
},
).to_library_async_logger()
let full = logger.to_async_logger()
logger.info("one")
logger.info("two")
inspect(full.target, content="async.projected.state")
inspect(full.pending_count(), content="2")
inspect(full.dropped_count(), content="0")
inspect(full.has_failed(), content="false")
inspect(full.last_error(), content="")
@async.with_task_group(group => {
group.spawn_bg(allow_failure=true, () => logger.run())
full.wait_idle()
inspect(full.has_failed(), content="true")
inspect(full.last_error().contains("TestFlushError"), content="true")
inspect(full.is_running(), content="false")
inspect(
full.pending_count(),
content=if async_runtime_supports_background_worker() { "1" } else { "1" },
)
inspect(
full.dropped_count(),
content=if async_runtime_supports_background_worker() { "0" } else { "0" },
)
logger.shutdown()
})
inspect(full.is_closed(), content="true")
inspect(full.has_failed(), content="true")
inspect(full.last_error().contains("TestFlushError"), content="true")
}
///|
async test "async logger projection preserves file-backed runtime helpers through unwrap" {
let configured = build_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.projected.file",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="async-projected-file.log",
),
queue=Some(
@bitlogger.QueueConfig::new(
2,
overflow=@bitlogger.QueueOverflowPolicy::DropOldest,
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
let projected = configured.to_library_async_logger()
let full = projected.to_async_logger()
inspect(projected.is_enabled(@bitlogger.Level::Error), content="true")
inspect(projected.is_enabled(@bitlogger.Level::Info), content="false")
inspect(full.target, content="async.projected.file")
full.error("one")
full.error("two")
full.error("three")
inspect(full.pending_count() == configured.pending_count(), content="true")
inspect(full.dropped_count() == configured.dropped_count(), content="true")
inspect(
full.sink.pending_count() == configured.sink.pending_count(),
content="true",
)
inspect(
full.sink.dropped_count() == configured.sink.dropped_count(),
content="true",
)
inspect(
full.sink.file_available() == configured.sink.file_available(),
content="true",
)
inspect(
full.sink.file_append_mode() == configured.sink.file_append_mode(),
content="true",
)
inspect(
full.sink.file_auto_flush() == configured.sink.file_auto_flush(),
content="true",
)
let full_state = full.sink.file_state()
let configured_state = configured.sink.file_state()
inspect(full_state.path == configured_state.path, content="true")
inspect(full_state.available == configured_state.available, content="true")
inspect(full_state.append == configured_state.append, content="true")
inspect(full_state.auto_flush == configured_state.auto_flush, content="true")
inspect(
full_state.open_failures == configured_state.open_failures,
content="true",
)
inspect(
full_state.write_failures == configured_state.write_failures,
content="true",
)
inspect(
full_state.flush_failures == configured_state.flush_failures,
content="true",
)
inspect(
full_state.rotation_failures == configured_state.rotation_failures,
content="true",
)
match (full.sink.file_runtime_state(), configured.sink.file_runtime_state()) {
(Some(snapshot), Some(other)) => {
inspect(snapshot.file.path == other.file.path, content="true")
inspect(snapshot.file.available == other.file.available, content="true")
inspect(snapshot.queued == other.queued, content="true")
inspect(snapshot.pending_count == other.pending_count, content="true")
inspect(snapshot.dropped_count == other.dropped_count, content="true")
}
_ => inspect(false, content="true")
}
inspect(full.sink.file_set_append_mode(false), content="true")
inspect(configured.sink.file_append_mode(), content="false")
inspect(
full.sink.file_append_mode() == configured.sink.file_append_mode(),
content="true",
)
inspect(full.sink.file_set_auto_flush(false), content="true")
inspect(configured.sink.file_auto_flush(), content="false")
inspect(
full.sink.file_auto_flush() == configured.sink.file_auto_flush(),
content="true",
)
inspect(
full.sink.file_set_rotation(
Some(@bitlogger.file_rotation(36, max_backups=2)),
),
content="true",
)
inspect(configured.sink.file_rotation_enabled(), content="true")
inspect(
full.sink.file_rotation_enabled() == configured.sink.file_rotation_enabled(),
content="true",
)
let reopen_append_ok = full.sink.file_reopen_append()
inspect(
reopen_append_ok == configured.sink.file_reopen_append(),
content="true",
)
inspect(
full.sink.file_append_mode() == configured.sink.file_append_mode(),
content="true",
)
inspect(full.sink.file_clear_rotation(), content="true")
inspect(configured.sink.file_rotation_enabled(), content="false")
inspect(
full.sink.file_rotation_enabled() == configured.sink.file_rotation_enabled(),
content="true",
)
inspect(full.sink.file_reset_policy(), content="true")
inspect(
full.sink.file_policy_matches_default() ==
configured.sink.file_policy_matches_default(),
content="true",
)
inspect(full.sink.file_reset_failure_counters(), content="true")
let reset_full_state = full.sink.file_state()
let reset_configured_state = configured.sink.file_state()
inspect(
reset_full_state.open_failures == reset_configured_state.open_failures,
content="true",
)
inspect(
reset_full_state.write_failures == reset_configured_state.write_failures,
content="true",
)
inspect(
reset_full_state.flush_failures == reset_configured_state.flush_failures,
content="true",
)
inspect(
reset_full_state.rotation_failures ==
reset_configured_state.rotation_failures,
content="true",
)
inspect(
full.sink.file_flush(),
content=if configured.sink.file_available() { "true" } else { "false" },
)
inspect(full.pending_count() == configured.pending_count(), content="true")
let close_expected = configured.sink.file_available()
inspect(
full.sink.file_close(),
content=if close_expected { "true" } else { "false" },
)
inspect(
full.sink.file_available() == configured.sink.file_available(),
content="true",
)
}
///|
test "application async logger aliases runtime async entry" {
let logger = build_application_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
target="async.app",
min_level=@bitlogger.Level::Warn,
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.app")
}
///|
async test "application async logger builder matches direct async builder behavior" {
let config = AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.same-build",
timestamp=true,
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropNewest,
),
),
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
)
let application = build_application_async_logger(config)
let direct = build_async_logger(config)
let application_state = application.state()
let direct_state = direct.state()
inspect(application.target, content=direct.target)
inspect(application.timestamp == direct.timestamp, content="true")
inspect(
application.is_enabled(@bitlogger.Level::Error) ==
direct.is_enabled(@bitlogger.Level::Error),
content="true",
)
inspect(
application.is_enabled(@bitlogger.Level::Info) ==
direct.is_enabled(@bitlogger.Level::Info),
content="true",
)
inspect(
stringify_async_logger_state(application_state) ==
stringify_async_logger_state(direct_state),
content="true",
)
let application_sink_kind = match application.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
@bitlogger.RuntimeSink::Console(_) => "Console"
@bitlogger.RuntimeSink::JsonConsole(_) => "JsonConsole"
@bitlogger.RuntimeSink::TextConsole(_) => "TextConsole"
@bitlogger.RuntimeSink::File(_) => "File"
}
let direct_sink_kind = match direct.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
@bitlogger.RuntimeSink::Console(_) => "Console"
@bitlogger.RuntimeSink::JsonConsole(_) => "JsonConsole"
@bitlogger.RuntimeSink::TextConsole(_) => "TextConsole"
@bitlogger.RuntimeSink::File(_) => "File"
}
inspect(application_sink_kind == direct_sink_kind, content="true")
application.info("skip")
application.error("one")
application.error("two")
application.error("three")
direct.info("skip")
direct.error("one")
direct.error("two")
direct.error("three")
inspect(application.pending_count() == direct.pending_count(), content="true")
inspect(application.dropped_count() == direct.dropped_count(), content="true")
inspect(application.is_closed() == direct.is_closed(), content="true")
inspect(application.is_running() == direct.is_running(), content="true")
inspect(
application.sink.pending_count() == direct.sink.pending_count(),
content="true",
)
inspect(
application.sink.dropped_count() == direct.sink.dropped_count(),
content="true",
)
@async.with_task_group(group => {
group.spawn_bg(() => application.run())
group.spawn_bg(() => direct.run())
application.shutdown()
direct.shutdown()
})
inspect(application.is_closed() == direct.is_closed(), content="true")
inspect(application.is_running() == direct.is_running(), content="true")
inspect(application.pending_count() == direct.pending_count(), content="true")
inspect(application.dropped_count() == direct.dropped_count(), content="true")
inspect(application.has_failed() == direct.has_failed(), content="true")
inspect(application.last_error() == direct.last_error(), content="true")
}
///|
async test "application async builder preserves file-backed runtime helpers" {
let config = AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.file.same-build",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="async-app-file-same-build.log",
),
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropOldest,
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
)
let application = build_application_async_logger(config)
let direct = build_async_logger(config)
application.info("skip")
application.error("one")
application.error("two")
application.error("three")
direct.info("skip")
direct.error("one")
direct.error("two")
direct.error("three")
inspect(application.pending_count() == direct.pending_count(), content="true")
inspect(application.dropped_count() == direct.dropped_count(), content="true")
inspect(
application.sink.pending_count() == direct.sink.pending_count(),
content="true",
)
inspect(
application.sink.dropped_count() == direct.sink.dropped_count(),
content="true",
)
inspect(
application.sink.file_available() == direct.sink.file_available(),
content="true",
)
inspect(
application.sink.file_append_mode() == direct.sink.file_append_mode(),
content="true",
)
inspect(
application.sink.file_auto_flush() == direct.sink.file_auto_flush(),
content="true",
)
let application_state = application.sink.file_state()
let direct_state = direct.sink.file_state()
inspect(application_state.path == direct_state.path, content="true")
inspect(application_state.available == direct_state.available, content="true")
inspect(application_state.append == direct_state.append, content="true")
inspect(
application_state.auto_flush == direct_state.auto_flush,
content="true",
)
inspect(
application_state.open_failures == direct_state.open_failures,
content="true",
)
inspect(
application_state.write_failures == direct_state.write_failures,
content="true",
)
inspect(
application_state.flush_failures == direct_state.flush_failures,
content="true",
)
inspect(
application_state.rotation_failures == direct_state.rotation_failures,
content="true",
)
match
(application.sink.file_runtime_state(), direct.sink.file_runtime_state()) {
(Some(snapshot), Some(other)) => {
inspect(snapshot.file.path == other.file.path, content="true")
inspect(snapshot.file.available == other.file.available, content="true")
inspect(snapshot.queued == other.queued, content="true")
inspect(snapshot.pending_count == other.pending_count, content="true")
inspect(snapshot.dropped_count == other.dropped_count, content="true")
}
_ => inspect(false, content="true")
}
inspect(
application.sink.file_set_append_mode(false) ==
direct.sink.file_set_append_mode(false),
content="true",
)
inspect(
application.sink.file_append_mode() == direct.sink.file_append_mode(),
content="true",
)
inspect(
application.sink.file_set_auto_flush(false) ==
direct.sink.file_set_auto_flush(false),
content="true",
)
inspect(
application.sink.file_auto_flush() == direct.sink.file_auto_flush(),
content="true",
)
inspect(
application.sink.file_set_rotation(
Some(@bitlogger.file_rotation(36, max_backups=2)),
) ==
direct.sink.file_set_rotation(
Some(@bitlogger.file_rotation(36, max_backups=2)),
),
content="true",
)
inspect(
application.sink.file_rotation_enabled() ==
direct.sink.file_rotation_enabled(),
content="true",
)
inspect(
application.sink.file_reopen_append() == direct.sink.file_reopen_append(),
content="true",
)
inspect(
application.sink.file_append_mode() == direct.sink.file_append_mode(),
content="true",
)
inspect(
application.sink.file_clear_rotation() == direct.sink.file_clear_rotation(),
content="true",
)
inspect(
application.sink.file_reset_policy() == direct.sink.file_reset_policy(),
content="true",
)
inspect(
application.sink.file_policy_matches_default() ==
direct.sink.file_policy_matches_default(),
content="true",
)
inspect(
application.sink.file_reset_failure_counters() ==
direct.sink.file_reset_failure_counters(),
content="true",
)
inspect(
application.sink.file_flush() == direct.sink.file_flush(),
content="true",
)
inspect(application.pending_count() == direct.pending_count(), content="true")
inspect(
application.sink.file_close() == direct.sink.file_close(),
content="true",
)
}
///|
async test "application async logger keeps async helper surface" {
let logger : ApplicationAsyncLogger = build_application_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.alias",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
let state = logger.state()
inspect(state.pending_count, content="0")
inspect(state.dropped_count, content="0")
inspect(state.is_closed, content="false")
inspect(state.is_running, content="false")
inspect(state.has_failed, content="false")
inspect(
match state.flush_policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
inspect(logger.pending_count(), content="2")
inspect(logger.dropped_count(), content="1")
inspect(logger.is_closed(), content="false")
inspect(logger.is_running(), content="false")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
}
///|
async test "application async logger shutdown clear abandons pending records" {
let logger : ApplicationAsyncLogger = build_application_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Info,
target="async.app.clear",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
),
),
)
logger.info("one")
inspect(logger.pending_count(), content="1")
inspect(logger.dropped_count(), content="0")
logger.shutdown(clear=true)
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
}
///|
async test "application async logger keeps broader async composition surface" {
let logger : ApplicationAsyncLogger = build_application_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.compose",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropNewest,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
.with_timestamp()
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.child("worker")
inspect(logger.target, content="async.app.compose.worker")
inspect(logger.timestamp, content="true")
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
logger.error("one")
logger.error("two")
inspect(logger.pending_count(), content="2")
inspect(logger.dropped_count(), content="0")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="0")
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
}
///|
async test "application async logger log supports per-call target override" {
let written_target : Ref[String] = Ref("")
let written_message : Ref[String] = Ref("")
let written_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let logger : ApplicationAsyncLogger = build_application_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.default",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.with_patch(fn(rec) {
written_target.val = rec.target
written_message.val = rec.message
written_fields.val = rec.fields
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
fields=[@bitlogger.field("mode", "test")],
target="async.app.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.app.override")
inspect(written_message.val, content="override")
inspect(written_fields.val.length(), content="2")
inspect(written_fields.val[0].key, content="service")
inspect(written_fields.val[0].value, content="bitlogger")
inspect(written_fields.val[1].key, content="mode")
inspect(written_fields.val[1].value, content="test")
let second_target : Ref[String] = Ref("")
let second_message : Ref[String] = Ref("")
let second_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let second : ApplicationAsyncLogger = build_application_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.default",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.with_patch(fn(rec) {
second_target.val = rec.target
second_message.val = rec.message
second_fields.val = rec.fields
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.log(@bitlogger.Level::Error, "default")
second.shutdown()
})
inspect(second_target.val, content="async.app.default")
inspect(second_message.val, content="default")
inspect(second_fields.val.length(), content="1")
inspect(second_fields.val[0].key, content="service")
}
///|
async test "application async logger severity helpers use stored target without override" {
let written_levels : Ref[Array[String]] = Ref([])
let written_targets : Ref[Array[String]] = Ref([])
let written_fields : Ref[Array[Array[@bitlogger.Field]]] = Ref([])
let logger : ApplicationAsyncLogger = build_application_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Info,
target="async.app.helpers",
sink=@bitlogger.SinkConfig::new(kind=@bitlogger.SinkKind::Console),
),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.with_patch(fn(rec) {
written_levels.val.push(rec.level.label())
written_targets.val.push(rec.target)
written_fields.val.push(rec.fields)
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("info", fields=[@bitlogger.field("mode", "info")])
logger.warn("warn", fields=[@bitlogger.field("mode", "warn")])
logger.error("error", fields=[@bitlogger.field("mode", "error")])
logger.shutdown()
})
inspect(written_levels.val.length(), content="3")
inspect(written_levels.val[0], content="INFO")
inspect(written_levels.val[1], content="WARN")
inspect(written_levels.val[2], content="ERROR")
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.app.helpers")
inspect(written_targets.val[1], content="async.app.helpers")
inspect(written_targets.val[2], content="async.app.helpers")
inspect(written_fields.val[0].length(), content="2")
inspect(written_fields.val[1].length(), content="2")
inspect(written_fields.val[2].length(), content="2")
inspect(written_fields.val[0][0].key, content="service")
inspect(written_fields.val[0][1].value, content="info")
inspect(written_fields.val[1][0].key, content="service")
inspect(written_fields.val[1][1].value, content="warn")
inspect(written_fields.val[2][0].key, content="service")
inspect(written_fields.val[2][1].value, content="error")
}
///|
test "application async logger builder preserves sync queue-backed runtime sink" {
let logger = build_application_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.queued",
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropNewest,
),
),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.app.queued")
inspect(logger.sink.pending_count(), content="0")
inspect(logger.sink.dropped_count(), content="0")
inspect(
match logger.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
_ => "Other"
},
content="QueuedConsole",
)
}
///|
test "application text async logger uses text facade build path" {
let logger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.app.text",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.app.text")
}
///|
async test "application text builder matches direct text builder behavior" {
let config = AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.text.same-build",
timestamp=true,
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropNewest,
),
),
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
template="TEXT:{target}:{message}",
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
)
let logger = build_application_text_async_logger(config)
let direct = build_async_text_logger(config)
let logger_state = logger.state()
let direct_state = direct.state()
inspect(logger.target, content=direct.target)
inspect(logger.timestamp == direct.timestamp, content="true")
inspect(
logger.is_enabled(@bitlogger.Level::Error) ==
direct.is_enabled(@bitlogger.Level::Error),
content="true",
)
inspect(
logger.is_enabled(@bitlogger.Level::Info) ==
direct.is_enabled(@bitlogger.Level::Info),
content="true",
)
inspect(
stringify_async_logger_state(logger_state) ==
stringify_async_logger_state(direct_state),
content="true",
)
let record = @bitlogger.Record::new(
@bitlogger.Level::Error,
"boom",
target="async.app.text.same-build",
)
inspect(
(logger.sink.formatter)(record) == (direct.sink.formatter)(record),
content="true",
)
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
direct.info("skip")
direct.error("one")
direct.error("two")
direct.error("three")
inspect(logger.pending_count() == direct.pending_count(), content="true")
inspect(logger.dropped_count() == direct.dropped_count(), content="true")
inspect(logger.is_closed() == direct.is_closed(), content="true")
inspect(logger.is_running() == direct.is_running(), content="true")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
group.spawn_bg(() => direct.run())
logger.shutdown()
direct.shutdown()
})
inspect(logger.is_closed() == direct.is_closed(), content="true")
inspect(logger.is_running() == direct.is_running(), content="true")
inspect(logger.pending_count() == direct.pending_count(), content="true")
inspect(logger.dropped_count() == direct.dropped_count(), content="true")
inspect(logger.has_failed() == direct.has_failed(), content="true")
inspect(logger.last_error() == direct.last_error(), content="true")
}
///|
async test "application text async logger keeps async helper surface" {
let logger : ApplicationTextAsyncLogger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.app.text.alias",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
let state = logger.state()
inspect(state.pending_count, content="0")
inspect(state.dropped_count, content="0")
inspect(state.is_closed, content="false")
inspect(state.is_running, content="false")
inspect(state.has_failed, content="false")
inspect(
match state.flush_policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
inspect(logger.pending_count(), content="2")
inspect(logger.dropped_count(), content="1")
inspect(logger.is_closed(), content="false")
inspect(logger.is_running(), content="false")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
}
///|
async test "application text async logger shutdown clear abandons pending records" {
let logger : ApplicationTextAsyncLogger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Info,
target="async.app.text.clear",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
),
),
)
logger.info("one")
inspect(logger.pending_count(), content="1")
inspect(logger.dropped_count(), content="0")
logger.shutdown(clear=true)
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
}
///|
async test "application text async logger keeps broader async composition surface" {
let logger : ApplicationTextAsyncLogger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.app.text.compose",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropNewest,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
.with_timestamp()
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.child("worker")
inspect(logger.target, content="async.app.text.compose.worker")
inspect(logger.timestamp, content="true")
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
logger.error("one")
logger.error("two")
inspect(logger.pending_count(), content="2")
inspect(logger.dropped_count(), content="0")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="0")
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
}
///|
async test "application text async logger log supports per-call target override" {
let written_target : Ref[String] = Ref("")
let written_message : Ref[String] = Ref("")
let written_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let logger : ApplicationTextAsyncLogger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.app.text.default",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.with_patch(fn(rec) {
written_target.val = rec.target
written_message.val = rec.message
written_fields.val = rec.fields
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
fields=[@bitlogger.field("mode", "test")],
target="async.app.text.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.app.text.override")
inspect(written_message.val, content="override")
inspect(written_fields.val.length(), content="2")
inspect(written_fields.val[0].key, content="service")
inspect(written_fields.val[0].value, content="bitlogger")
inspect(written_fields.val[1].key, content="mode")
inspect(written_fields.val[1].value, content="test")
let second_target : Ref[String] = Ref("")
let second_message : Ref[String] = Ref("")
let second_fields : Ref[Array[@bitlogger.Field]] = Ref([])
let second : ApplicationTextAsyncLogger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Warn,
target="async.app.text.default",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.with_patch(fn(rec) {
second_target.val = rec.target
second_message.val = rec.message
second_fields.val = rec.fields
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.log(@bitlogger.Level::Error, "default")
second.shutdown()
})
inspect(second_target.val, content="async.app.text.default")
inspect(second_message.val, content="default")
inspect(second_fields.val.length(), content="1")
inspect(second_fields.val[0].key, content="service")
}
///|
async test "application text async logger severity helpers use stored target without override" {
let written_levels : Ref[Array[String]] = Ref([])
let written_targets : Ref[Array[String]] = Ref([])
let written_fields : Ref[Array[Array[@bitlogger.Field]]] = Ref([])
let logger : ApplicationTextAsyncLogger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.text_console(
min_level=@bitlogger.Level::Info,
target="async.app.text.helpers",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
async_config=AsyncLoggerConfig::new(max_pending=4),
),
)
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.with_patch(fn(rec) {
written_levels.val.push(rec.level.label())
written_targets.val.push(rec.target)
written_fields.val.push(rec.fields)
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("info", fields=[@bitlogger.field("mode", "info")])
logger.warn("warn", fields=[@bitlogger.field("mode", "warn")])
logger.error("error", fields=[@bitlogger.field("mode", "error")])
logger.shutdown()
})
inspect(written_levels.val.length(), content="3")
inspect(written_levels.val[0], content="INFO")
inspect(written_levels.val[1], content="WARN")
inspect(written_levels.val[2], content="ERROR")
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.app.text.helpers")
inspect(written_targets.val[1], content="async.app.text.helpers")
inspect(written_targets.val[2], content="async.app.text.helpers")
inspect(written_fields.val[0].length(), content="2")
inspect(written_fields.val[1].length(), content="2")
inspect(written_fields.val[2].length(), content="2")
inspect(written_fields.val[0][0].key, content="service")
inspect(written_fields.val[0][1].value, content="info")
inspect(written_fields.val[1][0].key, content="service")
inspect(written_fields.val[1][1].value, content="warn")
inspect(written_fields.val[2][0].key, content="service")
inspect(written_fields.val[2][1].value, content="error")
}
///|
test "application text async logger builder ignores sync queue layer" {
let logger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.text.queued",
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropNewest,
),
),
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.app.text.queued")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="0")
}
///|
test "application text async logger ignores sink kind and still uses text formatter" {
let logger : ApplicationTextAsyncLogger = build_application_text_async_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.app.text.kind.alias",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
template="TEXT:{target}:{message}",
),
),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.app.text.kind.alias")
let rendered = (logger.sink.formatter)(
@bitlogger.Record::new(
@bitlogger.Level::Error,
"boom",
target="async.app.text.kind.alias",
),
)
inspect(rendered, content="TEXT:async.app.text.kind.alias:boom")
}
///|
test "build async text logger keeps text-console config fields" {
let logger = build_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.text.direct",
timestamp=true,
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=3,
overflow=AsyncOverflowPolicy::DropOldest,
),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.text.direct")
inspect(logger.timestamp, content="true")
inspect(
match logger.flush_policy() {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Never",
)
let rendered = (logger.sink.formatter)(
@bitlogger.Record::new(
@bitlogger.Level::Error,
"boom",
target="async.text.direct",
),
)
inspect(rendered, content="[ERROR] | [async.text.direct] | boom")
}
///|
test "build async text logger ignores sink kind and still uses text formatter" {
let logger = build_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.text.kind",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::File,
path="ignored.log",
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
template="TEXT:{target}:{message}",
),
),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.text.kind")
let rendered = (logger.sink.formatter)(
@bitlogger.Record::new(
@bitlogger.Level::Error,
"boom",
target="async.text.kind",
),
)
inspect(rendered, content="TEXT:async.text.kind:boom")
}
///|
async test "build async text logger keeps direct helper surface" {
let logger = build_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.text.helpers",
timestamp=true,
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::DropOldest,
flush=AsyncFlushPolicy::Shutdown,
),
),
)
let state = logger.state()
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.text.helpers")
inspect(logger.timestamp, content="true")
inspect(state.pending_count, content="0")
inspect(state.dropped_count, content="0")
inspect(state.is_closed, content="false")
inspect(state.is_running, content="false")
inspect(state.has_failed, content="false")
inspect(
match state.flush_policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
inspect(logger.pending_count(), content="2")
inspect(logger.dropped_count(), content="1")
inspect(logger.is_closed(), content="false")
inspect(logger.is_running(), content="false")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
}
///|
async test "build async text logger log supports per-call target override" {
let written_target : Ref[String] = Ref("")
let logger = build_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.text.builder.default",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
).with_patch(fn(rec) {
written_target.val = rec.target
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
target="async.text.builder.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.text.builder.override")
let second_target : Ref[String] = Ref("")
let second = build_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.text.builder.default",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=2,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
).with_patch(fn(rec) {
second_target.val = rec.target
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.error("default")
second.shutdown()
})
inspect(second_target.val, content="async.text.builder.default")
}
///|
async test "build async text logger severity helpers use stored target without override" {
let written_targets : Ref[Array[String]] = Ref([])
let logger = build_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Info,
target="async.text.builder.helpers",
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
flush=AsyncFlushPolicy::Never,
),
),
).with_patch(fn(rec) {
written_targets.val.push(rec.target)
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.info("first")
logger.warn("second")
logger.error("third")
logger.shutdown()
})
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.text.builder.helpers")
inspect(written_targets.val[1], content="async.text.builder.helpers")
inspect(written_targets.val[2], content="async.text.builder.helpers")
}
///|
async test "build async text logger batch flush policy keeps default no-op flush callback" {
let config = AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Info,
target="async.text.batch.flush",
timestamp=true,
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
template="TEXT:{target}:{message}",
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
max_batch=2,
flush=AsyncFlushPolicy::Batch,
),
)
let configured = build_async_text_logger(config)
let manual = async_logger(
@bitlogger.text_console_sink(
config.logger.sink.text_formatter.to_formatter(),
),
config=config.async_config,
min_level=config.logger.min_level,
target=config.logger.target,
flush=fn(_) -> Int raise {
raise TestFlushError("text batch flush exploded")
},
).with_timestamp(enabled=config.logger.timestamp)
@async.with_task_group(group => {
group.spawn_bg(() => configured.run())
group.spawn_bg(allow_failure=true, () => manual.run())
configured.error("one")
configured.error("two")
manual.error("one")
manual.error("two")
configured.wait_idle()
manual.wait_idle()
inspect(
match configured.flush_policy() {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Batch",
)
inspect(
match manual.flush_policy() {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Batch",
)
inspect(configured.has_failed(), content="false")
inspect(configured.last_error(), content="")
inspect(configured.pending_count(), content="0")
inspect(manual.has_failed(), content="true")
inspect(manual.last_error().contains("TestFlushError"), content="true")
configured.shutdown()
manual.shutdown(clear=true)
})
inspect(configured.is_closed(), content="true")
inspect(configured.is_running(), content="false")
inspect(configured.has_failed(), content="false")
inspect(configured.last_error(), content="")
inspect(configured.pending_count(), content="0")
inspect(manual.is_closed(), content="true")
inspect(manual.is_running(), content="false")
inspect(manual.has_failed(), content="true")
inspect(manual.last_error().contains("TestFlushError"), content="true")
}
///|
async test "build async text logger shutdown flush policy keeps default no-op flush callback" {
let config = AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Info,
target="async.text.shutdown.flush",
timestamp=true,
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
template="TEXT:{target}:{message}",
),
),
),
async_config=AsyncLoggerConfig::new(
max_pending=4,
overflow=AsyncOverflowPolicy::Blocking,
max_batch=2,
flush=AsyncFlushPolicy::Shutdown,
),
)
let configured = build_async_text_logger(config)
let manual = async_logger(
@bitlogger.text_console_sink(
config.logger.sink.text_formatter.to_formatter(),
),
config=config.async_config,
min_level=config.logger.min_level,
target=config.logger.target,
flush=fn(_) -> Int raise {
raise TestFlushError("text shutdown flush exploded")
},
).with_timestamp(enabled=config.logger.timestamp)
@async.with_task_group(group => {
group.spawn_bg(() => configured.run())
group.spawn_bg(allow_failure=true, () => manual.run())
configured.error("one")
configured.error("two")
manual.error("one")
manual.error("two")
configured.shutdown()
manual.shutdown()
})
inspect(
match configured.flush_policy() {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
inspect(
match manual.flush_policy() {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
inspect(configured.is_closed(), content="true")
inspect(configured.is_running(), content="false")
inspect(configured.has_failed(), content="false")
inspect(configured.last_error(), content="")
inspect(configured.pending_count(), content="0")
inspect(manual.is_closed(), content="true")
inspect(manual.is_running(), content="false")
inspect(manual.has_failed(), content="true")
inspect(manual.last_error().contains("TestFlushError"), content="true")
inspect(manual.pending_count(), content="0")
}
///|
test "build async text logger ignores sync queue layer" {
let logger = build_async_text_logger(
AsyncLoggerBuildConfig::new(
logger=@bitlogger.LoggerConfig::new(
min_level=@bitlogger.Level::Warn,
target="async.text.queued",
queue=Some(
@bitlogger.QueueConfig::new(
3,
overflow=@bitlogger.QueueOverflowPolicy::DropNewest,
),
),
sink=@bitlogger.SinkConfig::new(
kind=@bitlogger.SinkKind::TextConsole,
text_formatter=@bitlogger.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
),
async_config=AsyncLoggerConfig::new(max_pending=2),
),
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.text.queued")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="0")
}
///|
test "application async logger can be built from config text" {
let logger = parse_and_build_application_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.app.json")
}
///|
async test "application async parse-build matches parsed direct async builder behavior" {
let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.same-build\",\"timestamp\":true,\"queue\":{\"max_pending\":3,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}"
let application = parse_and_build_application_async_logger(raw)
let direct = build_async_logger(parse_async_logger_build_config_text(raw))
let application_state = application.state()
let direct_state = direct.state()
inspect(application.target, content=direct.target)
inspect(application.timestamp == direct.timestamp, content="true")
inspect(
application.is_enabled(@bitlogger.Level::Error) ==
direct.is_enabled(@bitlogger.Level::Error),
content="true",
)
inspect(
application.is_enabled(@bitlogger.Level::Info) ==
direct.is_enabled(@bitlogger.Level::Info),
content="true",
)
inspect(
stringify_async_logger_state(application_state) ==
stringify_async_logger_state(direct_state),
content="true",
)
let application_sink_kind = match application.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
@bitlogger.RuntimeSink::Console(_) => "Console"
@bitlogger.RuntimeSink::JsonConsole(_) => "JsonConsole"
@bitlogger.RuntimeSink::TextConsole(_) => "TextConsole"
@bitlogger.RuntimeSink::File(_) => "File"
}
let direct_sink_kind = match direct.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
@bitlogger.RuntimeSink::Console(_) => "Console"
@bitlogger.RuntimeSink::JsonConsole(_) => "JsonConsole"
@bitlogger.RuntimeSink::TextConsole(_) => "TextConsole"
@bitlogger.RuntimeSink::File(_) => "File"
}
inspect(application_sink_kind == direct_sink_kind, content="true")
application.info("skip")
application.error("one")
application.error("two")
application.error("three")
direct.info("skip")
direct.error("one")
direct.error("two")
direct.error("three")
inspect(application.pending_count() == direct.pending_count(), content="true")
inspect(application.dropped_count() == direct.dropped_count(), content="true")
inspect(application.is_closed() == direct.is_closed(), content="true")
inspect(application.is_running() == direct.is_running(), content="true")
inspect(
application.sink.pending_count() == direct.sink.pending_count(),
content="true",
)
inspect(
application.sink.dropped_count() == direct.sink.dropped_count(),
content="true",
)
@async.with_task_group(group => {
group.spawn_bg(() => application.run())
group.spawn_bg(() => direct.run())
application.shutdown()
direct.shutdown()
})
inspect(application.is_closed() == direct.is_closed(), content="true")
inspect(application.is_running() == direct.is_running(), content="true")
inspect(application.pending_count() == direct.pending_count(), content="true")
inspect(application.dropped_count() == direct.dropped_count(), content="true")
inspect(application.has_failed() == direct.has_failed(), content="true")
inspect(application.last_error() == direct.last_error(), content="true")
}
///|
async test "application async parse-build preserves file-backed runtime helpers" {
let raw = "{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.file.same-build\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropOldest\"},\"sink\":{\"kind\":\"file\",\"path\":\"async-app-json-file-same-build.log\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}"
let application = parse_and_build_application_async_logger(raw)
let direct = build_async_logger(parse_async_logger_build_config_text(raw))
application.info("skip")
application.error("one")
application.error("two")
application.error("three")
direct.info("skip")
direct.error("one")
direct.error("two")
direct.error("three")
inspect(application.pending_count() == direct.pending_count(), content="true")
inspect(application.dropped_count() == direct.dropped_count(), content="true")
inspect(
application.sink.pending_count() == direct.sink.pending_count(),
content="true",
)
inspect(
application.sink.dropped_count() == direct.sink.dropped_count(),
content="true",
)
inspect(
application.sink.file_available() == direct.sink.file_available(),
content="true",
)
inspect(
application.sink.file_append_mode() == direct.sink.file_append_mode(),
content="true",
)
inspect(
application.sink.file_auto_flush() == direct.sink.file_auto_flush(),
content="true",
)
let application_state = application.sink.file_state()
let direct_state = direct.sink.file_state()
inspect(application_state.path == direct_state.path, content="true")
inspect(application_state.available == direct_state.available, content="true")
inspect(application_state.append == direct_state.append, content="true")
inspect(
application_state.auto_flush == direct_state.auto_flush,
content="true",
)
inspect(
application_state.open_failures == direct_state.open_failures,
content="true",
)
inspect(
application_state.write_failures == direct_state.write_failures,
content="true",
)
inspect(
application_state.flush_failures == direct_state.flush_failures,
content="true",
)
inspect(
application_state.rotation_failures == direct_state.rotation_failures,
content="true",
)
match
(application.sink.file_runtime_state(), direct.sink.file_runtime_state()) {
(Some(snapshot), Some(other)) => {
inspect(snapshot.file.path == other.file.path, content="true")
inspect(snapshot.file.available == other.file.available, content="true")
inspect(snapshot.queued == other.queued, content="true")
inspect(snapshot.pending_count == other.pending_count, content="true")
inspect(snapshot.dropped_count == other.dropped_count, content="true")
}
_ => inspect(false, content="true")
}
inspect(
application.sink.file_set_append_mode(false) ==
direct.sink.file_set_append_mode(false),
content="true",
)
inspect(
application.sink.file_append_mode() == direct.sink.file_append_mode(),
content="true",
)
inspect(
application.sink.file_set_auto_flush(false) ==
direct.sink.file_set_auto_flush(false),
content="true",
)
inspect(
application.sink.file_auto_flush() == direct.sink.file_auto_flush(),
content="true",
)
inspect(
application.sink.file_set_rotation(
Some(@bitlogger.file_rotation(36, max_backups=2)),
) ==
direct.sink.file_set_rotation(
Some(@bitlogger.file_rotation(36, max_backups=2)),
),
content="true",
)
inspect(
application.sink.file_rotation_enabled() ==
direct.sink.file_rotation_enabled(),
content="true",
)
inspect(
application.sink.file_reopen_append() == direct.sink.file_reopen_append(),
content="true",
)
inspect(
application.sink.file_append_mode() == direct.sink.file_append_mode(),
content="true",
)
inspect(
application.sink.file_clear_rotation() == direct.sink.file_clear_rotation(),
content="true",
)
inspect(
application.sink.file_reset_policy() == direct.sink.file_reset_policy(),
content="true",
)
inspect(
application.sink.file_policy_matches_default() ==
direct.sink.file_policy_matches_default(),
content="true",
)
inspect(
application.sink.file_reset_failure_counters() ==
direct.sink.file_reset_failure_counters(),
content="true",
)
inspect(
application.sink.file_flush() == direct.sink.file_flush(),
content="true",
)
inspect(application.pending_count() == direct.pending_count(), content="true")
inspect(
application.sink.file_close() == direct.sink.file_close(),
content="true",
)
}
///|
async test "parsed application async logger keeps async helper surface" {
let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.helpers\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}",
)
let state = logger.state()
inspect(state.pending_count, content="0")
inspect(state.dropped_count, content="0")
inspect(state.is_closed, content="false")
inspect(state.is_running, content="false")
inspect(state.has_failed, content="false")
inspect(
match state.flush_policy {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
content="Shutdown",
)
logger.info("skip")
logger.error("one")
logger.error("two")
logger.error("three")
inspect(logger.pending_count(), content="2")
inspect(logger.dropped_count(), content="1")
inspect(logger.is_closed(), content="false")
inspect(logger.is_running(), content="false")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
}
///|
async test "parsed application async logger shutdown clear abandons pending records" {
let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger(
"{\"logger\":{\"min_level\":\"info\",\"target\":\"async.app.json.clear\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
logger.info("one")
inspect(logger.pending_count(), content="1")
inspect(logger.dropped_count(), content="0")
logger.shutdown(clear=true)
inspect(logger.is_closed(), content="true")
inspect(logger.is_running(), content="false")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="1")
}
///|
async test "parsed application async logger keeps broader async composition surface" {
let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.compose\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Shutdown\"}}",
)
.with_timestamp()
.with_context_fields([@bitlogger.field("service", "bitlogger")])
.child("worker")
inspect(logger.target, content="async.app.json.compose.worker")
inspect(logger.timestamp, content="true")
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
logger.error("one")
logger.error("two")
inspect(logger.pending_count(), content="2")
inspect(logger.dropped_count(), content="0")
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.shutdown()
})
inspect(logger.is_closed(), content="true")
inspect(logger.pending_count(), content="0")
inspect(logger.dropped_count(), content="0")
inspect(logger.has_failed(), content="false")
inspect(logger.last_error(), content="")
}
///|
async test "parsed application async logger log supports per-call target override" {
let written_target : Ref[String] = Ref("")
let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
).with_patch(fn(rec) {
written_target.val = rec.target
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.log(
@bitlogger.Level::Error,
"override",
target="async.app.json.override",
)
logger.shutdown()
})
inspect(written_target.val, content="async.app.json.override")
let second_target : Ref[String] = Ref("")
let second : ApplicationAsyncLogger = parse_and_build_application_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.default\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
).with_patch(fn(rec) {
second_target.val = rec.target
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => second.run())
second.error("default")
second.shutdown()
})
inspect(second_target.val, content="async.app.json.default")
}
///|
async test "parsed application async logger severity helpers use stored target without override" {
let written_targets : Ref[Array[String]] = Ref([])
let logger : ApplicationAsyncLogger = parse_and_build_application_async_logger(
"{\"logger\":{\"min_level\":\"trace\",\"target\":\"async.app.json.helpers.target\",\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":4,\"overflow\":\"Blocking\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
).with_patch(fn(rec) {
written_targets.val.push(rec.target)
rec
})
@async.with_task_group(group => {
group.spawn_bg(() => logger.run())
logger.debug("first")
logger.info("second")
logger.error("third")
logger.shutdown()
})
inspect(written_targets.val.length(), content="3")
inspect(written_targets.val[0], content="async.app.json.helpers.target")
inspect(written_targets.val[1], content="async.app.json.helpers.target")
inspect(written_targets.val[2], content="async.app.json.helpers.target")
}
///|
test "application async logger parse-and-build preserves parsed sync queue layer" {
let logger = parse_and_build_application_async_logger(
"{\"logger\":{\"min_level\":\"warn\",\"target\":\"async.app.json.queued\",\"queue\":{\"max_pending\":3,\"overflow\":\"DropNewest\"},\"sink\":{\"kind\":\"console\"}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropNewest\",\"max_batch\":1,\"linger_ms\":0,\"flush\":\"Never\"}}",
)
inspect(logger.is_enabled(@bitlogger.Level::Error), content="true")
inspect(logger.is_enabled(@bitlogger.Level::Info), content="false")
inspect(logger.target, content="async.app.json.queued")
inspect(
match logger.sink {
@bitlogger.RuntimeSink::QueuedConsole(_) => "QueuedConsole"
@bitlogger.RuntimeSink::QueuedJsonConsole(_) => "QueuedJsonConsole"
@bitlogger.RuntimeSink::QueuedTextConsole(_) => "QueuedTextConsole"
@bitlogger.RuntimeSink::QueuedFile(_) => "QueuedFile"
_ => "Other"
},
content="QueuedConsole",
)
}
///|
test "async parse-build facades forward parse failures" {
let raw = "{\"logger\":{\"sink\":{\"kind\":\"file\",\"path\":\"\"}},\"async_config\":{\"max_pending\":2}}"
let application_error = (fn() -> String raise {
ignore(parse_and_build_application_async_logger(raw))
"no error"
})() catch {
err => err.to_string()
}
inspect(application_error != "no error", content="true")
let library_error = (fn() -> String raise {
ignore(parse_and_build_library_async_logger(raw))
"no error"
})() catch {
err => err.to_string()
}
inspect(library_error != "no error", content="true")
}