mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-07-29 03:12:21 +00:00
🔊 Update 1.0.0
This commit is contained in:
+269
-154
@@ -1,3 +1,4 @@
|
||||
///|
|
||||
test "default logger can be reconfigured" {
|
||||
set_default_min_level(Level::Debug)
|
||||
set_default_target("global")
|
||||
@@ -6,6 +7,7 @@ test "default logger can be reconfigured" {
|
||||
inspect(logger.target, content="global")
|
||||
}
|
||||
|
||||
///|
|
||||
test "default logger snapshots shared defaults at creation time" {
|
||||
set_default_min_level(Level::Warn)
|
||||
set_default_target("before")
|
||||
@@ -24,24 +26,30 @@ test "default logger snapshots shared defaults at creation time" {
|
||||
inspect(after.target, content="after")
|
||||
}
|
||||
|
||||
///|
|
||||
test "logger can enable timestamps" {
|
||||
let logger = Logger::new(console_sink(), min_level=Level::Info, target="time")
|
||||
.with_timestamp()
|
||||
let logger = Logger::new(console_sink(), min_level=Level::Info, target="time").with_timestamp()
|
||||
inspect(logger.timestamp, content="true")
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter can customize visible parts" {
|
||||
let rec = record(
|
||||
Level::Info,
|
||||
"hello",
|
||||
timestamp_ms=123UL,
|
||||
target="svc.api",
|
||||
fields=[field("user", "alice"), field("request_id", "42")],
|
||||
let rec = record(Level::Info, "hello", timestamp_ms=123UL, target="svc.api", fields=[
|
||||
field("user", "alice"),
|
||||
field("request_id", "42"),
|
||||
])
|
||||
let compact = text_formatter(
|
||||
show_timestamp=false,
|
||||
show_target=false,
|
||||
field_separator=",",
|
||||
)
|
||||
inspect(
|
||||
format_text(rec, formatter=compact),
|
||||
content="[INFO] hello user=alice,request_id=42",
|
||||
)
|
||||
let compact = text_formatter(show_timestamp=false, show_target=false, field_separator=",")
|
||||
inspect(format_text(rec, formatter=compact), content="[INFO] hello user=alice,request_id=42")
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter can emit message only" {
|
||||
let rec = record(
|
||||
Level::Warn,
|
||||
@@ -59,6 +67,7 @@ test "text formatter can emit message only" {
|
||||
inspect(format_text(rec, formatter=message_only), content="just message")
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter supports template rendering" {
|
||||
let rec = record(
|
||||
Level::Info,
|
||||
@@ -73,20 +82,22 @@ test "text formatter supports template rendering" {
|
||||
template="[{level}] {target} {message} :: {fields} @{timestamp}",
|
||||
)
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="[INFO] svc.api template hello :: user=alice,request_id=42 @123",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter can render ansi level colors" {
|
||||
let rec = record(Level::Error, "boom", target="svc")
|
||||
let formatter = text_formatter(color_mode=ColorMode::Always)
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="[\u{001b}[31;1mERROR\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] boom",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter auto color respects NO_COLOR" {
|
||||
let rec = record(Level::Warn, "boom", target="svc")
|
||||
let previous = @env.get_env_var("NO_COLOR")
|
||||
@@ -101,6 +112,7 @@ test "text formatter auto color respects NO_COLOR" {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter renders named inline color tags in ansi mode" {
|
||||
let rec = record(Level::Info, "<red>boom</>", target="svc")
|
||||
inspect(
|
||||
@@ -109,6 +121,7 @@ test "text formatter renders named inline color tags in ansi mode" {
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter strips inline tags in plain mode" {
|
||||
let rec = record(Level::Info, "<red>boom</> <b>bold</>", target="svc")
|
||||
inspect(
|
||||
@@ -117,46 +130,83 @@ test "text formatter strips inline tags in plain mode" {
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter supports nested inline tags" {
|
||||
let rec = record(Level::Info, "<red><b>fatal</></>")
|
||||
inspect(
|
||||
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||
format_text(
|
||||
rec,
|
||||
formatter=text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
),
|
||||
),
|
||||
content="\u{001b}[31;1mfatal\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter supports named closing tags" {
|
||||
let rec = record(Level::Info, "<red>boom</red>")
|
||||
inspect(
|
||||
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||
format_text(
|
||||
rec,
|
||||
formatter=text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
),
|
||||
),
|
||||
content="\u{001b}[31mboom\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter supports mixed short and named closing tags" {
|
||||
let rec = record(Level::Info, "<red><b>fatal</b></red>")
|
||||
inspect(
|
||||
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||
format_text(
|
||||
rec,
|
||||
formatter=text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
),
|
||||
),
|
||||
content="\u{001b}[31;1mfatal\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter keeps unmatched named closing tags as plain text" {
|
||||
let rec = record(Level::Info, "boom</red>")
|
||||
inspect(
|
||||
format_text(rec, formatter=text_formatter(show_level=false, show_target=false)),
|
||||
format_text(
|
||||
rec,
|
||||
formatter=text_formatter(show_level=false, show_target=false),
|
||||
),
|
||||
content="boom</red>",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter supports hex inline colors" {
|
||||
let rec = record(Level::Info, "<#ff0000>hot</> <bg:#010203>bg</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||
format_text(
|
||||
rec,
|
||||
formatter=text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
),
|
||||
),
|
||||
content="\u{001b}[38;2;255;0;0mhot\u{001b}[0m \u{001b}[48;2;1;2;3mbg\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter can downgrade hex colors to basic ansi" {
|
||||
let rec = record(Level::Info, "<#ff0000>hot</> <bg:#010203>bg</>")
|
||||
inspect(
|
||||
@@ -173,14 +223,19 @@ test "text formatter can downgrade hex colors to basic ansi" {
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter keeps unknown inline tags as plain text" {
|
||||
let rec = record(Level::Info, "<unknown>boom</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=text_formatter(show_level=false, show_target=false)),
|
||||
format_text(
|
||||
rec,
|
||||
formatter=text_formatter(show_level=false, show_target=false),
|
||||
),
|
||||
content="<unknown>boom</>",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter can disable style markup parsing" {
|
||||
let rec = record(Level::Info, "<red>boom</> <b>bold</>", target="svc")
|
||||
inspect(
|
||||
@@ -192,6 +247,7 @@ test "text formatter can disable style markup parsing" {
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter builtin style markup mode ignores custom tags" {
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
@@ -203,17 +259,22 @@ test "text formatter builtin style markup mode ignores custom tags" {
|
||||
)
|
||||
let rec = record(Level::Info, "<brand>custom</> <red>builtin</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="<brand>custom</> \u{001b}[31mbuiltin\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "style markup mode label is stable" {
|
||||
inspect(style_markup_mode_label(StyleMarkupMode::Disabled), content="disabled")
|
||||
inspect(
|
||||
style_markup_mode_label(StyleMarkupMode::Disabled),
|
||||
content="disabled",
|
||||
)
|
||||
inspect(style_markup_mode_label(StyleMarkupMode::Builtin), content="builtin")
|
||||
inspect(style_markup_mode_label(StyleMarkupMode::Full), content="full")
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter supports custom style tags" {
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
@@ -224,83 +285,103 @@ test "text formatter supports custom style tags" {
|
||||
)
|
||||
let rec = record(Level::Info, "<accent>api</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="\u{001b}[38;2;76;201;240;1mapi\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter can override builtin style tags" {
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
).with_style_tags(
|
||||
default_style_tag_registry().set_tag("red", fg=Some("#ff5a5f"), underline=true),
|
||||
default_style_tag_registry().set_tag(
|
||||
"red",
|
||||
fg=Some("#ff5a5f"),
|
||||
underline=true,
|
||||
),
|
||||
)
|
||||
let rec = record(Level::Info, "<red>alert</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="\u{001b}[38;2;255;90;95;4malert\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "formatter style tags take priority over global style tags" {
|
||||
let previous = global_style_tag_registry()
|
||||
set_global_style_tag_registry(style_tag_registry().set_tag("accent", fg=Some("#123456")))
|
||||
set_global_style_tag_registry(
|
||||
style_tag_registry().set_tag("accent", fg=Some("#123456")),
|
||||
)
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
).with_style_tags(
|
||||
style_tag_registry().set_tag("accent", fg=Some("#abcdef")),
|
||||
)
|
||||
).with_style_tags(style_tag_registry().set_tag("accent", fg=Some("#abcdef")))
|
||||
let rec = record(Level::Info, "<accent>tag</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="\u{001b}[38;2;171;205;239mtag\u{001b}[0m",
|
||||
)
|
||||
set_global_style_tag_registry(previous)
|
||||
}
|
||||
|
||||
///|
|
||||
test "global style tags apply when formatter has no local registry" {
|
||||
let previous = global_style_tag_registry()
|
||||
set_global_style_tag_registry(style_tag_registry().set_tag("accent", fg=Some("#102030"), dim=true))
|
||||
set_global_style_tag_registry(
|
||||
style_tag_registry().set_tag("accent", fg=Some("#102030"), dim=true),
|
||||
)
|
||||
let rec = record(Level::Info, "<accent>tag</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always)),
|
||||
format_text(
|
||||
rec,
|
||||
formatter=text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
),
|
||||
),
|
||||
content="\u{001b}[38;2;16;32;48;2mtag\u{001b}[0m",
|
||||
)
|
||||
set_global_style_tag_registry(previous)
|
||||
}
|
||||
|
||||
///|
|
||||
test "reset global style tag registry restores builtin defaults" {
|
||||
set_global_style_tag_registry(style_tag_registry().set_tag("accent", fg=Some("#123456"), bold=true))
|
||||
set_global_style_tag_registry(
|
||||
style_tag_registry().set_tag("accent", fg=Some("#123456"), bold=true),
|
||||
)
|
||||
reset_global_style_tag_registry()
|
||||
let rec = record(Level::Info, "<success>ok</>")
|
||||
inspect(
|
||||
format_text(
|
||||
rec,
|
||||
formatter=text_formatter(show_level=false, show_target=false, color_mode=ColorMode::Always),
|
||||
formatter=text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
),
|
||||
),
|
||||
content="\u{001b}[32;1mok\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "style tag alias can reuse builtin tags" {
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
).with_style_tags(
|
||||
style_tag_registry().define_alias("danger", "red"),
|
||||
)
|
||||
).with_style_tags(style_tag_registry().define_alias("danger", "red"))
|
||||
let rec = record(Level::Info, "<danger>boom</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
content="\u{001b}[31mboom\u{001b}[0m",
|
||||
)
|
||||
inspect(format_text(rec, formatter~), content="\u{001b}[31mboom\u{001b}[0m")
|
||||
}
|
||||
|
||||
///|
|
||||
test "builtin semantic style tags are available" {
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
@@ -308,28 +389,37 @@ test "builtin semantic style tags are available" {
|
||||
color_mode=ColorMode::Always,
|
||||
style_markup=StyleMarkupMode::Builtin,
|
||||
)
|
||||
let rec = record(Level::Info, "<success>ok</> <warning>careful</> <danger>boom</> <muted>quiet</>")
|
||||
let rec = record(
|
||||
Level::Info,
|
||||
"<success>ok</> <warning>careful</> <danger>boom</> <muted>quiet</>",
|
||||
)
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="\u{001b}[32;1mok\u{001b}[0m \u{001b}[33;1mcareful\u{001b}[0m \u{001b}[31;1mboom\u{001b}[0m \u{001b}[90;2mquiet\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "builtin semantic style tags can still be overridden" {
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
show_target=false,
|
||||
color_mode=ColorMode::Always,
|
||||
).with_style_tags(
|
||||
default_style_tag_registry().set_tag("success", fg=Some("#00ffaa"), underline=true),
|
||||
default_style_tag_registry().set_tag(
|
||||
"success",
|
||||
fg=Some("#00ffaa"),
|
||||
underline=true,
|
||||
),
|
||||
)
|
||||
let rec = record(Level::Info, "<success>ok</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="\u{001b}[38;2;0;255;170;4mok\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter can enable markup for target separately" {
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
@@ -338,11 +428,12 @@ test "text formatter can enable markup for target separately" {
|
||||
)
|
||||
let rec = record(Level::Info, "hello", target="<danger>svc</>")
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="[\u{001b}[34m\u{001b}[31;1msvc\u{001b}[0m\u{001b}[0m] hello",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter can enable markup for field values separately" {
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
@@ -350,13 +441,16 @@ test "text formatter can enable markup for field values separately" {
|
||||
color_mode=ColorMode::Always,
|
||||
fields_style_markup=StyleMarkupMode::Builtin,
|
||||
)
|
||||
let rec = record(Level::Info, "hello", fields=[field("status", "<success>ok</>")])
|
||||
let rec = record(Level::Info, "hello", fields=[
|
||||
field("status", "<success>ok</>"),
|
||||
])
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="hello \u{001b}[35mstatus=\u{001b}[32;1mok\u{001b}[0m\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter leaves field keys raw when field markup is enabled" {
|
||||
let formatter = text_formatter(
|
||||
show_level=false,
|
||||
@@ -364,13 +458,16 @@ test "text formatter leaves field keys raw when field markup is enabled" {
|
||||
color_mode=ColorMode::Always,
|
||||
fields_style_markup=StyleMarkupMode::Builtin,
|
||||
)
|
||||
let rec = record(Level::Info, "hello", fields=[field("<danger>status</>", "ok")])
|
||||
let rec = record(Level::Info, "hello", fields=[
|
||||
field("<danger>status</>", "ok"),
|
||||
])
|
||||
inspect(
|
||||
format_text(rec, formatter=formatter),
|
||||
format_text(rec, formatter~),
|
||||
content="hello \u{001b}[35m<danger>status</>=ok\u{001b}[0m",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "text formatter template respects disabled fields" {
|
||||
let rec = record(Level::Warn, "just message", target="svc")
|
||||
let formatter = text_formatter(
|
||||
@@ -378,26 +475,30 @@ test "text formatter template respects disabled fields" {
|
||||
show_fields=false,
|
||||
template="[{level}] {target}{message}{fields}",
|
||||
)
|
||||
inspect(format_text(rec, formatter=formatter), content="[WARN] just message")
|
||||
inspect(format_text(rec, formatter~), content="[WARN] just message")
|
||||
}
|
||||
|
||||
///|
|
||||
test "formatted callback sink receives rendered text" {
|
||||
let rendered : Ref[String] = Ref("")
|
||||
let sink = text_callback_sink(
|
||||
text_formatter(show_timestamp=false, separator=" | "),
|
||||
fn(text) {
|
||||
rendered.val = text
|
||||
},
|
||||
fn(text) { rendered.val = text },
|
||||
)
|
||||
let logger = Logger::new(sink, min_level=Level::Info, target="svc")
|
||||
logger.info("hello", fields=[field("user", "alice")])
|
||||
inspect(rendered.val, content="[INFO] | [svc] | hello | user=alice")
|
||||
}
|
||||
|
||||
///|
|
||||
test "native file support flag is queryable" {
|
||||
inspect(native_files_supported() == true || native_files_supported() == false, content="true")
|
||||
inspect(
|
||||
native_files_supported() == true || native_files_supported() == false,
|
||||
content="true",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "file sink availability reflects backend support" {
|
||||
let sink = file_sink("bitlogger-test.log")
|
||||
let state = sink.state()
|
||||
@@ -407,12 +508,18 @@ test "file sink availability reflects backend support" {
|
||||
inspect(state.available == sink.is_available(), content="true")
|
||||
inspect(state.append == sink.append_mode(), content="true")
|
||||
inspect(state.auto_flush == sink.auto_flush_enabled(), content="true")
|
||||
inspect((state.rotation is None) == (sink.rotation_config() is None), content="true")
|
||||
inspect(
|
||||
(state.rotation is None) == (sink.rotation_config() is None),
|
||||
content="true",
|
||||
)
|
||||
inspect(sink.append_mode(), content="true")
|
||||
inspect(sink.auto_flush_enabled(), content="true")
|
||||
inspect(sink.rotation_enabled(), content="false")
|
||||
inspect(sink.rotation_config() is None, content="true")
|
||||
inspect(sink.open_failures(), content=if sink.is_available() { "0" } else { "1" })
|
||||
inspect(
|
||||
sink.open_failures(),
|
||||
content=if sink.is_available() { "0" } else { "1" },
|
||||
)
|
||||
inspect(sink.write_failures(), content="0")
|
||||
inspect(sink.flush_failures(), content="0")
|
||||
if sink.is_available() {
|
||||
@@ -424,8 +531,12 @@ test "file sink availability reflects backend support" {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "file sink unavailable backend keeps stable fallback state" {
|
||||
let sink = file_sink("bitlogger-unavailable.log", rotation=Some(file_rotation(64, max_backups=2)))
|
||||
let sink = file_sink(
|
||||
"bitlogger-unavailable.log",
|
||||
rotation=Some(file_rotation(64, max_backups=2)),
|
||||
)
|
||||
if sink.is_available() {
|
||||
inspect(sink.state().available, content="true")
|
||||
ignore(sink.close())
|
||||
@@ -449,6 +560,7 @@ test "file sink unavailable backend keeps stable fallback state" {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "file sink rotation config normalizes invalid inputs" {
|
||||
let rotation = file_rotation(0, max_backups=0)
|
||||
inspect(rotation.max_bytes, content="1")
|
||||
@@ -464,6 +576,7 @@ test "file sink rotation config normalizes invalid inputs" {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "file sink setters update auto flush and rotation state" {
|
||||
let sink = file_sink("bitlogger-setters.log")
|
||||
let default_policy = sink.default_policy()
|
||||
@@ -490,7 +603,10 @@ test "file sink setters update auto flush and rotation state" {
|
||||
sink.clear_rotation()
|
||||
inspect(sink.rotation_enabled(), content="false")
|
||||
inspect(sink.rotation_config() is None, content="true")
|
||||
inspect(sink.reopen(), content=if sink.is_available() { "true" } else { "false" })
|
||||
inspect(
|
||||
sink.reopen(),
|
||||
content=if sink.is_available() { "true" } else { "false" },
|
||||
)
|
||||
inspect(sink.append_mode(), content="false")
|
||||
let state = sink.state()
|
||||
let policy = sink.policy()
|
||||
@@ -508,6 +624,7 @@ test "file sink setters update auto flush and rotation state" {
|
||||
inspect(sink.policy_matches_default(), content="true")
|
||||
}
|
||||
|
||||
///|
|
||||
test "file sink reset policy restores configured defaults" {
|
||||
let sink = file_sink(
|
||||
"bitlogger-reset-policy.log",
|
||||
@@ -535,6 +652,7 @@ test "file sink reset policy restores configured defaults" {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "file sink set policy applies bundled runtime policy" {
|
||||
let sink = file_sink("bitlogger-set-policy.log")
|
||||
let default_policy = sink.default_policy()
|
||||
@@ -561,6 +679,7 @@ test "file sink set policy applies bundled runtime policy" {
|
||||
inspect(sink.policy_matches_default(), content="false")
|
||||
}
|
||||
|
||||
///|
|
||||
test "file sink tracks rotation failures on unavailable backend" {
|
||||
let path = "bitlogger-rotate.log"
|
||||
ignore(remove_file_internal(path))
|
||||
@@ -579,6 +698,7 @@ test "file sink tracks rotation failures on unavailable backend" {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "file sink reopen and failure counters reflect backend state" {
|
||||
let sink = file_sink("bitlogger-reopen.log")
|
||||
if sink.is_available() {
|
||||
@@ -630,6 +750,7 @@ test "file sink reopen and failure counters reflect backend state" {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "file sink can rotate on native backend" {
|
||||
let path = "bitlogger-rotate-native.log"
|
||||
ignore(remove_file_internal(path))
|
||||
@@ -666,20 +787,18 @@ test "file sink can rotate on native backend" {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "json formatter keeps structured shape" {
|
||||
let rec = record(
|
||||
Level::Error,
|
||||
"failed",
|
||||
timestamp_ms=55UL,
|
||||
target="svc",
|
||||
fields=[field("code", "500")],
|
||||
)
|
||||
let rec = record(Level::Error, "failed", timestamp_ms=55UL, target="svc", fields=[
|
||||
field("code", "500"),
|
||||
])
|
||||
inspect(
|
||||
format_json(rec),
|
||||
content="{\"level\":\"ERROR\",\"message\":\"failed\",\"fields\":{\"code\":\"500\"},\"timestamp_ms\":\"55\",\"target\":\"svc\"}",
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
test "callback sink receives record" {
|
||||
let captured_target : Ref[String] = Ref("")
|
||||
let captured_message : Ref[String] = Ref("")
|
||||
@@ -696,20 +815,15 @@ test "callback sink receives record" {
|
||||
inspect(captured_message.val, content="hello")
|
||||
}
|
||||
|
||||
///|
|
||||
test "split sink routes records by predicate" {
|
||||
let left_messages : Ref[Array[String]] = Ref([])
|
||||
let right_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
split_sink(
|
||||
callback_sink(fn(rec) {
|
||||
left_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) {
|
||||
right_messages.val.push(rec.message)
|
||||
}),
|
||||
fn(rec) {
|
||||
rec.target == "audit"
|
||||
},
|
||||
callback_sink(fn(rec) { left_messages.val.push(rec.message) }),
|
||||
callback_sink(fn(rec) { right_messages.val.push(rec.message) }),
|
||||
fn(rec) { rec.target == "audit" },
|
||||
),
|
||||
min_level=Level::Info,
|
||||
target="main",
|
||||
@@ -722,6 +836,7 @@ test "split sink routes records by predicate" {
|
||||
inspect(right_messages.val[0], content="drop to right")
|
||||
}
|
||||
|
||||
///|
|
||||
test "split_by_level routes warn and error separately" {
|
||||
let high_messages : Ref[Array[String]] = Ref([])
|
||||
let low_messages : Ref[Array[String]] = Ref([])
|
||||
@@ -748,21 +863,22 @@ test "split_by_level routes warn and error separately" {
|
||||
inspect(low_messages.val[0], content="INFO:info")
|
||||
}
|
||||
|
||||
///|
|
||||
test "callback sink sees child target and context logger shape" {
|
||||
let captured_target : Ref[String] = Ref("")
|
||||
let captured_message : Ref[String] = Ref("")
|
||||
let captured_field_count : Ref[Int] = Ref(0)
|
||||
let captured_timestamp : Ref[UInt64] = Ref(0UL)
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) {
|
||||
captured_target.val = rec.target
|
||||
captured_message.val = rec.message
|
||||
captured_field_count.val = rec.fields.length()
|
||||
captured_timestamp.val = rec.timestamp_ms
|
||||
}),
|
||||
min_level=Level::Info,
|
||||
target="app",
|
||||
)
|
||||
callback_sink(fn(rec) {
|
||||
captured_target.val = rec.target
|
||||
captured_message.val = rec.message
|
||||
captured_field_count.val = rec.fields.length()
|
||||
captured_timestamp.val = rec.timestamp_ms
|
||||
}),
|
||||
min_level=Level::Info,
|
||||
target="app",
|
||||
)
|
||||
.child("worker")
|
||||
.with_context_fields([field("service", "bitlogger")])
|
||||
.with_timestamp()
|
||||
@@ -773,6 +889,7 @@ test "callback sink sees child target and context logger shape" {
|
||||
inspect(captured_timestamp.val > 0UL, content="true")
|
||||
}
|
||||
|
||||
///|
|
||||
test "bind aliases context fields ergonomically" {
|
||||
let captured_target : Ref[String] = Ref("")
|
||||
let captured_message : Ref[String] = Ref("")
|
||||
@@ -798,12 +915,11 @@ test "bind aliases context fields ergonomically" {
|
||||
inspect(captured_fields.val[2].value, content="test")
|
||||
}
|
||||
|
||||
///|
|
||||
test "buffered sink flushes manually" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = buffered_sink(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
flush_limit=10,
|
||||
)
|
||||
let logger = Logger::new(sink, min_level=Level::Info, target="buffered")
|
||||
@@ -818,12 +934,11 @@ test "buffered sink flushes manually" {
|
||||
inspect(flushed_messages.val[1], content="two")
|
||||
}
|
||||
|
||||
///|
|
||||
test "buffered sink flushes automatically at limit" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = buffered_sink(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
flush_limit=2,
|
||||
)
|
||||
let logger = Logger::new(sink, min_level=Level::Info, target="buffered")
|
||||
@@ -836,15 +951,12 @@ test "buffered sink flushes automatically at limit" {
|
||||
inspect(flushed_messages.val[1], content="two")
|
||||
}
|
||||
|
||||
///|
|
||||
test "filter sink only forwards matching records" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = filter_sink(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
fn(rec) {
|
||||
rec.target == "kept"
|
||||
},
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
fn(rec) { rec.target == "kept" },
|
||||
)
|
||||
let kept = Logger::new(sink, min_level=Level::Info, target="kept")
|
||||
let dropped = Logger::new(sink, min_level=Level::Info, target="dropped")
|
||||
@@ -856,37 +968,34 @@ test "filter sink only forwards matching records" {
|
||||
inspect(flushed_messages.val[1], content="three")
|
||||
}
|
||||
|
||||
///|
|
||||
test "logger with_filter composes naturally" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Info,
|
||||
target="app",
|
||||
)
|
||||
.with_filter(fn(rec) {
|
||||
rec.target == "app.worker"
|
||||
})
|
||||
).with_filter(fn(rec) { rec.target == "app.worker" })
|
||||
logger.info("drop at app")
|
||||
logger.child("worker").info("keep at worker")
|
||||
inspect(flushed_messages.val.length(), content="1")
|
||||
inspect(flushed_messages.val[0], content="keep at worker")
|
||||
}
|
||||
|
||||
///|
|
||||
test "filter helpers support target level and message composition" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Trace,
|
||||
target="service",
|
||||
).with_filter(all_of([
|
||||
target_has_prefix("service"),
|
||||
level_at_least(Level::Info),
|
||||
message_contains("visible"),
|
||||
]))
|
||||
).with_filter(
|
||||
all_of([
|
||||
target_has_prefix("service"),
|
||||
level_at_least(Level::Info),
|
||||
message_contains("visible"),
|
||||
]),
|
||||
)
|
||||
logger.debug("visible debug")
|
||||
logger.info("hidden info")
|
||||
logger.child("api").info("visible info")
|
||||
@@ -894,39 +1003,46 @@ test "filter helpers support target level and message composition" {
|
||||
inspect(flushed_messages.val[0], content="visible info")
|
||||
}
|
||||
|
||||
///|
|
||||
test "field helpers can match and negate records" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Info,
|
||||
target="fields",
|
||||
).with_filter(all_of([
|
||||
has_field("request_id"),
|
||||
field_equals("kind", "audit"),
|
||||
not_(target_is("fields.drop")),
|
||||
]))
|
||||
).with_filter(
|
||||
all_of([
|
||||
has_field("request_id"),
|
||||
field_equals("kind", "audit"),
|
||||
not_(target_is("fields.drop")),
|
||||
]),
|
||||
)
|
||||
logger.info("missing field")
|
||||
logger.info("wrong kind", fields=[field("request_id", "1"), field("kind", "trace")])
|
||||
logger.child("drop").info("blocked target", fields=[field("request_id", "2"), field("kind", "audit")])
|
||||
logger.info("wrong kind", fields=[
|
||||
field("request_id", "1"),
|
||||
field("kind", "trace"),
|
||||
])
|
||||
logger
|
||||
.child("drop")
|
||||
.info("blocked target", fields=[
|
||||
field("request_id", "2"),
|
||||
field("kind", "audit"),
|
||||
])
|
||||
logger.info("kept", fields=[field("request_id", "3"), field("kind", "audit")])
|
||||
inspect(flushed_messages.val.length(), content="1")
|
||||
inspect(flushed_messages.val[0], content="kept")
|
||||
}
|
||||
|
||||
///|
|
||||
test "any_of helper accepts multiple predicates" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Info,
|
||||
target="multi",
|
||||
).with_filter(any_of([
|
||||
target_is("multi.keep"),
|
||||
field_equals("force", "true"),
|
||||
]))
|
||||
).with_filter(
|
||||
any_of([target_is("multi.keep"), field_equals("force", "true")]),
|
||||
)
|
||||
logger.info("drop")
|
||||
logger.child("keep").info("keep by target")
|
||||
logger.info("keep by field", fields=[field("force", "true")])
|
||||
@@ -935,6 +1051,7 @@ test "any_of helper accepts multiple predicates" {
|
||||
inspect(flushed_messages.val[1], content="keep by field")
|
||||
}
|
||||
|
||||
///|
|
||||
test "patch sink can rewrite message target and fields" {
|
||||
let captured_target : Ref[String] = Ref("")
|
||||
let captured_message : Ref[String] = Ref("")
|
||||
@@ -947,12 +1064,14 @@ test "patch sink can rewrite message target and fields" {
|
||||
}),
|
||||
min_level=Level::Info,
|
||||
target="auth",
|
||||
).with_patch(compose_patches([
|
||||
set_target("audit.auth"),
|
||||
prefix_message("[safe] "),
|
||||
redact_field("token"),
|
||||
append_fields([field("service", "bitlogger")]),
|
||||
]))
|
||||
).with_patch(
|
||||
compose_patches([
|
||||
set_target("audit.auth"),
|
||||
prefix_message("[safe] "),
|
||||
redact_field("token"),
|
||||
append_fields([field("service", "bitlogger")]),
|
||||
]),
|
||||
)
|
||||
logger.info("login", fields=[field("token", "secret"), field("user", "alice")])
|
||||
inspect(captured_target.val, content="audit.auth")
|
||||
inspect(captured_message.val, content="[safe] login")
|
||||
@@ -965,31 +1084,30 @@ test "patch sink can rewrite message target and fields" {
|
||||
inspect(captured_fields.val[2].value, content="bitlogger")
|
||||
}
|
||||
|
||||
///|
|
||||
test "patch helpers can redact multiple fields" {
|
||||
let captured_fields : Ref[Array[Field]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) {
|
||||
captured_fields.val = rec.fields
|
||||
}),
|
||||
callback_sink(fn(rec) { captured_fields.val = rec.fields }),
|
||||
min_level=Level::Info,
|
||||
target="audit",
|
||||
).with_patch(redact_fields(["token", "password"], placeholder="[redacted]"))
|
||||
logger.info(
|
||||
"credentials",
|
||||
fields=[field("token", "abc"), field("password", "123"), field("user", "alice")],
|
||||
)
|
||||
logger.info("credentials", fields=[
|
||||
field("token", "abc"),
|
||||
field("password", "123"),
|
||||
field("user", "alice"),
|
||||
])
|
||||
inspect(captured_fields.val.length(), content="3")
|
||||
inspect(captured_fields.val[0].value, content="[redacted]")
|
||||
inspect(captured_fields.val[1].value, content="[redacted]")
|
||||
inspect(captured_fields.val[2].value, content="alice")
|
||||
}
|
||||
|
||||
///|
|
||||
test "queued sink drains in order" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = queued_sink(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
)
|
||||
let logger = Logger::new(sink, min_level=Level::Info, target="queue")
|
||||
logger.info("one")
|
||||
@@ -1007,12 +1125,11 @@ test "queued sink drains in order" {
|
||||
inspect(flushed_messages.val[2], content="three")
|
||||
}
|
||||
|
||||
///|
|
||||
test "queued sink can drop newest when full" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = queued_sink(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
max_pending=2,
|
||||
overflow=QueueOverflowPolicy::DropNewest,
|
||||
)
|
||||
@@ -1028,12 +1145,11 @@ test "queued sink can drop newest when full" {
|
||||
inspect(flushed_messages.val[1], content="two")
|
||||
}
|
||||
|
||||
///|
|
||||
test "queued sink can drop oldest when full" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let sink = queued_sink(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
max_pending=2,
|
||||
overflow=QueueOverflowPolicy::DropOldest,
|
||||
)
|
||||
@@ -1049,15 +1165,14 @@ test "queued sink can drop oldest when full" {
|
||||
inspect(flushed_messages.val[1], content="three")
|
||||
}
|
||||
|
||||
///|
|
||||
test "logger with_queue preserves chaining ergonomics" {
|
||||
let flushed_messages : Ref[Array[String]] = Ref([])
|
||||
let logger = Logger::new(
|
||||
callback_sink(fn(rec) {
|
||||
flushed_messages.val.push(rec.message)
|
||||
}),
|
||||
min_level=Level::Info,
|
||||
target="service",
|
||||
)
|
||||
callback_sink(fn(rec) { flushed_messages.val.push(rec.message) }),
|
||||
min_level=Level::Info,
|
||||
target="service",
|
||||
)
|
||||
.with_patch(prefix_message("[queued] "))
|
||||
.with_queue(max_pending=2, overflow=QueueOverflowPolicy::DropOldest)
|
||||
logger.info("one")
|
||||
|
||||
Reference in New Issue
Block a user