3 Commits

Author SHA1 Message Date
Nanaloveyuki f44a4794ec 🩹 Fix the major version must be 0 2026-06-27 10:51:37 +08:00
Nanaloveyuki 0cbe25a551 🔊 Update 1.0.0 2026-06-27 10:45:36 +08:00
Nanaloveyuki 4cc43def73 ⬆️ Update Async and Mbt Version 2026-06-27 10:44:51 +08:00
58 changed files with 5754 additions and 2377 deletions
+26
View File
@@ -0,0 +1,26 @@
## BitLogger Update Changes
version 1.0.0(f0.6.0)
### Migration
- migration: run the MoonBit 0.10 formatter migration across the repository and adopt the current `trait` / `impl` formatting and `fn`-prefixed impl method style
- migration: replace the legacy root manifest with `moon.mod`, aligning the package root with the current MoonBit manifest format
- migration: update the async dependency to `moonbitlang/async@0.20.0`
### Docs
- docs: refresh the README example and project command notes after the formatter migration so examples match the current MoonBit surface
- docs: document that `async_basic` depends on `moonbitlang/async`, whose upstream README currently only claims native/LLVM support on Linux/MacOS
- docs: narrow the repository prompt validation guidance so Windows-local `src-async --target native` failures are not treated as BitLogger regressions unless reproduced on a supported native platform
### Verification
- verify: keep the main package passing `moon check`, `moon test`, `moon check --target native`, `moon check --target wasm-gc`, and `moon check --target js`
- verify: keep `src-async` passing `moon check` / `moon test` on `wasm-gc` and `js`, and confirm the native async path on a supported environment baseline
- verify: keep the example path healthy with `moon run examples/basic` and `moon check examples/async_basic --target native`
### Notes
- this release marks the MoonBit 0.10 generation as the new BitLogger baseline
- the repository now treats Linux/MacOS as the supported native baseline for `moonbitlang/async` until upstream Windows native support is stated clearly
+1
View File
@@ -2,6 +2,7 @@
Versioned BitLogger change summaries.
- [1.0.0](./1.0.0.md)
- [0.5.3](./0.5.3.md)
- [0.5.2](./0.5.2.md)
- [0.5.1](./0.5.1.md)
+16 -6
View File
@@ -1,6 +1,14 @@
///|
async fn main {
println("examples/async_basic is shipped as a native-only example because async fn main entry support is still limited on non-native targets, even though src-async itself keeps compatibility behavior.")
println(@lib_async.stringify_async_runtime_state(@lib_async.async_runtime_state(), pretty=true))
println(
"examples/async_basic is shipped as a native-only example because async fn main entry support is still limited on non-native targets, even though src-async itself keeps compatibility behavior.",
)
println(
@lib_async.stringify_async_runtime_state(
@lib_async.async_runtime_state(),
pretty=true,
),
)
let raw = "{\"logger\":{\"min_level\":\"info\",\"target\":\"async.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \"}}},\"async_config\":{\"max_pending\":2,\"overflow\":\"DropOldest\",\"max_batch\":4,\"linger_ms\":5,\"flush\":\"Batch\"}}"
let config = @lib_async.parse_async_logger_build_config_text(raw) catch {
@@ -14,10 +22,12 @@ async fn main {
let logger = @lib_async.build_async_logger(config)
.with_context_fields([@lib.field("service", "bitlogger")])
.with_filter(@lib.target_has_prefix("async"))
.with_patch(@lib.compose_patches([
@lib.prefix_message("[async] "),
@lib.redact_fields(["token"]),
]))
.with_patch(
@lib.compose_patches([
@lib.prefix_message("[async] "),
@lib.redact_fields(["token"]),
]),
)
println(@lib_async.stringify_async_logger_state(logger.state(), pretty=true))
+1 -1
View File
@@ -1,7 +1,7 @@
import {
"Nanaloveyuki/BitLogger/src" @lib,
"Nanaloveyuki/BitLogger/src-async" @lib_async,
"moonbitlang/async" @async,
"moonbitlang/async",
}
supported_targets = "+native"
+74 -32
View File
@@ -1,10 +1,14 @@
///|
fn main {
let preset_logger = @lib.build_logger(
@lib.with_queue(
@lib.text_console(
min_level=@lib.Level::Info,
target="preset",
text_formatter=@lib.TextFormatterConfig::new(show_timestamp=false, separator=" | "),
text_formatter=@lib.TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
max_pending=2,
overflow=@lib.QueueOverflowPolicy::DropOldest,
@@ -20,8 +24,11 @@ fn main {
@lib.info("hello from BitLogger", fields=[@lib.field("mode", "demo")])
// Direct Logger::new(...) composition stays useful for custom sink graphs.
let logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Trace, target="custom")
.with_context_fields([@lib.field("service", "bitlogger")])
let logger = @lib.Logger::new(
@lib.console_sink(),
min_level=@lib.Level::Trace,
target="custom",
).with_context_fields([@lib.field("service", "bitlogger")])
logger.debug("custom logger ready", fields=[@lib.field("sink", "console")])
let bound_logger = @lib.Logger::new(
@@ -31,7 +38,11 @@ fn main {
).bind(@lib.fields([("service", "bitlogger"), ("scope", "audit")]))
bound_logger.info("bound logger ready", fields=[@lib.field("mode", "demo")])
let json_logger = @lib.Logger::new(@lib.json_console_sink(), min_level=@lib.Level::Info, target="json")
let json_logger = @lib.Logger::new(
@lib.json_console_sink(),
min_level=@lib.Level::Info,
target="json",
)
json_logger.info("json output", fields=[@lib.field("kind", "example")])
let fanout_logger = @lib.Logger::new(
@@ -55,12 +66,18 @@ fn main {
split_logger.info("normal output")
split_logger.warn("warning output")
let timed_logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Info, target="timed")
.with_timestamp()
let timed_logger = @lib.Logger::new(
@lib.console_sink(),
min_level=@lib.Level::Info,
target="timed",
).with_timestamp()
timed_logger.info("timestamp enabled", fields=[@lib.field("kind", "time")])
let child_logger = @lib.Logger::new(@lib.console_sink(), min_level=@lib.Level::Info, target="app")
.child("worker")
let child_logger = @lib.Logger::new(
@lib.console_sink(),
min_level=@lib.Level::Info,
target="app",
).child("worker")
child_logger.info("child target ready")
let callback_logger = @lib.Logger::new(
@@ -90,8 +107,8 @@ fn main {
color_mode=@lib.ColorMode::Always,
).with_style_tags(
@lib.default_style_tag_registry()
.set_tag("accent", fg=Some("#4cc9f0"), bold=true)
.define_alias("danger", "red"),
.set_tag("accent", fg=Some("#4cc9f0"), bold=true)
.define_alias("danger", "red"),
)
let styled_logger = @lib.Logger::new(
@lib.text_console_sink(styled_formatter),
@@ -109,25 +126,36 @@ fn main {
min_level=@lib.Level::Info,
target="file",
)
file_logger.info("native file sink ready", fields=[@lib.field("kind", "file")])
file_logger.info("native file sink ready", fields=[
@lib.field("kind", "file"),
])
ignore(file_logger.sink.flush())
ignore(file_logger.sink.close())
}
let buffered = @lib.buffered_sink(@lib.console_sink(), flush_limit=2)
let buffered_logger = @lib.Logger::new(buffered, min_level=@lib.Level::Info, target="buffered")
let buffered_logger = @lib.Logger::new(
buffered,
min_level=@lib.Level::Info,
target="buffered",
)
buffered_logger.info("buffered one")
buffered_logger.info("buffered two")
buffered.flush()
let filtered = @lib.filter_sink(
@lib.console_sink(),
fn(rec) {
rec.target == "kept"
},
let filtered = @lib.filter_sink(@lib.console_sink(), fn(rec) {
rec.target == "kept"
})
let kept_logger = @lib.Logger::new(
filtered,
min_level=@lib.Level::Info,
target="kept",
)
let dropped_logger = @lib.Logger::new(
filtered,
min_level=@lib.Level::Info,
target="dropped",
)
let kept_logger = @lib.Logger::new(filtered, min_level=@lib.Level::Info, target="kept")
let dropped_logger = @lib.Logger::new(filtered, min_level=@lib.Level::Info, target="dropped")
kept_logger.info("filter kept this")
dropped_logger.info("filter dropped this")
@@ -135,10 +163,12 @@ fn main {
@lib.console_sink(),
min_level=@lib.Level::Info,
target="service",
).with_filter(@lib.all_of([
@lib.target_has_prefix("service"),
@lib.message_contains("kept"),
]))
).with_filter(
@lib.all_of([
@lib.target_has_prefix("service"),
@lib.message_contains("kept"),
]),
)
filtered_logger.info("logger filter dropped this")
filtered_logger.child("api").info("logger filter kept this")
@@ -146,12 +176,17 @@ fn main {
@lib.console_sink(),
min_level=@lib.Level::Info,
target="auth",
).with_patch(@lib.compose_patches([
@lib.prefix_message("[safe] "),
@lib.redact_fields(["token"]),
@lib.append_fields([@lib.field("service", "bitlogger")]),
]))
patched_logger.info("login", fields=[@lib.field("user", "alice"), @lib.field("token", "secret")])
).with_patch(
@lib.compose_patches([
@lib.prefix_message("[safe] "),
@lib.redact_fields(["token"]),
@lib.append_fields([@lib.field("service", "bitlogger")]),
]),
)
patched_logger.info("login", fields=[
@lib.field("user", "alice"),
@lib.field("token", "secret"),
])
let config_logger = @lib.parse_and_build_logger(
"{\"min_level\":\"debug\",\"target\":\"config.demo\",\"timestamp\":true,\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"field_separator\":\",\",\"template\":\"[{level}] {target} {message} :: {fields}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
@@ -165,7 +200,10 @@ fn main {
config_logger.info("configured from json")
ignore(config_logger.flush())
let queue_config = @lib.QueueConfig::new(4, overflow=@lib.QueueOverflowPolicy::DropOldest)
let queue_config = @lib.QueueConfig::new(
4,
overflow=@lib.QueueOverflowPolicy::DropOldest,
)
println(@lib.stringify_queue_config(queue_config))
let formatter_config = @lib.TextFormatterConfig::new(
@@ -195,13 +233,17 @@ fn main {
let file_runtime_logger = @lib.build_logger(
@lib.LoggerConfig::new(
sink=@lib.SinkConfig::new(kind=@lib.SinkKind::File, path="bitlogger-runtime-demo.log"),
sink=@lib.SinkConfig::new(
kind=@lib.SinkKind::File,
path="bitlogger-runtime-demo.log",
),
queue=Some(@lib.QueueConfig::new(8)),
),
)
file_runtime_logger.info("runtime file logger ready")
match file_runtime_logger.file_runtime_state() {
Some(snapshot) => println(@lib.stringify_runtime_file_state(snapshot, pretty=true))
Some(snapshot) =>
println(@lib.stringify_runtime_file_state(snapshot, pretty=true))
None => ()
}
ignore(file_runtime_logger.file_close())
+1
View File
@@ -1,3 +1,4 @@
///|
fn main {
let config = @lib.parse_logger_config_text(
"{\"min_level\":\"debug\",\"target\":\"demo.config\",\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"show_timestamp\":false,\"separator\":\" | \",\"template\":\"[{level}] {target} {message}\"}},\"queue\":{\"max_pending\":2,\"overflow\":\"DropOldest\"}}",
+1
View File
@@ -1,3 +1,4 @@
///|
fn main {
let logger = @lib.build_logger(
@lib.console(min_level=@lib.Level::Info, target="demo.console"),
+4 -1
View File
@@ -1,6 +1,9 @@
///|
fn main {
if !@lib.native_files_supported() {
println("native file sink is not available on this backend; examples/presets stays portable, while file_rotation remains native-focused")
println(
"native file sink is not available on this backend; examples/presets stays portable, while file_rotation remains native-focused",
)
return
}
+7 -2
View File
@@ -1,3 +1,4 @@
///|
fn main {
let config = @lib.with_queue(
@lib.text_console(
@@ -36,10 +37,14 @@ fn main {
}
let file_logger = @lib.build_logger(file_config)
file_logger.info("file preset ready", fields=[@lib.field("kind", "native-only")])
file_logger.info("file preset ready", fields=[
@lib.field("kind", "native-only"),
])
ignore(file_logger.flush())
ignore(file_logger.file_close())
} else {
println("file presets are skipped on this backend because native file support is unavailable")
println(
"file presets are skipped on this backend because native file support is unavailable",
)
}
}
+2 -3
View File
@@ -1,3 +1,4 @@
///|
fn main {
let logger = @lib.build_logger(
@lib.text_console(
@@ -6,9 +7,7 @@ fn main {
text_formatter=@lib.TextFormatterConfig::new(
show_timestamp=false,
color_mode=@lib.ColorMode::Always,
style_tags={
"accent": @lib.text_style(fg=Some("#4cc9f0"), bold=true),
},
style_tags={ "accent": @lib.text_style(fg=Some("#4cc9f0"), bold=true) },
),
),
)
+5 -1
View File
@@ -1,3 +1,4 @@
///|
fn main {
let logger = @lib.build_logger(
@lib.text_console(
@@ -11,5 +12,8 @@ fn main {
),
),
)
logger.info("formatted output", fields=[@lib.field("user", "alice"), @lib.field("request_id", "42")])
logger.info("formatted output", fields=[
@lib.field("user", "alice"),
@lib.field("request_id", "42"),
])
}
+19
View File
@@ -0,0 +1,19 @@
name = "Nanaloveyuki/BitLogger"
version = "0.6.0"
import {
"maria/json_parser@0.1.1",
"moonbitlang/async@0.20.0",
}
readme = "src/README.mbt.md"
repository = "https://github.com/Nanaloveyuki/BitLogger"
license = "MIT"
keywords = [ "logger", "logging", "moonbit" ]
description = "A structured logger for MoonBit."
-17
View File
@@ -1,17 +0,0 @@
{
"name": "Nanaloveyuki/BitLogger",
"version": "0.5.3",
"deps": {
"maria/json_parser": "0.1.1",
"moonbitlang/async": "0.19.0"
},
"readme": "src/README.mbt.md",
"repository": "https://github.com/Nanaloveyuki/BitLogger",
"license": "MIT",
"keywords": [
"logger",
"logging",
"moonbit"
],
"description": "A structured logger for MoonBit."
}
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -1,19 +1,26 @@
///|
pub type ApplicationAsyncLogger = AsyncLogger[@bitlogger.RuntimeSink]
pub type ApplicationTextAsyncLogger = AsyncLogger[@bitlogger.FormattedConsoleSink]
///|
pub type ApplicationTextAsyncLogger = AsyncLogger[
@bitlogger.FormattedConsoleSink,
]
///|
pub fn build_application_async_logger(
config : AsyncLoggerBuildConfig,
) -> ApplicationAsyncLogger {
build_async_logger(config)
}
///|
pub fn build_application_text_async_logger(
config : AsyncLoggerBuildConfig,
) -> ApplicationTextAsyncLogger {
build_async_text_logger(config)
}
///|
pub fn parse_and_build_application_async_logger(
input : String,
) -> ApplicationAsyncLogger raise {
+5
View File
@@ -1,20 +1,25 @@
///|
pub fn async_runtime_mode() -> AsyncRuntimeMode {
@utils.native_worker_async_runtime_mode()
}
///|
pub fn async_runtime_supports_background_worker() -> Bool {
ignore(all_async_runtime_modes())
true
}
///|
fn async_runtime_guard_closed_on_log() -> Bool {
false
}
///|
fn async_runtime_shutdown_clears_pending_after_wait_idle() -> Bool {
true
}
///|
fn async_runtime_shutdown_waits_for_worker() -> Bool {
true
}
+153 -69
View File
@@ -1,84 +1,125 @@
///|
pub(all) suberror AsyncLoggerClosed {
AsyncLoggerClosed
}
///|
pub type AsyncOverflowPolicy = @utils.AsyncOverflowPolicy
///|
pub type AsyncFlushPolicy = @utils.AsyncFlushPolicy
///|
pub type AsyncRuntimeMode = @utils.AsyncRuntimeMode
///|
fn all_async_runtime_modes() -> Array[AsyncRuntimeMode] {
[@utils.native_worker_async_runtime_mode(), @utils.compatibility_async_runtime_mode()]
[
@utils.native_worker_async_runtime_mode(),
@utils.compatibility_async_runtime_mode(),
]
}
///|
pub type AsyncRuntimeState = @utils.AsyncRuntimeState
///|
pub type AsyncLoggerState = @utils.AsyncLoggerState
///|
pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String {
@utils.async_runtime_mode_label(mode)
}
///|
pub fn async_runtime_state() -> AsyncRuntimeState {
AsyncRuntimeState::new(async_runtime_mode(), async_runtime_supports_background_worker())
AsyncRuntimeState::new(
async_runtime_mode(),
async_runtime_supports_background_worker(),
)
}
pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> @json_parser.JsonValue {
///|
pub fn async_runtime_state_to_json(
state : AsyncRuntimeState,
) -> @json_parser.JsonValue {
@utils.async_runtime_state_to_json(state)
}
///|
pub fn stringify_async_runtime_state(
state : AsyncRuntimeState,
pretty~ : Bool = false,
pretty? : Bool = false,
) -> String {
@utils.stringify_async_runtime_state(state, pretty=pretty)
@utils.stringify_async_runtime_state(state, pretty~)
}
pub fn async_logger_state_to_json(state : AsyncLoggerState) -> @json_parser.JsonValue {
///|
pub fn async_logger_state_to_json(
state : AsyncLoggerState,
) -> @json_parser.JsonValue {
@utils.async_logger_state_to_json(state)
}
///|
pub fn stringify_async_logger_state(
state : AsyncLoggerState,
pretty~ : Bool = false,
pretty? : Bool = false,
) -> String {
@utils.stringify_async_logger_state(state, pretty=pretty)
@utils.stringify_async_logger_state(state, pretty~)
}
///|
pub type AsyncLoggerConfig = @utils.AsyncLoggerConfig
pub fn parse_async_logger_config_text(input : String) -> AsyncLoggerConfig raise {
///|
pub fn parse_async_logger_config_text(
input : String,
) -> AsyncLoggerConfig raise {
@utils.parse_async_logger_config_text(input)
}
pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> @json_parser.JsonValue {
///|
pub fn async_logger_config_to_json(
config : AsyncLoggerConfig,
) -> @json_parser.JsonValue {
@utils.async_logger_config_to_json(config)
}
pub fn stringify_async_logger_config(config : AsyncLoggerConfig, pretty~ : Bool = false) -> String {
@utils.stringify_async_logger_config(config, pretty=pretty)
///|
pub fn stringify_async_logger_config(
config : AsyncLoggerConfig,
pretty? : Bool = false,
) -> String {
@utils.stringify_async_logger_config(config, pretty~)
}
///|
pub type AsyncLoggerBuildConfig = @utils.AsyncLoggerBuildConfig
pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildConfig raise {
///|
pub fn parse_async_logger_build_config_text(
input : String,
) -> AsyncLoggerBuildConfig raise {
@utils.parse_async_logger_build_config_text(input)
}
///|
pub fn async_logger_build_config_to_json(
config : AsyncLoggerBuildConfig,
) -> @json_parser.JsonValue {
@utils.async_logger_build_config_to_json(config)
}
///|
pub fn stringify_async_logger_build_config(
config : AsyncLoggerBuildConfig,
pretty~ : Bool = false,
pretty? : Bool = false,
) -> String {
@utils.stringify_async_logger_build_config(config, pretty=pretty)
@utils.stringify_async_logger_build_config(config, pretty~)
}
///|
pub struct AsyncLogger[S] {
min_level : @bitlogger.Level
target : String
@@ -101,12 +142,13 @@ pub struct AsyncLogger[S] {
last_error : Ref[String]
}
///|
pub fn[S] async_logger(
sink : S,
config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),
min_level~ : @bitlogger.Level = @bitlogger.Level::Info,
target~ : String = "",
flush~ : (S) -> Int raise = fn(_) { 0 },
config? : AsyncLoggerConfig = AsyncLoggerConfig::new(),
min_level? : @bitlogger.Level = @bitlogger.Level::Info,
target? : String = "",
flush? : (S) -> Int raise = fn(_) { 0 },
) -> AsyncLogger[S] {
{
min_level,
@@ -131,6 +173,7 @@ pub fn[S] async_logger(
}
}
///|
fn queue_kind_of(config : AsyncLoggerConfig) -> @aqueue.Kind {
let limit = if config.max_pending < 0 { 0 } else { config.max_pending }
match config.overflow {
@@ -140,14 +183,23 @@ fn queue_kind_of(config : AsyncLoggerConfig) -> @aqueue.Kind {
}
}
pub fn[S] AsyncLogger::with_timestamp(self : AsyncLogger[S], enabled~ : Bool = true) -> AsyncLogger[S] {
///|
pub fn[S] AsyncLogger::with_timestamp(
self : AsyncLogger[S],
enabled? : Bool = true,
) -> AsyncLogger[S] {
{ ..self, timestamp: enabled }
}
pub fn[S] AsyncLogger::with_target(self : AsyncLogger[S], target : String) -> AsyncLogger[S] {
{ ..self, target }
///|
pub fn[S] AsyncLogger::with_target(
self : AsyncLogger[S],
target : String,
) -> AsyncLogger[S] {
{ ..self, target, }
}
///|
pub fn[S] AsyncLogger::with_context_fields(
self : AsyncLogger[S],
fields : Array[@bitlogger.Field],
@@ -155,39 +207,33 @@ pub fn[S] AsyncLogger::with_context_fields(
{ ..self, context_fields: fields }
}
///|
pub fn[S] AsyncLogger::with_filter(
self : AsyncLogger[S],
predicate : (@bitlogger.Record) -> Bool,
) -> AsyncLogger[S] {
let current = self.filter
{
..self,
filter: fn(rec) {
current(rec) && predicate(rec)
},
}
{ ..self, filter: fn(rec) { current(rec) && predicate(rec) } }
}
///|
pub fn[S] AsyncLogger::with_patch(
self : AsyncLogger[S],
patch : @bitlogger.RecordPatch,
) -> AsyncLogger[S] {
let current = self.patch
{
..self,
patch: fn(rec) {
patch(current(rec))
},
}
{ ..self, patch: fn(rec) { patch(current(rec)) } }
}
///|
pub fn[S] AsyncLogger::with_min_level(
self : AsyncLogger[S],
min_level : @bitlogger.Level,
) -> AsyncLogger[S] {
{ ..self, min_level }
{ ..self, min_level, }
}
///|
fn combine_targets(parent : String, child : String) -> String {
if parent == "" {
child
@@ -198,14 +244,23 @@ fn combine_targets(parent : String, child : String) -> String {
}
}
pub fn[S] AsyncLogger::child(self : AsyncLogger[S], target : String) -> AsyncLogger[S] {
///|
pub fn[S] AsyncLogger::child(
self : AsyncLogger[S],
target : String,
) -> AsyncLogger[S] {
{ ..self, target: combine_targets(self.target, target) }
}
pub fn[S] AsyncLogger::is_enabled(self : AsyncLogger[S], level : @bitlogger.Level) -> Bool {
///|
pub fn[S] AsyncLogger::is_enabled(
self : AsyncLogger[S],
level : @bitlogger.Level,
) -> Bool {
level.enabled(self.min_level)
}
///|
fn merge_fields(
left : Array[@bitlogger.Field],
right : Array[@bitlogger.Field],
@@ -219,32 +274,27 @@ fn merge_fields(
}
}
///|
pub async fn[S] AsyncLogger::log(
self : AsyncLogger[S],
level : @bitlogger.Level,
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
target? : String = "",
) -> Unit {
guard !(async_runtime_guard_closed_on_log() && self.is_closed()) else {
()
}
guard self.is_enabled(level) else {
()
}
guard !(async_runtime_guard_closed_on_log() && self.is_closed()) else { () }
guard self.is_enabled(level) else { () }
let actual_target = if target == "" { self.target } else { target }
let timestamp_ms = if self.timestamp { @env.now() } else { 0UL }
let rec = @bitlogger.Record::new(
level,
message,
timestamp_ms=timestamp_ms,
timestamp_ms~,
target=actual_target,
fields=merge_fields(self.context_fields, fields),
)
let rec = (self.patch)(rec)
guard (self.filter)(rec) else {
()
}
guard (self.filter)(rec) else { () }
let accepted = self.queue.try_put(rec) catch {
err if err is AsyncLoggerClosed => false
err => raise err
@@ -254,7 +304,7 @@ pub async fn[S] AsyncLogger::log(
} else {
match self.overflow {
AsyncOverflowPolicy::Blocking => {
let accepted = (async fn() -> Bool raise {
let accepted = (async fn() -> Bool {
self.queue.put(rec)
true
})() catch {
@@ -265,81 +315,93 @@ pub async fn[S] AsyncLogger::log(
self.pending_count.val += 1
}
}
AsyncOverflowPolicy::DropOldest | AsyncOverflowPolicy::DropNewest => {
AsyncOverflowPolicy::DropOldest | AsyncOverflowPolicy::DropNewest =>
self.dropped_count.val += 1
}
}
}
}
///|
pub async fn[S] AsyncLogger::trace(
self : AsyncLogger[S],
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
) -> Unit {
self.log(@bitlogger.Level::Trace, message, fields=fields)
self.log(@bitlogger.Level::Trace, message, fields~)
}
///|
pub async fn[S] AsyncLogger::debug(
self : AsyncLogger[S],
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
) -> Unit {
self.log(@bitlogger.Level::Debug, message, fields=fields)
self.log(@bitlogger.Level::Debug, message, fields~)
}
///|
pub async fn[S] AsyncLogger::info(
self : AsyncLogger[S],
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
) -> Unit {
self.log(@bitlogger.Level::Info, message, fields=fields)
self.log(@bitlogger.Level::Info, message, fields~)
}
///|
pub async fn[S] AsyncLogger::warn(
self : AsyncLogger[S],
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
) -> Unit {
self.log(@bitlogger.Level::Warn, message, fields=fields)
self.log(@bitlogger.Level::Warn, message, fields~)
}
///|
pub async fn[S] AsyncLogger::error(
self : AsyncLogger[S],
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
) -> Unit {
self.log(@bitlogger.Level::Error, message, fields=fields)
self.log(@bitlogger.Level::Error, message, fields~)
}
///|
pub fn[S] AsyncLogger::pending_count(self : AsyncLogger[S]) -> Int {
self.pending_count.val
}
///|
pub fn[S] AsyncLogger::dropped_count(self : AsyncLogger[S]) -> Int {
self.dropped_count.val
}
///|
pub fn[S] AsyncLogger::is_closed(self : AsyncLogger[S]) -> Bool {
self.is_closed.val
}
///|
pub fn[S] AsyncLogger::is_running(self : AsyncLogger[S]) -> Bool {
self.is_running.val
}
///|
pub fn[S] AsyncLogger::has_failed(self : AsyncLogger[S]) -> Bool {
self.has_failed.val
}
///|
pub fn[S] AsyncLogger::last_error(self : AsyncLogger[S]) -> String {
self.last_error.val
}
///|
pub fn[S] AsyncLogger::flush_policy(self : AsyncLogger[S]) -> AsyncFlushPolicy {
self.flush_policy
}
///|
pub fn[S] AsyncLogger::state(self : AsyncLogger[S]) -> AsyncLoggerState {
AsyncLoggerState::new(
async_runtime_state(),
@@ -353,7 +415,11 @@ pub fn[S] AsyncLogger::state(self : AsyncLogger[S]) -> AsyncLoggerState {
)
}
pub fn[S] AsyncLogger::close(self : AsyncLogger[S], clear? : Bool = false) -> Unit {
///|
pub fn[S] AsyncLogger::close(
self : AsyncLogger[S],
clear? : Bool = false,
) -> Unit {
self.is_closed.val = true
if clear {
let abandoned = self.pending_count()
@@ -362,9 +428,10 @@ pub fn[S] AsyncLogger::close(self : AsyncLogger[S], clear? : Bool = false) -> Un
self.pending_count.val = 0
}
}
self.queue.close(error=AsyncLoggerClosed, clear=clear)
self.queue.close(error=AsyncLoggerClosed, clear~)
}
///|
pub async fn[S] AsyncLogger::wait_idle(self : AsyncLogger[S]) -> Unit {
while self.pending_count() > 0 {
if self.has_failed() {
@@ -374,12 +441,17 @@ pub async fn[S] AsyncLogger::wait_idle(self : AsyncLogger[S]) -> Unit {
}
}
pub async fn[S] AsyncLogger::shutdown(self : AsyncLogger[S], clear? : Bool = false) -> Unit {
///|
pub async fn[S] AsyncLogger::shutdown(
self : AsyncLogger[S],
clear? : Bool = false,
) -> Unit {
if clear {
self.close(clear=true)
} else {
self.wait_idle()
if async_runtime_shutdown_clears_pending_after_wait_idle() && self.pending_count() > 0 {
if async_runtime_shutdown_clears_pending_after_wait_idle() &&
self.pending_count() > 0 {
self.close(clear=true)
} else {
self.close()
@@ -392,6 +464,7 @@ pub async fn[S] AsyncLogger::shutdown(self : AsyncLogger[S], clear? : Bool = fal
}
}
///|
async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
while true {
let rec = logger.queue.get() catch {
@@ -419,7 +492,9 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
if logger.linger_ms <= 0 {
break
}
let waited = @async.with_timeout_opt(logger.linger_ms, () => logger.queue.get()) catch {
let waited = @async.with_timeout_opt(logger.linger_ms, () => {
logger.queue.get()
}) catch {
err if err is AsyncLoggerClosed => None
err => raise err
}
@@ -447,7 +522,10 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
}
}
pub async fn[S : @bitlogger.Sink] AsyncLogger::run(self : AsyncLogger[S]) -> Unit {
///|
pub async fn[S : @bitlogger.Sink] AsyncLogger::run(
self : AsyncLogger[S],
) -> Unit {
self.is_running.val = true
self.has_failed.val = false
self.last_error.val = ""
@@ -462,6 +540,7 @@ pub async fn[S : @bitlogger.Sink] AsyncLogger::run(self : AsyncLogger[S]) -> Uni
self.is_running.val = false
}
///|
pub fn build_async_logger(
config : AsyncLoggerBuildConfig,
) -> AsyncLogger[@bitlogger.RuntimeSink] {
@@ -475,9 +554,14 @@ pub fn build_async_logger(
).with_timestamp(enabled=logger.timestamp)
}
pub fn build_async_text_logger(config : AsyncLoggerBuildConfig) -> AsyncLogger[@bitlogger.FormattedConsoleSink] {
///|
pub fn build_async_text_logger(
config : AsyncLoggerBuildConfig,
) -> AsyncLogger[@bitlogger.FormattedConsoleSink] {
async_logger(
@bitlogger.text_console_sink(config.logger.sink.text_formatter.to_formatter()),
@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,
+5
View File
@@ -1,20 +1,25 @@
///|
pub fn async_runtime_mode() -> AsyncRuntimeMode {
@utils.compatibility_async_runtime_mode()
}
///|
pub fn async_runtime_supports_background_worker() -> Bool {
ignore(all_async_runtime_modes())
false
}
///|
fn async_runtime_guard_closed_on_log() -> Bool {
true
}
///|
fn async_runtime_shutdown_clears_pending_after_wait_idle() -> Bool {
false
}
///|
fn async_runtime_shutdown_waits_for_worker() -> Bool {
false
}
+43 -25
View File
@@ -1,61 +1,67 @@
///|
pub struct LibraryAsyncLogger[S] {
inner : AsyncLogger[S]
}
///|
fn[S] library_async_logger(logger : AsyncLogger[S]) -> LibraryAsyncLogger[S] {
{ inner: logger }
}
pub fn[S] AsyncLogger::to_library_async_logger(self : AsyncLogger[S]) -> LibraryAsyncLogger[S] {
///|
pub fn[S] AsyncLogger::to_library_async_logger(
self : AsyncLogger[S],
) -> LibraryAsyncLogger[S] {
library_async_logger(self)
}
///|
pub fn[S] LibraryAsyncLogger::new(
sink : S,
config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),
min_level~ : @bitlogger.Level = @bitlogger.Level::Info,
target~ : String = "",
flush~ : (S) -> Int raise = fn(_) { 0 },
config? : AsyncLoggerConfig = AsyncLoggerConfig::new(),
min_level? : @bitlogger.Level = @bitlogger.Level::Info,
target? : String = "",
flush? : (S) -> Int raise = fn(_) { 0 },
) -> LibraryAsyncLogger[S] {
library_async_logger(
async_logger(
sink,
config=config,
min_level=min_level,
target=target,
flush=flush,
),
)
library_async_logger(async_logger(sink, config~, min_level~, target~, flush~))
}
pub fn[S] LibraryAsyncLogger::to_async_logger(self : LibraryAsyncLogger[S]) -> AsyncLogger[S] {
///|
pub fn[S] LibraryAsyncLogger::to_async_logger(
self : LibraryAsyncLogger[S],
) -> AsyncLogger[S] {
self.inner
}
///|
fn[S] configured_library_async_logger(
logger : AsyncLogger[S],
) -> LibraryAsyncLogger[S] {
library_async_logger(logger)
}
///|
pub fn build_library_async_logger(
config : AsyncLoggerBuildConfig,
) -> LibraryAsyncLogger[@bitlogger.RuntimeSink] {
configured_library_async_logger(build_async_logger(config))
}
///|
pub fn build_library_async_text_logger(
config : AsyncLoggerBuildConfig,
) -> LibraryAsyncLogger[@bitlogger.FormattedConsoleSink] {
configured_library_async_logger(build_async_text_logger(config))
}
///|
pub fn parse_and_build_library_async_logger(
input : String,
) -> LibraryAsyncLogger[@bitlogger.RuntimeSink] raise {
build_library_async_logger(parse_async_logger_build_config_text(input))
}
///|
pub fn[S] LibraryAsyncLogger::with_target(
self : LibraryAsyncLogger[S],
target : String,
@@ -63,6 +69,7 @@ pub fn[S] LibraryAsyncLogger::with_target(
library_async_logger(self.inner.with_target(target))
}
///|
pub fn[S] LibraryAsyncLogger::child(
self : LibraryAsyncLogger[S],
target : String,
@@ -70,6 +77,7 @@ pub fn[S] LibraryAsyncLogger::child(
library_async_logger(self.inner.child(target))
}
///|
pub fn[S] LibraryAsyncLogger::with_context_fields(
self : LibraryAsyncLogger[S],
fields : Array[@bitlogger.Field],
@@ -77,6 +85,7 @@ pub fn[S] LibraryAsyncLogger::with_context_fields(
library_async_logger(self.inner.with_context_fields(fields))
}
///|
pub fn[S] LibraryAsyncLogger::bind(
self : LibraryAsyncLogger[S],
fields : Array[@bitlogger.Field],
@@ -84,6 +93,7 @@ pub fn[S] LibraryAsyncLogger::bind(
self.with_context_fields(fields)
}
///|
pub fn[S] LibraryAsyncLogger::is_enabled(
self : LibraryAsyncLogger[S],
level : @bitlogger.Level,
@@ -91,47 +101,55 @@ pub fn[S] LibraryAsyncLogger::is_enabled(
self.inner.is_enabled(level)
}
///|
pub async fn[S] LibraryAsyncLogger::log(
self : LibraryAsyncLogger[S],
level : @bitlogger.Level,
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
target? : String = "",
) -> Unit {
self.inner.log(level, message, fields=fields, target=target)
self.inner.log(level, message, fields~, target~)
}
///|
pub async fn[S] LibraryAsyncLogger::info(
self : LibraryAsyncLogger[S],
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
) -> Unit {
self.inner.info(message, fields=fields)
self.inner.info(message, fields~)
}
///|
pub async fn[S] LibraryAsyncLogger::warn(
self : LibraryAsyncLogger[S],
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
) -> Unit {
self.inner.warn(message, fields=fields)
self.inner.warn(message, fields~)
}
///|
pub async fn[S] LibraryAsyncLogger::error(
self : LibraryAsyncLogger[S],
message : String,
fields~ : Array[@bitlogger.Field] = [],
fields? : Array[@bitlogger.Field] = [],
) -> Unit {
self.inner.error(message, fields=fields)
self.inner.error(message, fields~)
}
pub async fn[S : @bitlogger.Sink] LibraryAsyncLogger::run(self : LibraryAsyncLogger[S]) -> Unit {
///|
pub async fn[S : @bitlogger.Sink] LibraryAsyncLogger::run(
self : LibraryAsyncLogger[S],
) -> Unit {
self.inner.run()
}
///|
pub async fn[S] LibraryAsyncLogger::shutdown(
self : LibraryAsyncLogger[S],
clear? : Bool = false,
) -> Unit {
self.inner.shutdown(clear=clear)
self.inner.shutdown(clear~)
}
+12 -6
View File
@@ -1,17 +1,23 @@
import {
"Nanaloveyuki/BitLogger/src" @bitlogger,
"Nanaloveyuki/BitLogger/src-async/utils" @utils,
"maria/json_parser" @json_parser,
"moonbitlang/async" @async,
"moonbitlang/async/aqueue" @aqueue,
"moonbitlang/core/env" @env,
"Nanaloveyuki/BitLogger/src-async/utils",
"maria/json_parser",
"moonbitlang/async",
"moonbitlang/async/aqueue",
"moonbitlang/core/env",
"moonbitlang/core/ref",
}
options(
targets: {
"async_logger_shared.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ],
"application_async_logger.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ],
"application_async_logger.mbt": [
"native",
"llvm",
"js",
"wasm",
"wasm-gc",
],
"library_async_logger.mbt": [ "native", "llvm", "js", "wasm", "wasm-gc" ],
"async_logger_native.mbt": [ "native", "llvm" ],
"async_logger_stub.mbt": [ "js", "wasm", "wasm-gc" ],
+137 -61
View File
@@ -1,20 +1,24 @@
///|
pub(all) enum AsyncOverflowPolicy {
Blocking
DropOldest
DropNewest
}
///|
pub(all) enum AsyncFlushPolicy {
Never
Batch
Shutdown
}
///|
pub enum AsyncRuntimeMode {
NativeWorker
Compatibility
}
///|
pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String {
match mode {
AsyncRuntimeMode::NativeWorker => "native_worker"
@@ -22,19 +26,23 @@ pub fn async_runtime_mode_label(mode : AsyncRuntimeMode) -> String {
}
}
///|
pub fn native_worker_async_runtime_mode() -> AsyncRuntimeMode {
AsyncRuntimeMode::NativeWorker
}
///|
pub fn compatibility_async_runtime_mode() -> AsyncRuntimeMode {
AsyncRuntimeMode::Compatibility
}
///|
pub struct AsyncRuntimeState {
mode : AsyncRuntimeMode
background_worker : Bool
}
///|
pub fn AsyncRuntimeState::new(
mode : AsyncRuntimeMode,
background_worker : Bool,
@@ -42,6 +50,7 @@ pub fn AsyncRuntimeState::new(
{ mode, background_worker }
}
///|
pub struct AsyncLoggerState {
runtime : AsyncRuntimeState
pending_count : Int
@@ -53,6 +62,7 @@ pub struct AsyncLoggerState {
flush_policy : AsyncFlushPolicy
}
///|
pub fn AsyncLoggerState::new(
runtime : AsyncRuntimeState,
pending_count : Int,
@@ -75,16 +85,20 @@ pub fn AsyncLoggerState::new(
}
}
pub fn async_runtime_state_to_json(state : AsyncRuntimeState) -> @json_parser.JsonValue {
///|
pub fn async_runtime_state_to_json(
state : AsyncRuntimeState,
) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"mode": @json_parser.JsonValue::String(async_runtime_mode_label(state.mode)),
"background_worker": @json_parser.JsonValue::Bool(state.background_worker),
})
}
///|
pub fn stringify_async_runtime_state(
state : AsyncRuntimeState,
pretty~ : Bool = false,
pretty? : Bool = false,
) -> String {
let value = async_runtime_state_to_json(state)
if pretty {
@@ -94,6 +108,7 @@ pub fn stringify_async_runtime_state(
}
}
///|
fn async_flush_policy_label(policy : AsyncFlushPolicy) -> String {
match policy {
AsyncFlushPolicy::Never => "Never"
@@ -102,26 +117,39 @@ fn async_flush_policy_label(policy : AsyncFlushPolicy) -> String {
}
}
fn async_logger_state_to_json_value(state : AsyncLoggerState) -> @json_parser.JsonValue {
///|
fn async_logger_state_to_json_value(
state : AsyncLoggerState,
) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"runtime": async_runtime_state_to_json(state.runtime),
"pending_count": @json_parser.JsonValue::Number(state.pending_count.to_double()),
"dropped_count": @json_parser.JsonValue::Number(state.dropped_count.to_double()),
"pending_count": @json_parser.JsonValue::Number(
state.pending_count.to_double(),
),
"dropped_count": @json_parser.JsonValue::Number(
state.dropped_count.to_double(),
),
"is_closed": @json_parser.JsonValue::Bool(state.is_closed),
"is_running": @json_parser.JsonValue::Bool(state.is_running),
"has_failed": @json_parser.JsonValue::Bool(state.has_failed),
"last_error": @json_parser.JsonValue::String(state.last_error),
"flush_policy": @json_parser.JsonValue::String(async_flush_policy_label(state.flush_policy)),
"flush_policy": @json_parser.JsonValue::String(
async_flush_policy_label(state.flush_policy),
),
})
}
pub fn async_logger_state_to_json(state : AsyncLoggerState) -> @json_parser.JsonValue {
///|
pub fn async_logger_state_to_json(
state : AsyncLoggerState,
) -> @json_parser.JsonValue {
async_logger_state_to_json_value(state)
}
///|
pub fn stringify_async_logger_state(
state : AsyncLoggerState,
pretty~ : Bool = false,
pretty? : Bool = false,
) -> String {
let value = async_logger_state_to_json_value(state)
if pretty {
@@ -131,6 +159,7 @@ pub fn stringify_async_logger_state(
}
}
///|
pub struct AsyncLoggerConfig {
max_pending : Int
overflow : AsyncOverflowPolicy
@@ -139,22 +168,32 @@ pub struct AsyncLoggerConfig {
flush : AsyncFlushPolicy
}
///|
pub fn AsyncLoggerConfig::new(
max_pending~ : Int = 0,
overflow~ : AsyncOverflowPolicy = AsyncOverflowPolicy::Blocking,
max_batch~ : Int = 1,
linger_ms~ : Int = 0,
flush~ : AsyncFlushPolicy = AsyncFlushPolicy::Never,
max_pending? : Int = 0,
overflow? : AsyncOverflowPolicy = AsyncOverflowPolicy::Blocking,
max_batch? : Int = 1,
linger_ms? : Int = 0,
flush? : AsyncFlushPolicy = AsyncFlushPolicy::Never,
) -> AsyncLoggerConfig {
{
max_pending,
overflow,
max_batch: if max_batch <= 1 { 1 } else { max_batch },
linger_ms: if linger_ms < 0 { 0 } else { linger_ms },
max_batch: if max_batch <= 1 {
1
} else {
max_batch
},
linger_ms: if linger_ms < 0 {
0
} else {
linger_ms
},
flush,
}
}
///|
fn parse_async_overflow(name : String) -> AsyncOverflowPolicy raise {
match name.to_upper() {
"BLOCKING" => AsyncOverflowPolicy::Blocking
@@ -165,6 +204,7 @@ fn parse_async_overflow(name : String) -> AsyncOverflowPolicy raise {
}
}
///|
fn parse_async_flush(name : String) -> AsyncFlushPolicy raise {
match name.to_upper() {
"NEVER" => AsyncFlushPolicy::Never
@@ -175,75 +215,100 @@ fn parse_async_flush(name : String) -> AsyncFlushPolicy raise {
}
}
pub fn parse_async_logger_config_text(input : String) -> AsyncLoggerConfig raise {
///|
pub fn parse_async_logger_config_text(
input : String,
) -> AsyncLoggerConfig raise {
let root = @json_parser.parse(input)
let obj = match root.as_object() {
Some(obj) => obj
None => raise Failure::Failure("Expected object for async logger config")
}
let max_pending = match obj.get("max_pending") {
Some(value) => match value.as_number() {
Some(number) => number.to_int()
None => raise Failure::Failure("Expected number at async_config.max_pending")
}
Some(value) =>
match value.as_number() {
Some(number) => number.to_int()
None =>
raise Failure::Failure("Expected number at async_config.max_pending")
}
None => 0
}
let overflow = match obj.get("overflow") {
Some(value) => match value.as_string() {
Some(text) => parse_async_overflow(text)
None => raise Failure::Failure("Expected string at async_config.overflow")
}
Some(value) =>
match value.as_string() {
Some(text) => parse_async_overflow(text)
None =>
raise Failure::Failure("Expected string at async_config.overflow")
}
None => AsyncOverflowPolicy::Blocking
}
let max_batch = match obj.get("max_batch") {
Some(value) => match value.as_number() {
Some(number) => number.to_int()
None => raise Failure::Failure("Expected number at async_config.max_batch")
}
Some(value) =>
match value.as_number() {
Some(number) => number.to_int()
None =>
raise Failure::Failure("Expected number at async_config.max_batch")
}
None => 1
}
let linger_ms = match obj.get("linger_ms") {
Some(value) => match value.as_number() {
Some(number) => number.to_int()
None => raise Failure::Failure("Expected number at async_config.linger_ms")
}
Some(value) =>
match value.as_number() {
Some(number) => number.to_int()
None =>
raise Failure::Failure("Expected number at async_config.linger_ms")
}
None => 0
}
let flush = match obj.get("flush") {
Some(value) => match value.as_string() {
Some(text) => parse_async_flush(text)
None => raise Failure::Failure("Expected string at async_config.flush")
}
Some(value) =>
match value.as_string() {
Some(text) => parse_async_flush(text)
None => raise Failure::Failure("Expected string at async_config.flush")
}
None => AsyncFlushPolicy::Never
}
AsyncLoggerConfig::new(
max_pending=max_pending,
overflow=overflow,
max_batch=max_batch,
linger_ms=linger_ms,
flush=flush,
max_pending~,
overflow~,
max_batch~,
linger_ms~,
flush~,
)
}
pub fn async_logger_config_to_json(config : AsyncLoggerConfig) -> @json_parser.JsonValue {
///|
pub fn async_logger_config_to_json(
config : AsyncLoggerConfig,
) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"max_pending": @json_parser.JsonValue::Number(config.max_pending.to_double()),
"max_pending": @json_parser.JsonValue::Number(
config.max_pending.to_double(),
),
"max_batch": @json_parser.JsonValue::Number(config.max_batch.to_double()),
"linger_ms": @json_parser.JsonValue::Number(config.linger_ms.to_double()),
"overflow": @json_parser.JsonValue::String(match config.overflow {
AsyncOverflowPolicy::Blocking => "Blocking"
AsyncOverflowPolicy::DropOldest => "DropOldest"
AsyncOverflowPolicy::DropNewest => "DropNewest"
}),
"flush": @json_parser.JsonValue::String(match config.flush {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
}),
"overflow": @json_parser.JsonValue::String(
match config.overflow {
AsyncOverflowPolicy::Blocking => "Blocking"
AsyncOverflowPolicy::DropOldest => "DropOldest"
AsyncOverflowPolicy::DropNewest => "DropNewest"
},
),
"flush": @json_parser.JsonValue::String(
match config.flush {
AsyncFlushPolicy::Never => "Never"
AsyncFlushPolicy::Batch => "Batch"
AsyncFlushPolicy::Shutdown => "Shutdown"
},
),
})
}
pub fn stringify_async_logger_config(config : AsyncLoggerConfig, pretty~ : Bool = false) -> String {
///|
pub fn stringify_async_logger_config(
config : AsyncLoggerConfig,
pretty? : Bool = false,
) -> String {
let value = async_logger_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
@@ -252,35 +317,45 @@ pub fn stringify_async_logger_config(config : AsyncLoggerConfig, pretty~ : Bool
}
}
///|
pub struct AsyncLoggerBuildConfig {
logger : @bitlogger.LoggerConfig
async_config : AsyncLoggerConfig
}
///|
pub fn AsyncLoggerBuildConfig::new(
logger~ : @bitlogger.LoggerConfig = @bitlogger.default_logger_config(),
async_config~ : AsyncLoggerConfig = AsyncLoggerConfig::new(),
logger? : @bitlogger.LoggerConfig = @bitlogger.default_logger_config(),
async_config? : AsyncLoggerConfig = AsyncLoggerConfig::new(),
) -> AsyncLoggerBuildConfig {
{ logger, async_config }
}
pub fn parse_async_logger_build_config_text(input : String) -> AsyncLoggerBuildConfig raise {
///|
pub fn parse_async_logger_build_config_text(
input : String,
) -> AsyncLoggerBuildConfig raise {
let root = @json_parser.parse(input)
let obj = match root.as_object() {
Some(obj) => obj
None => raise Failure::Failure("Expected object at async logger build config root")
None =>
raise Failure::Failure(
"Expected object at async logger build config root",
)
}
let logger = match obj.get("logger") {
Some(value) => @bitlogger.parse_logger_config_text(@json_parser.stringify(value))
Some(value) =>
@bitlogger.parse_logger_config_text(@json_parser.stringify(value))
None => @bitlogger.default_logger_config()
}
let async_config = match obj.get("async_config") {
Some(value) => parse_async_logger_config_text(@json_parser.stringify(value))
None => AsyncLoggerConfig::new()
}
AsyncLoggerBuildConfig::new(logger=logger, async_config=async_config)
AsyncLoggerBuildConfig::new(logger~, async_config~)
}
///|
pub fn async_logger_build_config_to_json(
config : AsyncLoggerBuildConfig,
) -> @json_parser.JsonValue {
@@ -290,9 +365,10 @@ pub fn async_logger_build_config_to_json(
})
}
///|
pub fn stringify_async_logger_build_config(
config : AsyncLoggerBuildConfig,
pretty~ : Bool = false,
pretty? : Bool = false,
) -> String {
let value = async_logger_build_config_to_json(config)
if pretty {
+1 -1
View File
@@ -1,4 +1,4 @@
import {
"Nanaloveyuki/BitLogger/src" @bitlogger,
"maria/json_parser" @json_parser,
"maria/json_parser",
}
+1798 -576
View File
File diff suppressed because it is too large Load Diff
+269 -154
View File
@@ -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")
+8 -2
View File
@@ -19,13 +19,17 @@ BitLogger 是一个使用 MoonBit 编写的结构化日志库.
## Example / 示例
```moonbit
```moonbit nocheck
///|
test {
let logger = build_logger(
text_console(
min_level=Level::Debug,
target="demo",
text_formatter=TextFormatterConfig::new(show_timestamp=false, separator=" | "),
text_formatter=TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
),
),
)
logger.info("starting", fields=[field("port", "8080")])
@@ -76,7 +80,9 @@ Project command note / 项目命令说明:
- `console_basic` / `text_formatter` / `style_tags` / `config_build` / `presets` are the portable-first set
- `console_basic` / `text_formatter` / `style_tags` / `config_build` / `presets` 是 portable-first 示例集
- `file_rotation` is native-sensitive, and `async_basic` stays native-only because of the current `async fn main` entry limitation
- `async_basic` currently relies on `moonbitlang/async`, whose upstream README only claims native/LLVM support on Linux/MacOS; do not treat Windows native as a supported baseline for this example until upstream support is stated clearly
- `file_rotation` 是 native-sensitive`async_basic` 的 native-only 限制来自当前 `async fn main` 入口能力
- `async_basic` 当前依赖 `moonbitlang/async`,其上游 README 目前只明确承诺 Linux/MacOS 的 native/LLVM 支持;在上游明确支持之前,不要把 Windows native 当作这个示例的支持基线
- package-level API docs / 单接口 API 文档:
- `../docs/api/`
- common entry points / 常用入口:
+3
View File
@@ -1,9 +1,12 @@
///|
pub type ApplicationLogger = ConfiguredLogger
///|
pub fn build_application_logger(config : LoggerConfig) -> ApplicationLogger {
build_logger(config)
}
///|
pub fn parse_and_build_application_logger(
input : String,
) -> ApplicationLogger raise ConfigError {
+45 -11
View File
@@ -1,66 +1,100 @@
///|
pub type ConfigError = @utils.ConfigError
///|
pub type SinkKind = @utils.SinkKind
///|
pub type TextFormatterConfig = @utils.TextFormatterConfig
///|
pub type QueueConfig = @utils.QueueConfig
///|
pub type SinkConfig = @utils.SinkConfig
///|
pub type LoggerConfig = @utils.LoggerConfig
///|
pub fn default_text_formatter_config() -> TextFormatterConfig {
@utils.default_text_formatter_config()
}
///|
pub fn default_sink_config() -> SinkConfig {
@utils.default_sink_config()
}
///|
pub fn default_logger_config() -> LoggerConfig {
@utils.default_logger_config()
}
pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigError {
///|
pub fn parse_logger_config_text(
input : String,
) -> LoggerConfig raise ConfigError {
@utils.parse_logger_config_text(input)
}
///|
pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue {
@utils.queue_config_to_json(queue)
}
pub fn stringify_queue_config(queue : QueueConfig, pretty~ : Bool = false) -> String {
@utils.stringify_queue_config(queue, pretty=pretty)
///|
pub fn stringify_queue_config(
queue : QueueConfig,
pretty? : Bool = false,
) -> String {
@utils.stringify_queue_config(queue, pretty~)
}
pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.JsonValue {
///|
pub fn text_formatter_config_to_json(
config : TextFormatterConfig,
) -> @json_parser.JsonValue {
@utils.text_formatter_config_to_json(config)
}
///|
pub fn stringify_text_formatter_config(
config : TextFormatterConfig,
pretty~ : Bool = false,
pretty? : Bool = false,
) -> String {
@utils.stringify_text_formatter_config(config, pretty=pretty)
@utils.stringify_text_formatter_config(config, pretty~)
}
pub fn file_rotation_config_to_json(config : FileRotation) -> @json_parser.JsonValue {
///|
pub fn file_rotation_config_to_json(
config : FileRotation,
) -> @json_parser.JsonValue {
@utils.file_rotation_config_to_json(config)
}
///|
pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue {
@utils.sink_config_to_json(config)
}
pub fn stringify_sink_config(config : SinkConfig, pretty~ : Bool = false) -> String {
@utils.stringify_sink_config(config, pretty=pretty)
///|
pub fn stringify_sink_config(
config : SinkConfig,
pretty? : Bool = false,
) -> String {
@utils.stringify_sink_config(config, pretty~)
}
///|
pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue {
@utils.logger_config_to_json(config)
}
pub fn stringify_logger_config(config : LoggerConfig, pretty~ : Bool = false) -> String {
@utils.stringify_logger_config(config, pretty=pretty)
///|
pub fn stringify_logger_config(
config : LoggerConfig,
pretty? : Bool = false,
) -> String {
@utils.stringify_logger_config(config, pretty~)
}
+4
View File
@@ -1,3 +1,4 @@
///|
pub(all) enum Level {
Trace
Debug
@@ -6,6 +7,7 @@ pub(all) enum Level {
Error
}
///|
pub fn Level::priority(self : Level) -> Int {
match self {
Level::Trace => 10
@@ -16,6 +18,7 @@ pub fn Level::priority(self : Level) -> Int {
}
}
///|
pub fn Level::label(self : Level) -> String {
match self {
Level::Trace => "TRACE"
@@ -26,6 +29,7 @@ pub fn Level::label(self : Level) -> String {
}
}
///|
pub fn Level::enabled(self : Level, min_level : Level) -> Bool {
self.priority() >= min_level.priority()
}
+16 -8
View File
@@ -1,18 +1,20 @@
///|
pub struct Field {
key : String
value : String
}
///|
pub fn field(key : String, value : String) -> Field {
{ key, value }
}
///|
pub fn fields(entries : Array[(String, String)]) -> Array[Field] {
entries.map(fn(entry) {
field(entry.0, entry.1)
})
entries.map(fn(entry) { field(entry.0, entry.1) })
}
///|
pub struct Record {
level : Level
timestamp_ms : UInt64
@@ -21,30 +23,34 @@ pub struct Record {
fields : Array[Field]
}
///|
pub fn Record::new(
level : Level,
message : String,
timestamp_ms~ : UInt64 = 0UL,
target~ : String = "",
fields~ : Array[Field] = [],
timestamp_ms? : UInt64 = 0UL,
target? : String = "",
fields? : Array[Field] = [],
) -> Record {
{ level, timestamp_ms, target, message, fields }
}
///|
pub fn Field::with_value(self : Field, value : String) -> Field {
field(self.key, value)
}
///|
pub fn Record::with_target(self : Record, target : String) -> Record {
Record::new(
self.level,
self.message,
timestamp_ms=self.timestamp_ms,
target=target,
target~,
fields=self.fields,
)
}
///|
pub fn Record::with_message(self : Record, message : String) -> Record {
Record::new(
self.level,
@@ -55,16 +61,18 @@ pub fn Record::with_message(self : Record, message : String) -> Record {
)
}
///|
pub fn Record::with_fields(self : Record, fields : Array[Field]) -> Record {
Record::new(
self.level,
self.message,
timestamp_ms=self.timestamp_ms,
target=self.target,
fields=fields,
fields~,
)
}
///|
pub fn Record::copy(self : Record) -> Record {
Record::new(
self.level,
+10
View File
@@ -1,37 +1,47 @@
///|
type FileHandle = @utils.FileHandle
///|
fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
@utils.open_file_handle_internal(path, append)
}
///|
fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
@utils.write_file_handle_internal(handle, content)
}
///|
fn flush_file_handle_internal(handle : FileHandle) -> Bool {
@utils.flush_file_handle_internal(handle)
}
///|
fn close_file_handle_internal(handle : FileHandle) -> Bool {
@utils.close_file_handle_internal(handle)
}
///|
fn file_size_internal(handle : FileHandle) -> Int {
@utils.file_size_internal(handle)
}
///|
fn rename_file_internal(from_path : String, to_path : String) -> Bool {
@utils.rename_file_internal(from_path, to_path)
}
///|
fn remove_file_internal(path : String) -> Bool {
@utils.remove_file_internal(path)
}
///|
fn string_byte_length_internal(content : String) -> Int {
@utils.string_byte_length_internal(content)
}
///|
fn native_files_supported_internal() -> Bool {
@utils.native_files_supported_internal()
}
+10
View File
@@ -1,37 +1,47 @@
///|
type FileHandle = @utils.FileHandle
///|
fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
@utils.open_file_handle_internal(path, append)
}
///|
fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
@utils.write_file_handle_internal(handle, content)
}
///|
fn flush_file_handle_internal(handle : FileHandle) -> Bool {
@utils.flush_file_handle_internal(handle)
}
///|
fn close_file_handle_internal(handle : FileHandle) -> Bool {
@utils.close_file_handle_internal(handle)
}
///|
fn file_size_internal(handle : FileHandle) -> Int {
@utils.file_size_internal(handle)
}
///|
fn rename_file_internal(from_path : String, to_path : String) -> Bool {
@utils.rename_file_internal(from_path, to_path)
}
///|
fn remove_file_internal(path : String) -> Bool {
@utils.remove_file_internal(path)
}
///|
fn string_byte_length_internal(content : String) -> Int {
@utils.string_byte_length_internal(content)
}
///|
fn native_files_supported_internal() -> Bool {
@utils.native_files_supported_internal()
}
+10
View File
@@ -1,37 +1,47 @@
///|
pub type RecordPredicate = @utils.RecordPredicate
///|
pub fn level_at_least(min_level : Level) -> RecordPredicate {
@utils.level_at_least(min_level)
}
///|
pub fn target_is(target : String) -> RecordPredicate {
@utils.target_is(target)
}
///|
pub fn target_has_prefix(prefix : String) -> RecordPredicate {
@utils.target_has_prefix(prefix)
}
///|
pub fn message_contains(fragment : String) -> RecordPredicate {
@utils.message_contains(fragment)
}
///|
pub fn has_field(key : String) -> RecordPredicate {
@utils.has_field(key)
}
///|
pub fn field_equals(key : String, value : String) -> RecordPredicate {
@utils.field_equals(key, value)
}
///|
pub fn not_(predicate : RecordPredicate) -> RecordPredicate {
@utils.not_(predicate)
}
///|
pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
@utils.all_of(predicates)
}
///|
pub fn any_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
@utils.any_of(predicates)
}
+57 -42
View File
@@ -1,103 +1,118 @@
///|
pub type RecordFormatter = @utils.RecordFormatter
///|
pub type ColorMode = @utils.ColorMode
///|
pub type ColorSupport = @utils.ColorSupport
///|
pub type StyleMarkupMode = @utils.StyleMarkupMode
///|
pub type TextStyle = @utils.TextStyle
///|
pub fn text_style(
fg~ : String? = None,
bg~ : String? = None,
bold~ : Bool = false,
dim~ : Bool = false,
italic~ : Bool = false,
underline~ : Bool = false,
fg? : String? = None,
bg? : String? = None,
bold? : Bool = false,
dim? : Bool = false,
italic? : Bool = false,
underline? : Bool = false,
) -> TextStyle {
@utils.text_style(
fg=fg,
bg=bg,
bold=bold,
dim=dim,
italic=italic,
underline=underline,
)
@utils.text_style(fg~, bg~, bold~, dim~, italic~, underline~)
}
///|
pub type StyleTagRegistry = @utils.StyleTagRegistry
///|
pub fn style_tag_registry() -> StyleTagRegistry {
@utils.style_tag_registry()
}
///|
pub fn default_style_tag_registry() -> StyleTagRegistry {
@utils.default_style_tag_registry()
}
///|
pub fn global_style_tag_registry() -> StyleTagRegistry {
@utils.global_style_tag_registry()
}
///|
pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit {
@utils.set_global_style_tag_registry(registry)
}
///|
pub fn reset_global_style_tag_registry() -> Unit {
@utils.reset_global_style_tag_registry()
}
///|
pub type TextFormatter = @utils.TextFormatter
///|
pub fn text_formatter(
show_timestamp~ : Bool = true,
show_level~ : Bool = true,
show_target~ : Bool = true,
show_fields~ : Bool = true,
separator~ : String = " ",
field_separator~ : String = " ",
template~ : String = "",
color_mode~ : ColorMode = ColorMode::Never,
color_support~ : ColorSupport = ColorSupport::TrueColor,
style_markup~ : StyleMarkupMode = StyleMarkupMode::Full,
target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
style_tags~ : StyleTagRegistry? = None,
show_timestamp? : Bool = true,
show_level? : Bool = true,
show_target? : Bool = true,
show_fields? : Bool = true,
separator? : String = " ",
field_separator? : String = " ",
template? : String = "",
color_mode? : ColorMode = ColorMode::Never,
color_support? : ColorSupport = ColorSupport::TrueColor,
style_markup? : StyleMarkupMode = StyleMarkupMode::Full,
target_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled,
fields_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled,
style_tags? : StyleTagRegistry? = None,
) -> TextFormatter {
@utils.text_formatter(
show_timestamp=show_timestamp,
show_level=show_level,
show_target=show_target,
show_fields=show_fields,
separator=separator,
field_separator=field_separator,
template=template,
color_mode=color_mode,
color_support=color_support,
style_markup=style_markup,
target_style_markup=target_style_markup,
fields_style_markup=fields_style_markup,
style_tags=style_tags,
show_timestamp~,
show_level~,
show_target~,
show_fields~,
separator~,
field_separator~,
template~,
color_mode~,
color_support~,
style_markup~,
target_style_markup~,
fields_style_markup~,
style_tags~,
)
}
///|
pub fn color_support_label(support : ColorSupport) -> String {
@utils.color_support_label(support)
}
///|
pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String {
@utils.style_markup_mode_label(mode)
}
///|
pub fn color_mode_label(mode : ColorMode) -> String {
@utils.color_mode_label(mode)
}
pub fn format_text(rec : Record, formatter~ : TextFormatter = text_formatter()) -> String {
@utils.format_text(rec, formatter=formatter)
///|
pub fn format_text(
rec : Record,
formatter? : TextFormatter = text_formatter(),
) -> String {
@utils.format_text(rec, formatter~)
}
///|
pub fn format_json(rec : Record) -> String {
@utils.format_json(rec)
}
+13 -1
View File
@@ -1,15 +1,27 @@
///|
let default_console_sink : ConsoleSink = console_sink()
///|
let default_min_level_ref : Ref[Level] = Ref(Level::Info)
///|
let default_target_ref : Ref[String] = Ref("")
///|
pub fn set_default_min_level(level : Level) -> Unit {
default_min_level_ref.val = level
}
///|
pub fn set_default_target(target : String) -> Unit {
default_target_ref.val = target
}
///|
pub fn default_logger() -> Logger[ConsoleSink] {
Logger::new(default_console_sink, min_level=default_min_level_ref.val, target=default_target_ref.val)
Logger::new(
default_console_sink,
min_level=default_min_level_ref.val,
target=default_target_ref.val,
)
}
+22 -12
View File
@@ -1,23 +1,33 @@
pub fn log(level : Level, message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().log(level, message, fields=fields)
///|
pub fn log(
level : Level,
message : String,
fields? : Array[Field] = [],
) -> Unit {
default_logger().log(level, message, fields~)
}
pub fn trace(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().trace(message, fields=fields)
///|
pub fn trace(message : String, fields? : Array[Field] = []) -> Unit {
default_logger().trace(message, fields~)
}
pub fn debug(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().debug(message, fields=fields)
///|
pub fn debug(message : String, fields? : Array[Field] = []) -> Unit {
default_logger().debug(message, fields~)
}
pub fn info(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().info(message, fields=fields)
///|
pub fn info(message : String, fields? : Array[Field] = []) -> Unit {
default_logger().info(message, fields~)
}
pub fn warn(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().warn(message, fields=fields)
///|
pub fn warn(message : String, fields? : Array[Field] = []) -> Unit {
default_logger().warn(message, fields~)
}
pub fn error(message : String, fields~ : Array[Field] = []) -> Unit {
default_logger().error(message, fields=fields)
///|
pub fn error(message : String, fields? : Array[Field] = []) -> Unit {
default_logger().error(message, fields~)
}
+1
View File
@@ -1 +1,2 @@
///|
pub type Level = @core.Level
+47 -16
View File
@@ -1,49 +1,70 @@
///|
pub struct LibraryLogger[S] {
inner : Logger[S]
}
///|
fn[S] library_logger(logger : Logger[S]) -> LibraryLogger[S] {
{ inner: logger }
}
///|
pub fn[S] Logger::to_library_logger(self : Logger[S]) -> LibraryLogger[S] {
library_logger(self)
}
///|
pub fn[S] LibraryLogger::new(
sink : S,
min_level~ : Level = Level::Info,
target~ : String = "",
min_level? : Level = Level::Info,
target? : String = "",
) -> LibraryLogger[S] {
library_logger(Logger::new(sink, min_level=min_level, target=target))
library_logger(Logger::new(sink, min_level~, target~))
}
///|
pub fn[S] LibraryLogger::to_logger(self : LibraryLogger[S]) -> Logger[S] {
self.inner
}
fn configured_library_logger(logger : ConfiguredLogger) -> LibraryLogger[RuntimeSink] {
///|
fn configured_library_logger(
logger : ConfiguredLogger,
) -> LibraryLogger[RuntimeSink] {
library_logger(logger)
}
pub fn build_library_logger(config : LoggerConfig) -> LibraryLogger[RuntimeSink] {
///|
pub fn build_library_logger(
config : LoggerConfig,
) -> LibraryLogger[RuntimeSink] {
configured_library_logger(build_logger(config))
}
///|
pub fn parse_and_build_library_logger(
input : String,
) -> LibraryLogger[RuntimeSink] raise ConfigError {
configured_library_logger(parse_and_build_logger(input))
}
pub fn[S] LibraryLogger::with_target(self : LibraryLogger[S], target : String) -> LibraryLogger[S] {
///|
pub fn[S] LibraryLogger::with_target(
self : LibraryLogger[S],
target : String,
) -> LibraryLogger[S] {
library_logger(self.inner.with_target(target))
}
pub fn[S] LibraryLogger::child(self : LibraryLogger[S], target : String) -> LibraryLogger[S] {
///|
pub fn[S] LibraryLogger::child(
self : LibraryLogger[S],
target : String,
) -> LibraryLogger[S] {
library_logger(self.inner.child(target))
}
///|
pub fn[S] LibraryLogger::with_context_fields(
self : LibraryLogger[S],
fields : Array[Field],
@@ -51,6 +72,7 @@ pub fn[S] LibraryLogger::with_context_fields(
library_logger(self.inner.with_context_fields(fields))
}
///|
pub fn[S] LibraryLogger::bind(
self : LibraryLogger[S],
fields : Array[Field],
@@ -58,44 +80,53 @@ pub fn[S] LibraryLogger::bind(
self.with_context_fields(fields)
}
pub fn[S] LibraryLogger::is_enabled(self : LibraryLogger[S], level : Level) -> Bool {
///|
pub fn[S] LibraryLogger::is_enabled(
self : LibraryLogger[S],
level : Level,
) -> Bool {
self.inner.is_enabled(level)
}
///|
pub fn[S : Sink] LibraryLogger::log(
self : LibraryLogger[S],
level : Level,
message : String,
fields~ : Array[Field] = [],
fields? : Array[Field] = [],
target? : String = "",
) -> Unit {
self.inner.log(level, message, fields=fields, target=target)
self.inner.log(level, message, fields~, target~)
}
///|
pub fn[S : Sink] LibraryLogger::info(
self : LibraryLogger[S],
message : String,
fields~ : Array[Field] = [],
fields? : Array[Field] = [],
) -> Unit {
self.inner.info(message, fields=fields)
self.inner.info(message, fields~)
}
///|
pub fn[S : Sink] LibraryLogger::warn(
self : LibraryLogger[S],
message : String,
fields~ : Array[Field] = [],
fields? : Array[Field] = [],
) -> Unit {
self.inner.warn(message, fields=fields)
self.inner.warn(message, fields~)
}
///|
pub fn[S : Sink] LibraryLogger::error(
self : LibraryLogger[S],
message : String,
fields~ : Array[Field] = [],
fields? : Array[Field] = [],
) -> Unit {
self.inner.error(message, fields=fields)
self.inner.error(message, fields~)
}
///|
pub fn default_library_logger() -> LibraryLogger[ConsoleSink] {
library_logger(default_logger())
}
+47 -12
View File
@@ -1,3 +1,4 @@
///|
pub struct Logger[S] {
min_level : Level
sink : S
@@ -5,14 +6,21 @@ pub struct Logger[S] {
timestamp : Bool
}
pub fn[S] Logger::new(sink : S, min_level~ : Level = Level::Info, target~ : String = "") -> Logger[S] {
///|
pub fn[S] Logger::new(
sink : S,
min_level? : Level = Level::Info,
target? : String = "",
) -> Logger[S] {
{ min_level, sink, target, timestamp: false }
}
///|
pub fn[S] Logger::with_target(self : Logger[S], target : String) -> Logger[S] {
{ ..self, target }
{ ..self, target, }
}
///|
fn combine_targets(parent : String, child : String) -> String {
if parent == "" {
child
@@ -23,11 +31,16 @@ fn combine_targets(parent : String, child : String) -> String {
}
}
///|
pub fn[S] Logger::child(self : Logger[S], target : String) -> Logger[S] {
{ ..self, target: combine_targets(self.target, target) }
}
pub fn[S] Logger::with_context_fields(self : Logger[S], fields : Array[Field]) -> Logger[ContextSink[S]] {
///|
pub fn[S] Logger::with_context_fields(
self : Logger[S],
fields : Array[Field],
) -> Logger[ContextSink[S]] {
{
min_level: self.min_level,
sink: ContextSink::{ sink: self.sink, context_fields: fields },
@@ -36,11 +49,19 @@ pub fn[S] Logger::with_context_fields(self : Logger[S], fields : Array[Field]) -
}
}
pub fn[S] Logger::bind(self : Logger[S], fields : Array[Field]) -> Logger[ContextSink[S]] {
///|
pub fn[S] Logger::bind(
self : Logger[S],
fields : Array[Field],
) -> Logger[ContextSink[S]] {
self.with_context_fields(fields)
}
pub fn[S] Logger::with_filter(self : Logger[S], predicate : (Record) -> Bool) -> Logger[FilterSink[S]] {
///|
pub fn[S] Logger::with_filter(
self : Logger[S],
predicate : (Record) -> Bool,
) -> Logger[FilterSink[S]] {
{
min_level: self.min_level,
sink: filter_sink(self.sink, predicate),
@@ -49,7 +70,11 @@ pub fn[S] Logger::with_filter(self : Logger[S], predicate : (Record) -> Bool) ->
}
}
pub fn[S] Logger::with_patch(self : Logger[S], patch : RecordPatch) -> Logger[PatchSink[S]] {
///|
pub fn[S] Logger::with_patch(
self : Logger[S],
patch : RecordPatch,
) -> Logger[PatchSink[S]] {
{
min_level: self.min_level,
sink: patch_sink(self.sink, patch),
@@ -58,27 +83,37 @@ pub fn[S] Logger::with_patch(self : Logger[S], patch : RecordPatch) -> Logger[Pa
}
}
///|
pub fn[S] Logger::with_queue(
self : Logger[S],
max_pending~ : Int = 0,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
max_pending? : Int = 0,
overflow? : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> Logger[QueuedSink[S]] {
{
min_level: self.min_level,
sink: queued_sink(self.sink, max_pending=max_pending, overflow=overflow),
sink: queued_sink(self.sink, max_pending~, overflow~),
target: self.target,
timestamp: self.timestamp,
}
}
pub fn[S] Logger::with_min_level(self : Logger[S], min_level : Level) -> Logger[S] {
{ ..self, min_level }
///|
pub fn[S] Logger::with_min_level(
self : Logger[S],
min_level : Level,
) -> Logger[S] {
{ ..self, min_level, }
}
pub fn[S] Logger::with_timestamp(self : Logger[S], enabled~ : Bool = true) -> Logger[S] {
///|
pub fn[S] Logger::with_timestamp(
self : Logger[S],
enabled? : Bool = true,
) -> Logger[S] {
{ ..self, timestamp: enabled }
}
///|
pub fn[S] Logger::is_enabled(self : Logger[S], level : Level) -> Bool {
level.enabled(self.min_level)
}
+38 -12
View File
@@ -1,8 +1,9 @@
///|
pub fn[S : Sink] Logger::log(
self : Logger[S],
level : Level,
message : String,
fields~ : Array[Field] = [],
fields? : Array[Field] = [],
target? : String = "",
) -> Unit {
if !self.is_enabled(level) {
@@ -11,27 +12,52 @@ pub fn[S : Sink] Logger::log(
let actual_target = if target == "" { self.target } else { target }
let timestamp_ms = if self.timestamp { @env.now() } else { 0UL }
self.sink.write(
record(level, message, timestamp_ms=timestamp_ms, target=actual_target, fields=fields),
record(level, message, timestamp_ms~, target=actual_target, fields~),
)
}
}
pub fn[S : Sink] Logger::trace(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Trace, message, fields=fields)
///|
pub fn[S : Sink] Logger::trace(
self : Logger[S],
message : String,
fields? : Array[Field] = [],
) -> Unit {
self.log(Level::Trace, message, fields~)
}
pub fn[S : Sink] Logger::debug(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Debug, message, fields=fields)
///|
pub fn[S : Sink] Logger::debug(
self : Logger[S],
message : String,
fields? : Array[Field] = [],
) -> Unit {
self.log(Level::Debug, message, fields~)
}
pub fn[S : Sink] Logger::info(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Info, message, fields=fields)
///|
pub fn[S : Sink] Logger::info(
self : Logger[S],
message : String,
fields? : Array[Field] = [],
) -> Unit {
self.log(Level::Info, message, fields~)
}
pub fn[S : Sink] Logger::warn(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Warn, message, fields=fields)
///|
pub fn[S : Sink] Logger::warn(
self : Logger[S],
message : String,
fields? : Array[Field] = [],
) -> Unit {
self.log(Level::Warn, message, fields~)
}
pub fn[S : Sink] Logger::error(self : Logger[S], message : String, fields~ : Array[Field] = []) -> Unit {
self.log(Level::Error, message, fields=fields)
///|
pub fn[S : Sink] Logger::error(
self : Logger[S],
message : String,
fields? : Array[Field] = [],
) -> Unit {
self.log(Level::Error, message, fields~)
}
+5 -5
View File
@@ -1,12 +1,12 @@
import {
"Nanaloveyuki/BitLogger/src/core" @core,
"Nanaloveyuki/BitLogger/src/utils" @utils,
"maria/json_parser" @json_parser,
"Nanaloveyuki/BitLogger/src/core",
"Nanaloveyuki/BitLogger/src/utils",
"maria/json_parser",
"moonbitlang/core/array",
"moonbitlang/core/builtin",
"moonbitlang/core/env" @env,
"moonbitlang/core/env",
"moonbitlang/core/json",
"moonbitlang/core/queue" @queue,
"moonbitlang/core/queue",
"moonbitlang/core/ref",
}
+15 -4
View File
@@ -1,29 +1,40 @@
///|
pub type RecordPatch = @utils.RecordPatch
///|
pub fn identity_patch() -> RecordPatch {
@utils.identity_patch()
}
///|
pub fn set_target(target : String) -> RecordPatch {
@utils.set_target(target)
}
///|
pub fn prefix_message(prefix : String) -> RecordPatch {
@utils.prefix_message(prefix)
}
///|
pub fn append_fields(extra_fields : Array[Field]) -> RecordPatch {
@utils.append_fields(extra_fields)
}
pub fn redact_field(key : String, placeholder~ : String = "***") -> RecordPatch {
@utils.redact_field(key, placeholder=placeholder)
///|
pub fn redact_field(key : String, placeholder? : String = "***") -> RecordPatch {
@utils.redact_field(key, placeholder~)
}
pub fn redact_fields(keys : Array[String], placeholder~ : String = "***") -> RecordPatch {
@utils.redact_fields(keys, placeholder=placeholder)
///|
pub fn redact_fields(
keys : Array[String],
placeholder? : String = "***",
) -> RecordPatch {
@utils.redact_fields(keys, placeholder~)
}
///|
pub fn compose_patches(patches : Array[RecordPatch]) -> RecordPatch {
@utils.compose_patches(patches)
}
+46 -40
View File
@@ -1,89 +1,95 @@
///|
pub fn console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
min_level? : Level = Level::Info,
target? : String = "",
timestamp? : Bool = false,
) -> LoggerConfig {
LoggerConfig::new(
min_level=min_level,
target=target,
timestamp=timestamp,
min_level~,
target~,
timestamp~,
sink=SinkConfig::new(kind=SinkKind::Console),
)
}
///|
pub fn json_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
min_level? : Level = Level::Info,
target? : String = "",
timestamp? : Bool = false,
) -> LoggerConfig {
LoggerConfig::new(
min_level=min_level,
target=target,
timestamp=timestamp,
min_level~,
target~,
timestamp~,
sink=SinkConfig::new(kind=SinkKind::JsonConsole),
)
}
///|
pub fn text_console(
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
min_level? : Level = Level::Info,
target? : String = "",
timestamp? : Bool = false,
text_formatter? : TextFormatterConfig = default_text_formatter_config(),
) -> LoggerConfig {
LoggerConfig::new(
min_level=min_level,
target=target,
timestamp=timestamp,
sink=SinkConfig::new(kind=SinkKind::TextConsole, text_formatter=text_formatter),
min_level~,
target~,
timestamp~,
sink=SinkConfig::new(kind=SinkKind::TextConsole, text_formatter~),
)
}
///|
pub fn file(
path : String,
min_level~ : Level = Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
min_level? : Level = Level::Info,
target? : String = "",
timestamp? : Bool = false,
append? : Bool = true,
auto_flush? : Bool = true,
rotation? : FileRotation? = None,
text_formatter? : TextFormatterConfig = default_text_formatter_config(),
) -> LoggerConfig raise ConfigError {
if path == "" {
raise ConfigError::InvalidConfig("File sink requires non-empty path")
}
LoggerConfig::new(
min_level=min_level,
target=target,
timestamp=timestamp,
min_level~,
target~,
timestamp~,
sink=SinkConfig::new(
kind=SinkKind::File,
path=path,
append=append,
auto_flush=auto_flush,
rotation=rotation,
text_formatter=text_formatter,
path~,
append~,
auto_flush~,
rotation~,
text_formatter~,
),
)
}
///|
pub fn with_queue(
config : LoggerConfig,
max_pending~ : Int = 0,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
max_pending? : Int = 0,
overflow? : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> LoggerConfig {
LoggerConfig::new(
min_level=config.min_level,
target=config.target,
timestamp=config.timestamp,
sink=config.sink,
queue=Some(QueueConfig::new(max_pending, overflow=overflow)),
queue=Some(QueueConfig::new(max_pending, overflow~)),
)
}
///|
pub fn with_file_rotation(
config : LoggerConfig,
max_bytes : Int,
max_backups~ : Int = 1,
max_backups? : Int = 1,
) -> LoggerConfig {
match config.sink.kind {
SinkKind::File =>
@@ -96,7 +102,7 @@ pub fn with_file_rotation(
path=config.sink.path,
append=config.sink.append,
auto_flush=config.sink.auto_flush,
rotation=Some(file_rotation(max_bytes, max_backups=max_backups)),
rotation=Some(file_rotation(max_bytes, max_backups~)),
text_formatter=config.sink.text_formatter,
),
queue=config.queue,
+97 -45
View File
@@ -1,39 +1,56 @@
///|
test "logger config presets use expected defaults" {
let console_config = console()
inspect(console_config.min_level.label(), content="INFO")
inspect(console_config.target, content="")
inspect(console_config.timestamp, content="false")
inspect(match console_config.sink.kind {
SinkKind::Console => "Console"
_ => "other"
}, content="Console")
inspect(
match console_config.sink.kind {
SinkKind::Console => "Console"
_ => "other"
},
content="Console",
)
inspect(console_config.queue is None, content="true")
let json_config = json_console()
inspect(match json_config.sink.kind {
SinkKind::JsonConsole => "JsonConsole"
_ => "other"
}, content="JsonConsole")
inspect(
match json_config.sink.kind {
SinkKind::JsonConsole => "JsonConsole"
_ => "other"
},
content="JsonConsole",
)
inspect(json_config.queue is None, content="true")
let text_config = text_console()
inspect(match text_config.sink.kind {
SinkKind::TextConsole => "TextConsole"
_ => "other"
}, content="TextConsole")
inspect(
match text_config.sink.kind {
SinkKind::TextConsole => "TextConsole"
_ => "other"
},
content="TextConsole",
)
inspect(text_config.sink.text_formatter.show_timestamp, content="true")
inspect(color_mode_label(text_config.sink.text_formatter.color_mode), content="never")
inspect(
color_mode_label(text_config.sink.text_formatter.color_mode),
content="never",
)
}
///|
test "file preset uses file sink defaults" {
let config = file("bitlogger.log")
inspect(config.min_level.label(), content="INFO")
inspect(config.target, content="")
inspect(config.timestamp, content="false")
inspect(match config.sink.kind {
SinkKind::File => "File"
_ => "other"
}, content="File")
inspect(
match config.sink.kind {
SinkKind::File => "File"
_ => "other"
},
content="File",
)
inspect(config.sink.path, content="bitlogger.log")
inspect(config.sink.append, content="true")
inspect(config.sink.auto_flush, content="true")
@@ -41,6 +58,7 @@ test "file preset uses file sink defaults" {
inspect(config.queue is None, content="true")
}
///|
test "file preset rejects empty path like parser file sink config" {
let preset_error = (fn() -> String raise ConfigError {
ignore(file(""))
@@ -51,7 +69,9 @@ test "file preset rejects empty path like parser file sink config" {
inspect(preset_error, content="File sink requires non-empty path")
let parser_error = (fn() -> String raise ConfigError {
ignore(parse_logger_config_text("{\"sink\":{\"kind\":\"file\",\"path\":\"\"}}"))
ignore(
parse_logger_config_text("{\"sink\":{\"kind\":\"file\",\"path\":\"\"}}"),
)
"no error"
})() catch {
ConfigError::InvalidConfig(message) => message
@@ -59,8 +79,12 @@ test "file preset rejects empty path like parser file sink config" {
inspect(parser_error, content="File sink requires non-empty path")
}
///|
test "preset helpers compose queue and file rotation without losing config" {
let formatter = TextFormatterConfig::new(show_timestamp=false, separator=" | ")
let formatter = TextFormatterConfig::new(
show_timestamp=false,
separator=" | ",
)
let config = file(
"service.log",
min_level=Level::Warn,
@@ -86,10 +110,13 @@ test "preset helpers compose queue and file rotation without losing config" {
match config.queue {
Some(queue) => {
inspect(queue.max_pending, content="32")
inspect(match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
}, content="DropOldest")
inspect(
match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
},
content="DropOldest",
)
}
None => inspect(false, content="true")
}
@@ -102,6 +129,7 @@ test "preset helpers compose queue and file rotation without losing config" {
}
}
///|
test "queue helper preserves file rotation when applied after rotation" {
let config = with_queue(
with_file_rotation(file("service.log", append=false), 256, max_backups=2),
@@ -113,10 +141,13 @@ test "queue helper preserves file rotation when applied after rotation" {
match config.queue {
Some(queue) => {
inspect(queue.max_pending, content="8")
inspect(match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
}, content="DropNewest")
inspect(
match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
},
content="DropNewest",
)
}
None => inspect(false, content="true")
}
@@ -129,29 +160,44 @@ test "queue helper preserves file rotation when applied after rotation" {
}
}
///|
test "file rotation helper leaves non-file presets unchanged" {
let console_config = with_file_rotation(console(target="console"), 64, max_backups=2)
inspect(match console_config.sink.kind {
SinkKind::Console => "Console"
_ => "other"
}, content="Console")
let console_config = with_file_rotation(
console(target="console"),
64,
max_backups=2,
)
inspect(
match console_config.sink.kind {
SinkKind::Console => "Console"
_ => "other"
},
content="Console",
)
inspect(console_config.target, content="console")
inspect(console_config.sink.rotation is None, content="true")
let text_config = with_file_rotation(
text_console(target="text", text_formatter=TextFormatterConfig::new(separator=" :: ")),
text_console(
target="text",
text_formatter=TextFormatterConfig::new(separator=" :: "),
),
96,
max_backups=4,
)
inspect(match text_config.sink.kind {
SinkKind::TextConsole => "TextConsole"
_ => "other"
}, content="TextConsole")
inspect(
match text_config.sink.kind {
SinkKind::TextConsole => "TextConsole"
_ => "other"
},
content="TextConsole",
)
inspect(text_config.target, content="text")
inspect(text_config.sink.text_formatter.separator, content=" :: ")
inspect(text_config.sink.rotation is None, content="true")
}
///|
test "file rotation helper keeps queued non-file presets unchanged" {
let config = with_queue(
json_console(min_level=Level::Error, target="json"),
@@ -161,18 +207,24 @@ test "file rotation helper keeps queued non-file presets unchanged" {
let rotated = with_file_rotation(config, 512, max_backups=5)
inspect(rotated.min_level.label(), content="ERROR")
inspect(rotated.target, content="json")
inspect(match rotated.sink.kind {
SinkKind::JsonConsole => "JsonConsole"
_ => "other"
}, content="JsonConsole")
inspect(
match rotated.sink.kind {
SinkKind::JsonConsole => "JsonConsole"
_ => "other"
},
content="JsonConsole",
)
inspect(rotated.sink.rotation is None, content="true")
match rotated.queue {
Some(queue) => {
inspect(queue.max_pending, content="4")
inspect(match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
}, content="DropOldest")
inspect(
match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
},
content="DropOldest",
)
}
None => inspect(false, content="true")
}
+9 -4
View File
@@ -1,21 +1,26 @@
///|
pub type Field = @core.Field
///|
pub fn field(key : String, value : String) -> Field {
@core.field(key, value)
}
///|
pub fn fields(entries : Array[(String, String)]) -> Array[Field] {
@core.fields(entries)
}
///|
pub type Record = @core.Record
///|
fn record(
level : Level,
message : String,
timestamp_ms~ : UInt64 = 0UL,
target~ : String = "",
fields~ : Array[Field] = [],
timestamp_ms? : UInt64 = 0UL,
target? : String = "",
fields? : Array[Field] = [],
) -> Record {
@core.Record::new(level, message, timestamp_ms=timestamp_ms, target=target, fields=fields)
@core.Record::new(level, message, timestamp_ms~, target~, fields~)
}
+134 -37
View File
@@ -1,3 +1,4 @@
///|
pub fn RuntimeSink::file_available(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.is_available()
@@ -6,14 +7,19 @@ pub fn RuntimeSink::file_available(self : RuntimeSink) -> Bool {
}
}
pub fn RuntimeSink::file_reopen(self : RuntimeSink, append~ : Bool? = None) -> Bool {
///|
pub fn RuntimeSink::file_reopen(
self : RuntimeSink,
append? : Bool? = None,
) -> Bool {
match self {
File(sink) => sink.reopen(append=append)
QueuedFile(sink) => sink.sink.reopen(append=append)
File(sink) => sink.reopen(append~)
QueuedFile(sink) => sink.sink.reopen(append~)
_ => false
}
}
///|
pub fn RuntimeSink::file_reopen_with_current_policy(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.reopen_with_current_policy()
@@ -22,6 +28,7 @@ pub fn RuntimeSink::file_reopen_with_current_policy(self : RuntimeSink) -> Bool
}
}
///|
pub fn RuntimeSink::file_reopen_append(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.reopen_append()
@@ -30,6 +37,7 @@ pub fn RuntimeSink::file_reopen_append(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_reopen_truncate(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.reopen_truncate()
@@ -38,6 +46,7 @@ pub fn RuntimeSink::file_reopen_truncate(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_append_mode(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.append_mode()
@@ -46,7 +55,11 @@ pub fn RuntimeSink::file_append_mode(self : RuntimeSink) -> Bool {
}
}
pub fn RuntimeSink::file_set_append_mode(self : RuntimeSink, append : Bool) -> Bool {
///|
pub fn RuntimeSink::file_set_append_mode(
self : RuntimeSink,
append : Bool,
) -> Bool {
match self {
File(sink) => {
sink.set_append_mode(append)
@@ -60,6 +73,7 @@ pub fn RuntimeSink::file_set_append_mode(self : RuntimeSink, append : Bool) -> B
}
}
///|
pub fn RuntimeSink::file_path(self : RuntimeSink) -> String {
match self {
File(sink) => sink.path()
@@ -68,6 +82,7 @@ pub fn RuntimeSink::file_path(self : RuntimeSink) -> String {
}
}
///|
pub fn RuntimeSink::file_auto_flush(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.auto_flush_enabled()
@@ -76,6 +91,7 @@ pub fn RuntimeSink::file_auto_flush(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_rotation_enabled(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.rotation_enabled()
@@ -84,6 +100,7 @@ pub fn RuntimeSink::file_rotation_enabled(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_rotation_config(self : RuntimeSink) -> FileRotation? {
match self {
File(sink) => sink.rotation_config()
@@ -92,7 +109,11 @@ pub fn RuntimeSink::file_rotation_config(self : RuntimeSink) -> FileRotation? {
}
}
pub fn RuntimeSink::file_set_auto_flush(self : RuntimeSink, enabled : Bool) -> Bool {
///|
pub fn RuntimeSink::file_set_auto_flush(
self : RuntimeSink,
enabled : Bool,
) -> Bool {
match self {
File(sink) => {
sink.set_auto_flush(enabled)
@@ -106,7 +127,11 @@ pub fn RuntimeSink::file_set_auto_flush(self : RuntimeSink, enabled : Bool) -> B
}
}
pub fn RuntimeSink::file_set_policy(self : RuntimeSink, policy : FileSinkPolicy) -> Bool {
///|
pub fn RuntimeSink::file_set_policy(
self : RuntimeSink,
policy : FileSinkPolicy,
) -> Bool {
match self {
File(sink) => {
sink.set_policy(policy)
@@ -120,7 +145,11 @@ pub fn RuntimeSink::file_set_policy(self : RuntimeSink, policy : FileSinkPolicy)
}
}
pub fn RuntimeSink::file_set_rotation(self : RuntimeSink, rotation : FileRotation?) -> Bool {
///|
pub fn RuntimeSink::file_set_rotation(
self : RuntimeSink,
rotation : FileRotation?,
) -> Bool {
match self {
File(sink) => {
sink.set_rotation(rotation)
@@ -134,6 +163,7 @@ pub fn RuntimeSink::file_set_rotation(self : RuntimeSink, rotation : FileRotatio
}
}
///|
pub fn RuntimeSink::file_clear_rotation(self : RuntimeSink) -> Bool {
match self {
File(sink) => {
@@ -148,6 +178,7 @@ pub fn RuntimeSink::file_clear_rotation(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_flush(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.flush()
@@ -159,6 +190,7 @@ pub fn RuntimeSink::file_flush(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_close(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.close()
@@ -170,6 +202,7 @@ pub fn RuntimeSink::file_close(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_open_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.open_failures()
@@ -178,6 +211,7 @@ pub fn RuntimeSink::file_open_failures(self : RuntimeSink) -> Int {
}
}
///|
pub fn RuntimeSink::file_write_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.write_failures()
@@ -186,6 +220,7 @@ pub fn RuntimeSink::file_write_failures(self : RuntimeSink) -> Int {
}
}
///|
pub fn RuntimeSink::file_flush_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.flush_failures()
@@ -194,6 +229,7 @@ pub fn RuntimeSink::file_flush_failures(self : RuntimeSink) -> Int {
}
}
///|
pub fn RuntimeSink::file_rotation_failures(self : RuntimeSink) -> Int {
match self {
File(sink) => sink.rotation_failures()
@@ -202,6 +238,7 @@ pub fn RuntimeSink::file_rotation_failures(self : RuntimeSink) -> Int {
}
}
///|
pub fn RuntimeSink::file_reset_failure_counters(self : RuntimeSink) -> Bool {
match self {
File(sink) => {
@@ -216,6 +253,7 @@ pub fn RuntimeSink::file_reset_failure_counters(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_reset_policy(self : RuntimeSink) -> Bool {
match self {
File(sink) => {
@@ -230,6 +268,7 @@ pub fn RuntimeSink::file_reset_policy(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_policy(self : RuntimeSink) -> FileSinkPolicy {
match self {
File(sink) => sink.policy()
@@ -238,6 +277,7 @@ pub fn RuntimeSink::file_policy(self : RuntimeSink) -> FileSinkPolicy {
}
}
///|
pub fn RuntimeSink::file_default_policy(self : RuntimeSink) -> FileSinkPolicy {
match self {
File(sink) => sink.default_policy()
@@ -246,6 +286,7 @@ pub fn RuntimeSink::file_default_policy(self : RuntimeSink) -> FileSinkPolicy {
}
}
///|
pub fn RuntimeSink::file_policy_matches_default(self : RuntimeSink) -> Bool {
match self {
File(sink) => sink.policy_matches_default()
@@ -254,91 +295,125 @@ pub fn RuntimeSink::file_policy_matches_default(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::file_state(self : RuntimeSink) -> FileSinkState {
match self {
File(sink) => sink.state()
QueuedFile(sink) => sink.sink.state()
_ => FileSinkState::new(
"",
available=false,
append=false,
auto_flush=false,
rotation=None,
open_failures=0,
write_failures=0,
flush_failures=0,
rotation_failures=0,
)
_ =>
FileSinkState::new(
"",
available=false,
append=false,
auto_flush=false,
rotation=None,
open_failures=0,
write_failures=0,
flush_failures=0,
rotation_failures=0,
)
}
}
///|
pub fn RuntimeSink::file_runtime_state(self : RuntimeSink) -> RuntimeFileState? {
match self {
File(sink) => Some(RuntimeFileState::new(sink.state()))
QueuedFile(sink) => Some(
RuntimeFileState::new(
sink.sink.state(),
queued=true,
pending_count=sink.pending_count(),
dropped_count=sink.dropped_count(),
),
)
QueuedFile(sink) =>
Some(
RuntimeFileState::new(
sink.sink.state(),
queued=true,
pending_count=sink.pending_count(),
dropped_count=sink.dropped_count(),
),
)
_ => None
}
}
///|
pub fn ConfiguredLogger::file_available(self : ConfiguredLogger) -> Bool {
self.sink.file_available()
}
pub fn ConfiguredLogger::file_reopen(self : ConfiguredLogger, append~ : Bool? = None) -> Bool {
self.sink.file_reopen(append=append)
///|
pub fn ConfiguredLogger::file_reopen(
self : ConfiguredLogger,
append? : Bool? = None,
) -> Bool {
self.sink.file_reopen(append~)
}
pub fn ConfiguredLogger::file_reopen_with_current_policy(self : ConfiguredLogger) -> Bool {
///|
pub fn ConfiguredLogger::file_reopen_with_current_policy(
self : ConfiguredLogger,
) -> Bool {
self.sink.file_reopen_with_current_policy()
}
///|
pub fn ConfiguredLogger::file_reopen_append(self : ConfiguredLogger) -> Bool {
self.sink.file_reopen_append()
}
///|
pub fn ConfiguredLogger::file_reopen_truncate(self : ConfiguredLogger) -> Bool {
self.sink.file_reopen_truncate()
}
///|
pub fn ConfiguredLogger::file_append_mode(self : ConfiguredLogger) -> Bool {
self.sink.file_append_mode()
}
pub fn ConfiguredLogger::file_set_append_mode(self : ConfiguredLogger, append : Bool) -> Bool {
///|
pub fn ConfiguredLogger::file_set_append_mode(
self : ConfiguredLogger,
append : Bool,
) -> Bool {
self.sink.file_set_append_mode(append)
}
///|
pub fn ConfiguredLogger::file_path(self : ConfiguredLogger) -> String {
self.sink.file_path()
}
///|
pub fn ConfiguredLogger::file_auto_flush(self : ConfiguredLogger) -> Bool {
self.sink.file_auto_flush()
}
///|
pub fn ConfiguredLogger::file_rotation_enabled(self : ConfiguredLogger) -> Bool {
self.sink.file_rotation_enabled()
}
pub fn ConfiguredLogger::file_rotation_config(self : ConfiguredLogger) -> FileRotation? {
///|
pub fn ConfiguredLogger::file_rotation_config(
self : ConfiguredLogger,
) -> FileRotation? {
self.sink.file_rotation_config()
}
pub fn ConfiguredLogger::file_set_auto_flush(self : ConfiguredLogger, enabled : Bool) -> Bool {
///|
pub fn ConfiguredLogger::file_set_auto_flush(
self : ConfiguredLogger,
enabled : Bool,
) -> Bool {
self.sink.file_set_auto_flush(enabled)
}
pub fn ConfiguredLogger::file_set_policy(self : ConfiguredLogger, policy : FileSinkPolicy) -> Bool {
///|
pub fn ConfiguredLogger::file_set_policy(
self : ConfiguredLogger,
policy : FileSinkPolicy,
) -> Bool {
self.sink.file_set_policy(policy)
}
///|
pub fn ConfiguredLogger::file_set_rotation(
self : ConfiguredLogger,
rotation : FileRotation?,
@@ -346,58 +421,80 @@ pub fn ConfiguredLogger::file_set_rotation(
self.sink.file_set_rotation(rotation)
}
///|
pub fn ConfiguredLogger::file_clear_rotation(self : ConfiguredLogger) -> Bool {
self.sink.file_clear_rotation()
}
///|
pub fn ConfiguredLogger::file_flush(self : ConfiguredLogger) -> Bool {
self.sink.file_flush()
}
///|
pub fn ConfiguredLogger::file_close(self : ConfiguredLogger) -> Bool {
self.sink.file_close()
}
///|
pub fn ConfiguredLogger::file_open_failures(self : ConfiguredLogger) -> Int {
self.sink.file_open_failures()
}
///|
pub fn ConfiguredLogger::file_write_failures(self : ConfiguredLogger) -> Int {
self.sink.file_write_failures()
}
///|
pub fn ConfiguredLogger::file_flush_failures(self : ConfiguredLogger) -> Int {
self.sink.file_flush_failures()
}
///|
pub fn ConfiguredLogger::file_rotation_failures(self : ConfiguredLogger) -> Int {
self.sink.file_rotation_failures()
}
pub fn ConfiguredLogger::file_reset_failure_counters(self : ConfiguredLogger) -> Bool {
///|
pub fn ConfiguredLogger::file_reset_failure_counters(
self : ConfiguredLogger,
) -> Bool {
self.sink.file_reset_failure_counters()
}
///|
pub fn ConfiguredLogger::file_reset_policy(self : ConfiguredLogger) -> Bool {
self.sink.file_reset_policy()
}
///|
pub fn ConfiguredLogger::file_policy(self : ConfiguredLogger) -> FileSinkPolicy {
self.sink.file_policy()
}
pub fn ConfiguredLogger::file_default_policy(self : ConfiguredLogger) -> FileSinkPolicy {
///|
pub fn ConfiguredLogger::file_default_policy(
self : ConfiguredLogger,
) -> FileSinkPolicy {
self.sink.file_default_policy()
}
pub fn ConfiguredLogger::file_policy_matches_default(self : ConfiguredLogger) -> Bool {
///|
pub fn ConfiguredLogger::file_policy_matches_default(
self : ConfiguredLogger,
) -> Bool {
self.sink.file_policy_matches_default()
}
///|
pub fn ConfiguredLogger::file_state(self : ConfiguredLogger) -> FileSinkState {
self.sink.file_state()
}
pub fn ConfiguredLogger::file_runtime_state(self : ConfiguredLogger) -> RuntimeFileState? {
///|
pub fn ConfiguredLogger::file_runtime_state(
self : ConfiguredLogger,
) -> RuntimeFileState? {
self.sink.file_runtime_state()
}
+110 -45
View File
@@ -1,3 +1,4 @@
///|
pub(all) enum RuntimeSink {
Console(ConsoleSink)
JsonConsole(JsonConsoleSink)
@@ -9,33 +10,54 @@ pub(all) enum RuntimeSink {
QueuedFile(QueuedSink[FileSink])
}
///|
pub type RuntimeFileState = @utils.RuntimeFileState
pub fn file_sink_policy_to_json(policy : FileSinkPolicy) -> @json_parser.JsonValue {
///|
pub fn file_sink_policy_to_json(
policy : FileSinkPolicy,
) -> @json_parser.JsonValue {
@utils.file_sink_policy_to_json(policy)
}
pub fn stringify_file_sink_policy(policy : FileSinkPolicy, pretty~ : Bool = false) -> String {
@utils.stringify_file_sink_policy(policy, pretty=pretty)
///|
pub fn stringify_file_sink_policy(
policy : FileSinkPolicy,
pretty? : Bool = false,
) -> String {
@utils.stringify_file_sink_policy(policy, pretty~)
}
///|
pub fn file_sink_state_to_json(state : FileSinkState) -> @json_parser.JsonValue {
@utils.file_sink_state_to_json(state)
}
pub fn stringify_file_sink_state(state : FileSinkState, pretty~ : Bool = false) -> String {
@utils.stringify_file_sink_state(state, pretty=pretty)
///|
pub fn stringify_file_sink_state(
state : FileSinkState,
pretty? : Bool = false,
) -> String {
@utils.stringify_file_sink_state(state, pretty~)
}
pub fn runtime_file_state_to_json(state : RuntimeFileState) -> @json_parser.JsonValue {
///|
pub fn runtime_file_state_to_json(
state : RuntimeFileState,
) -> @json_parser.JsonValue {
@utils.runtime_file_state_to_json(state)
}
pub fn stringify_runtime_file_state(state : RuntimeFileState, pretty~ : Bool = false) -> String {
@utils.stringify_runtime_file_state(state, pretty=pretty)
///|
pub fn stringify_runtime_file_state(
state : RuntimeFileState,
pretty? : Bool = false,
) -> String {
@utils.stringify_runtime_file_state(state, pretty~)
}
pub impl Sink for RuntimeSink with write(self, rec) {
///|
pub impl Sink for RuntimeSink with fn write(self, rec) {
match self {
Console(sink) => sink.write(rec)
JsonConsole(sink) => sink.write(rec)
@@ -48,6 +70,7 @@ pub impl Sink for RuntimeSink with write(self, rec) {
}
}
///|
pub fn RuntimeSink::flush(self : RuntimeSink) -> Int {
match self {
Console(_) => 0
@@ -61,19 +84,21 @@ pub fn RuntimeSink::flush(self : RuntimeSink) -> Int {
}
}
pub fn RuntimeSink::drain(self : RuntimeSink, max_items~ : Int = -1) -> Int {
///|
pub fn RuntimeSink::drain(self : RuntimeSink, max_items? : Int = -1) -> Int {
match self {
Console(_) => 0
JsonConsole(_) => 0
TextConsole(_) => 0
File(sink) => if sink.flush() { 1 } else { 0 }
QueuedConsole(sink) => sink.drain(max_items=max_items)
QueuedJsonConsole(sink) => sink.drain(max_items=max_items)
QueuedTextConsole(sink) => sink.drain(max_items=max_items)
QueuedFile(sink) => sink.drain(max_items=max_items)
QueuedConsole(sink) => sink.drain(max_items~)
QueuedJsonConsole(sink) => sink.drain(max_items~)
QueuedTextConsole(sink) => sink.drain(max_items~)
QueuedFile(sink) => sink.drain(max_items~)
}
}
///|
pub fn RuntimeSink::close(self : RuntimeSink) -> Bool {
match self {
Console(_) => true
@@ -87,6 +112,7 @@ pub fn RuntimeSink::close(self : RuntimeSink) -> Bool {
}
}
///|
pub fn RuntimeSink::pending_count(self : RuntimeSink) -> Int {
match self {
Console(_) => 0
@@ -100,6 +126,7 @@ pub fn RuntimeSink::pending_count(self : RuntimeSink) -> Int {
}
}
///|
pub fn RuntimeSink::dropped_count(self : RuntimeSink) -> Int {
match self {
Console(_) => 0
@@ -113,63 +140,96 @@ pub fn RuntimeSink::dropped_count(self : RuntimeSink) -> Int {
}
}
///|
pub type ConfiguredLogger = Logger[RuntimeSink]
///|
pub fn ConfiguredLogger::flush(self : ConfiguredLogger) -> Int {
self.sink.flush()
}
pub fn ConfiguredLogger::drain(self : ConfiguredLogger, max_items~ : Int = -1) -> Int {
self.sink.drain(max_items=max_items)
///|
pub fn ConfiguredLogger::drain(
self : ConfiguredLogger,
max_items? : Int = -1,
) -> Int {
self.sink.drain(max_items~)
}
///|
pub fn ConfiguredLogger::close(self : ConfiguredLogger) -> Bool {
self.sink.close()
}
///|
pub fn ConfiguredLogger::pending_count(self : ConfiguredLogger) -> Int {
self.sink.pending_count()
}
///|
pub fn ConfiguredLogger::dropped_count(self : ConfiguredLogger) -> Int {
self.sink.dropped_count()
}
///|
fn build_runtime_sink(config : SinkConfig) -> RuntimeSink {
match config.kind {
SinkKind::Console => RuntimeSink::Console(console_sink())
SinkKind::JsonConsole => RuntimeSink::JsonConsole(json_console_sink())
SinkKind::TextConsole => RuntimeSink::TextConsole(
text_console_sink(config.text_formatter.to_formatter()),
)
SinkKind::File => RuntimeSink::File(
file_sink(
config.path,
append=config.append,
auto_flush=config.auto_flush,
rotation=config.rotation,
formatter=fn(rec) {
format_text(rec, formatter=config.text_formatter.to_formatter())
},
),
)
SinkKind::TextConsole =>
RuntimeSink::TextConsole(
text_console_sink(config.text_formatter.to_formatter()),
)
SinkKind::File =>
RuntimeSink::File(
file_sink(
config.path,
append=config.append,
auto_flush=config.auto_flush,
rotation=config.rotation,
formatter=fn(rec) {
format_text(rec, formatter=config.text_formatter.to_formatter())
},
),
)
}
}
///|
fn apply_queue_config(sink : RuntimeSink, queue : QueueConfig) -> RuntimeSink {
match sink {
Console(inner) => RuntimeSink::QueuedConsole(
queued_sink(inner, max_pending=queue.max_pending, overflow=queue.overflow),
)
JsonConsole(inner) => RuntimeSink::QueuedJsonConsole(
queued_sink(inner, max_pending=queue.max_pending, overflow=queue.overflow),
)
TextConsole(inner) => RuntimeSink::QueuedTextConsole(
queued_sink(inner, max_pending=queue.max_pending, overflow=queue.overflow),
)
File(inner) => RuntimeSink::QueuedFile(
queued_sink(inner, max_pending=queue.max_pending, overflow=queue.overflow),
)
Console(inner) =>
RuntimeSink::QueuedConsole(
queued_sink(
inner,
max_pending=queue.max_pending,
overflow=queue.overflow,
),
)
JsonConsole(inner) =>
RuntimeSink::QueuedJsonConsole(
queued_sink(
inner,
max_pending=queue.max_pending,
overflow=queue.overflow,
),
)
TextConsole(inner) =>
RuntimeSink::QueuedTextConsole(
queued_sink(
inner,
max_pending=queue.max_pending,
overflow=queue.overflow,
),
)
File(inner) =>
RuntimeSink::QueuedFile(
queued_sink(
inner,
max_pending=queue.max_pending,
overflow=queue.overflow,
),
)
QueuedConsole(_) => sink
QueuedJsonConsole(_) => sink
QueuedTextConsole(_) => sink
@@ -177,16 +237,21 @@ fn apply_queue_config(sink : RuntimeSink, queue : QueueConfig) -> RuntimeSink {
}
}
///|
pub fn build_logger(config : LoggerConfig) -> ConfiguredLogger {
let sink = build_runtime_sink(config.sink)
let actual_sink = match config.queue {
None => sink
Some(queue) => apply_queue_config(sink, queue)
}
Logger::new(actual_sink, min_level=config.min_level, target=config.target)
.with_timestamp(enabled=config.timestamp)
Logger::new(actual_sink, min_level=config.min_level, target=config.target).with_timestamp(
enabled=config.timestamp,
)
}
pub fn parse_and_build_logger(input : String) -> ConfiguredLogger raise ConfigError {
///|
pub fn parse_and_build_logger(
input : String,
) -> ConfiguredLogger raise ConfigError {
build_logger(parse_logger_config_text(input))
}
+78 -29
View File
@@ -1,26 +1,32 @@
///|
pub trait Sink {
write(Self, Record) -> Unit
fn write(Self, Record) -> Unit
}
///|
pub struct ConsoleSink {
_dummy : Unit
}
///|
pub fn console_sink() -> ConsoleSink {
{ _dummy: () }
}
pub impl Sink for ConsoleSink with write(self, rec) {
///|
pub impl Sink for ConsoleSink with fn write(self, rec) {
ignore(self)
println(format_text(rec))
}
///|
pub struct ContextSink[S] {
sink : S
context_fields : Array[Field]
}
pub impl[S : Sink] Sink for ContextSink[S] with write(self, rec) {
///|
pub impl[S : Sink] Sink for ContextSink[S] with fn write(self, rec) {
let merged = if self.context_fields.length() == 0 {
rec.fields
} else if rec.fields.length() == 0 {
@@ -31,42 +37,51 @@ pub impl[S : Sink] Sink for ContextSink[S] with write(self, rec) {
self.sink.write(rec.with_fields(merged))
}
///|
pub struct JsonConsoleSink {
_dummy : Unit
}
///|
pub fn json_console_sink() -> JsonConsoleSink {
{ _dummy: () }
}
pub impl Sink for JsonConsoleSink with write(self, rec) {
///|
pub impl Sink for JsonConsoleSink with fn write(self, rec) {
ignore(self)
println(format_json(rec))
}
///|
pub struct FormattedConsoleSink {
formatter : RecordFormatter
}
pub fn formatted_console_sink(formatter : RecordFormatter) -> FormattedConsoleSink {
///|
pub fn formatted_console_sink(
formatter : RecordFormatter,
) -> FormattedConsoleSink {
{ formatter, }
}
///|
pub fn text_console_sink(formatter : TextFormatter) -> FormattedConsoleSink {
formatted_console_sink(fn(rec) {
format_text(rec, formatter=formatter)
})
formatted_console_sink(fn(rec) { format_text(rec, formatter~) })
}
pub impl Sink for FormattedConsoleSink with write(self, rec) {
///|
pub impl Sink for FormattedConsoleSink with fn write(self, rec) {
println((self.formatter)(rec))
}
///|
pub struct FormattedCallbackSink {
formatter : RecordFormatter
callback : (String) -> Unit
}
///|
pub fn formatted_callback_sink(
formatter : RecordFormatter,
callback : (String) -> Unit,
@@ -74,54 +89,63 @@ pub fn formatted_callback_sink(
{ formatter, callback }
}
///|
pub fn text_callback_sink(
formatter : TextFormatter,
callback : (String) -> Unit,
) -> FormattedCallbackSink {
formatted_callback_sink(fn(rec) {
format_text(rec, formatter=formatter)
}, callback)
formatted_callback_sink(fn(rec) { format_text(rec, formatter~) }, callback)
}
pub impl Sink for FormattedCallbackSink with write(self, rec) {
///|
pub impl Sink for FormattedCallbackSink with fn write(self, rec) {
(self.callback)((self.formatter)(rec))
}
///|
pub struct FanoutSink[A, B] {
left : A
right : B
}
///|
pub fn[A, B] fanout_sink(left : A, right : B) -> FanoutSink[A, B] {
{ left, right }
}
pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with write(self, rec) {
///|
pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with fn write(self, rec) {
self.left.write(rec)
self.right.write(rec.copy())
}
///|
pub struct SplitSink[A, B] {
left : A
right : B
predicate : (Record) -> Bool
}
pub fn[A, B] split_sink(left : A, right : B, predicate : (Record) -> Bool) -> SplitSink[A, B] {
///|
pub fn[A, B] split_sink(
left : A,
right : B,
predicate : (Record) -> Bool,
) -> SplitSink[A, B] {
{ left, right, predicate }
}
///|
pub fn[A, B] split_by_level(
left : A,
right : B,
min_level~ : Level = Level::Warn,
min_level? : Level = Level::Warn,
) -> SplitSink[A, B] {
split_sink(left, right, fn(rec) {
rec.level.enabled(min_level)
})
split_sink(left, right, fn(rec) { rec.level.enabled(min_level) })
}
pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with write(self, rec) {
///|
pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with fn write(self, rec) {
if (self.predicate)(rec) {
self.left.write(rec)
} else {
@@ -129,33 +153,40 @@ pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with write(self, rec) {
}
}
///|
pub struct CallbackSink {
callback : (Record) -> Unit
}
///|
pub fn callback_sink(callback : (Record) -> Unit) -> CallbackSink {
{ callback, }
}
pub impl Sink for CallbackSink with write(self, rec) {
///|
pub impl Sink for CallbackSink with fn write(self, rec) {
(self.callback)(rec)
}
///|
pub struct BufferedSink[S] {
sink : S
buffer : Ref[Array[Record]]
flush_limit : Int
}
pub fn[S] buffered_sink(sink : S, flush_limit~ : Int = 1) -> BufferedSink[S] {
///|
pub fn[S] buffered_sink(sink : S, flush_limit? : Int = 1) -> BufferedSink[S] {
let actual_limit = if flush_limit <= 0 { 1 } else { flush_limit }
{ sink, buffer: Ref([]), flush_limit: actual_limit }
}
///|
pub fn[S] BufferedSink::pending_count(self : BufferedSink[S]) -> Int {
self.buffer.val.length()
}
///|
pub fn[S : Sink] BufferedSink::flush(self : BufferedSink[S]) -> Unit {
if self.buffer.val.length() == 0 {
()
@@ -168,15 +199,18 @@ pub fn[S : Sink] BufferedSink::flush(self : BufferedSink[S]) -> Unit {
}
}
pub impl[S : Sink] Sink for BufferedSink[S] with write(self, rec) {
///|
pub impl[S : Sink] Sink for BufferedSink[S] with fn write(self, rec) {
self.buffer.val.push(rec)
if self.buffer.val.length() >= self.flush_limit {
self.flush()
}
}
///|
pub type QueueOverflowPolicy = @utils.QueueOverflowPolicy
///|
pub struct QueuedSink[S] {
sink : S
queue : @queue.Queue[Record]
@@ -185,10 +219,11 @@ pub struct QueuedSink[S] {
dropped_count : Ref[Int]
}
///|
pub fn[S] queued_sink(
sink : S,
max_pending~ : Int = 0,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
max_pending? : Int = 0,
overflow? : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> QueuedSink[S] {
{
sink,
@@ -199,15 +234,21 @@ pub fn[S] queued_sink(
}
}
///|
pub fn[S] QueuedSink::pending_count(self : QueuedSink[S]) -> Int {
self.queue.length()
}
///|
pub fn[S] QueuedSink::dropped_count(self : QueuedSink[S]) -> Int {
self.dropped_count.val
}
pub fn[S : Sink] QueuedSink::drain(self : QueuedSink[S], max_items~ : Int = -1) -> Int {
///|
pub fn[S : Sink] QueuedSink::drain(
self : QueuedSink[S],
max_items? : Int = -1,
) -> Int {
if max_items == 0 {
return 0
}
@@ -225,11 +266,13 @@ pub fn[S : Sink] QueuedSink::drain(self : QueuedSink[S], max_items~ : Int = -1)
}
}
///|
pub fn[S : Sink] QueuedSink::flush(self : QueuedSink[S]) -> Int {
self.drain()
}
pub impl[S] Sink for QueuedSink[S] with write(self, rec) {
///|
pub impl[S] Sink for QueuedSink[S] with fn write(self, rec) {
let full = self.max_pending > 0 && self.pending_count() >= self.max_pending
if !full {
self.queue.push(rec)
@@ -245,30 +288,36 @@ pub impl[S] Sink for QueuedSink[S] with write(self, rec) {
}
}
///|
pub struct FilterSink[S] {
sink : S
predicate : (Record) -> Bool
}
///|
pub fn[S] filter_sink(sink : S, predicate : (Record) -> Bool) -> FilterSink[S] {
{ sink, predicate }
}
pub impl[S : Sink] Sink for FilterSink[S] with write(self, rec) {
///|
pub impl[S : Sink] Sink for FilterSink[S] with fn write(self, rec) {
if (self.predicate)(rec) {
self.sink.write(rec)
}
}
///|
pub struct PatchSink[S] {
sink : S
patch : RecordPatch
}
///|
pub fn[S] patch_sink(sink : S, patch : RecordPatch) -> PatchSink[S] {
{ sink, patch }
}
pub impl[S : Sink] Sink for PatchSink[S] with write(self, rec) {
///|
pub impl[S : Sink] Sink for PatchSink[S] with fn write(self, rec) {
self.sink.write((self.patch)(rec))
}
+79 -33
View File
@@ -1,3 +1,4 @@
///|
pub struct FileSink {
path : String
append : Ref[Bool]
@@ -14,28 +15,32 @@ pub struct FileSink {
rotation_failures : Ref[Int]
}
///|
pub type FileRotation = @utils.FileRotation
///|
pub type FileSinkState = @utils.FileSinkState
///|
pub type FileSinkPolicy = @utils.FileSinkPolicy
pub fn file_rotation(max_bytes : Int, max_backups~ : Int = 1) -> FileRotation {
@utils.file_rotation(max_bytes, max_backups=max_backups)
///|
pub fn file_rotation(max_bytes : Int, max_backups? : Int = 1) -> FileRotation {
@utils.file_rotation(max_bytes, max_backups~)
}
///|
pub fn native_files_supported() -> Bool {
native_files_supported_internal()
}
///|
pub fn file_sink(
path : String,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
formatter~ : RecordFormatter = fn(rec) {
format_text(rec)
},
append? : Bool = true,
auto_flush? : Bool = true,
rotation? : FileRotation? = None,
formatter? : RecordFormatter = fn(rec) { format_text(rec) },
) -> FileSink {
let handle = open_file_handle_internal(path, append)
{
@@ -55,10 +60,12 @@ pub fn file_sink(
}
}
///|
pub fn FileSink::is_available(self : FileSink) -> Bool {
self.handle.val is Some(_)
}
///|
pub fn FileSink::flush(self : FileSink) -> Bool {
match self.handle.val {
None => false
@@ -72,48 +79,62 @@ pub fn FileSink::flush(self : FileSink) -> Bool {
}
}
///|
pub fn FileSink::append_mode(self : FileSink) -> Bool {
self.append.val
}
///|
pub fn FileSink::set_append_mode(self : FileSink, append : Bool) -> Unit {
self.append.val = append
}
///|
pub fn FileSink::path(self : FileSink) -> String {
self.path
}
///|
pub fn FileSink::auto_flush_enabled(self : FileSink) -> Bool {
self.auto_flush.val
}
///|
pub fn FileSink::rotation_enabled(self : FileSink) -> Bool {
self.rotation.val is Some(_)
}
///|
pub fn FileSink::rotation_config(self : FileSink) -> FileRotation? {
self.rotation.val
}
///|
pub fn FileSink::set_auto_flush(self : FileSink, enabled : Bool) -> Unit {
self.auto_flush.val = enabled
}
///|
pub fn FileSink::set_policy(self : FileSink, policy : FileSinkPolicy) -> Unit {
self.append.val = policy.append
self.auto_flush.val = policy.auto_flush
self.rotation.val = policy.rotation
}
pub fn FileSink::set_rotation(self : FileSink, rotation : FileRotation?) -> Unit {
///|
pub fn FileSink::set_rotation(
self : FileSink,
rotation : FileRotation?,
) -> Unit {
self.rotation.val = rotation
}
///|
pub fn FileSink::clear_rotation(self : FileSink) -> Unit {
self.rotation.val = None
}
///|
pub fn FileSink::close(self : FileSink) -> Bool {
match self.handle.val {
None => false
@@ -125,22 +146,27 @@ pub fn FileSink::close(self : FileSink) -> Bool {
}
}
///|
pub fn FileSink::rotation_failures(self : FileSink) -> Int {
self.rotation_failures.val
}
///|
pub fn FileSink::open_failures(self : FileSink) -> Int {
self.open_failures.val
}
///|
pub fn FileSink::write_failures(self : FileSink) -> Int {
self.write_failures.val
}
///|
pub fn FileSink::flush_failures(self : FileSink) -> Int {
self.flush_failures.val
}
///|
pub fn FileSink::reset_failure_counters(self : FileSink) -> Unit {
self.open_failures.val = 0
self.write_failures.val = 0
@@ -148,12 +174,14 @@ pub fn FileSink::reset_failure_counters(self : FileSink) -> Unit {
self.rotation_failures.val = 0
}
///|
pub fn FileSink::reset_policy(self : FileSink) -> Unit {
self.append.val = self.default_append
self.auto_flush.val = self.default_auto_flush
self.rotation.val = self.default_rotation
}
///|
pub fn FileSink::policy(self : FileSink) -> FileSinkPolicy {
FileSinkPolicy::new(
append=self.append.val,
@@ -162,6 +190,7 @@ pub fn FileSink::policy(self : FileSink) -> FileSinkPolicy {
)
}
///|
pub fn FileSink::default_policy(self : FileSink) -> FileSinkPolicy {
FileSinkPolicy::new(
append=self.default_append,
@@ -170,6 +199,7 @@ pub fn FileSink::default_policy(self : FileSink) -> FileSinkPolicy {
)
}
///|
pub fn FileSink::policy_matches_default(self : FileSink) -> Bool {
let current = self.policy()
let default = self.default_policy()
@@ -178,14 +208,20 @@ pub fn FileSink::policy_matches_default(self : FileSink) -> Bool {
policy_rotation_equals_internal(current.rotation, default.rotation)
}
fn policy_rotation_equals_internal(left : FileRotation?, right : FileRotation?) -> Bool {
///|
fn policy_rotation_equals_internal(
left : FileRotation?,
right : FileRotation?,
) -> Bool {
match (left, right) {
(None, None) => true
(Some(a), Some(b)) => a.max_bytes == b.max_bytes && a.max_backups == b.max_backups
(Some(a), Some(b)) =>
a.max_bytes == b.max_bytes && a.max_backups == b.max_backups
_ => false
}
}
///|
pub fn FileSink::state(self : FileSink) -> FileSinkState {
FileSinkState::new(
self.path,
@@ -200,7 +236,8 @@ pub fn FileSink::state(self : FileSink) -> FileSinkState {
)
}
pub fn FileSink::reopen(self : FileSink, append~ : Bool? = None) -> Bool {
///|
pub fn FileSink::reopen(self : FileSink, append? : Bool? = None) -> Bool {
let append_mode = append.unwrap_or(self.append.val)
self.append.val = append_mode
match self.handle.val {
@@ -220,22 +257,27 @@ pub fn FileSink::reopen(self : FileSink, append~ : Bool? = None) -> Bool {
}
}
///|
pub fn FileSink::reopen_with_current_policy(self : FileSink) -> Bool {
self.reopen()
}
///|
pub fn FileSink::reopen_append(self : FileSink) -> Bool {
self.reopen(append=Some(true))
}
///|
pub fn FileSink::reopen_truncate(self : FileSink) -> Bool {
self.reopen(append=Some(false))
}
///|
fn rotated_file_path(path : String, index : Int) -> String {
"\{path}.\{index}"
}
///|
fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
let closed = match sink.handle.val {
None => true
@@ -249,7 +291,9 @@ fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
return false
}
if rotation.max_backups > 0 {
ignore(remove_file_internal(rotated_file_path(sink.path, rotation.max_backups)))
ignore(
remove_file_internal(rotated_file_path(sink.path, rotation.max_backups)),
)
for index = rotation.max_backups - 1; index >= 1; {
let from_path = rotated_file_path(sink.path, index)
let to_path = rotated_file_path(sink.path, index + 1)
@@ -264,40 +308,42 @@ fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
sink.handle.val is Some(_)
}
///|
fn rotate_if_needed_internal(sink : FileSink, next_line_bytes : Int) -> Bool {
match sink.rotation.val {
None => true
Some(rotation) => match sink.handle.val {
None => false
Some(handle) => {
let size = file_size_internal(handle)
if size + next_line_bytes <= rotation.max_bytes {
true
} else {
let rotated = rotate_file_sink_internal(sink, rotation)
if !rotated {
sink.rotation_failures.val += 1
Some(rotation) =>
match sink.handle.val {
None => false
Some(handle) => {
let size = file_size_internal(handle)
if size + next_line_bytes <= rotation.max_bytes {
true
} else {
let rotated = rotate_file_sink_internal(sink, rotation)
if !rotated {
sink.rotation_failures.val += 1
}
rotated
}
rotated
}
}
}
}
}
pub impl Sink for FileSink with write(self, rec) {
///|
pub impl Sink for FileSink with fn write(self, rec) {
match self.handle.val {
None => {
self.write_failures.val += 1
}
None => self.write_failures.val += 1
Some(_) => {
let line = "\{(self.formatter)(rec)}\n"
let can_write = rotate_if_needed_internal(self, string_byte_length_internal(line))
let can_write = rotate_if_needed_internal(
self,
string_byte_length_internal(line),
)
if can_write {
match self.handle.val {
None => {
self.write_failures.val += 1
}
None => self.write_failures.val += 1
Some(active) => {
let wrote = write_file_handle_internal(active, line)
if wrote {
+195 -95
View File
@@ -1,7 +1,9 @@
///|
pub(all) suberror ConfigError {
InvalidConfig(String)
}
///|
pub(all) enum SinkKind {
Console
JsonConsole
@@ -9,6 +11,7 @@ pub(all) enum SinkKind {
File
}
///|
pub struct TextFormatterConfig {
show_timestamp : Bool
show_level : Bool
@@ -25,20 +28,21 @@ pub struct TextFormatterConfig {
style_tags : Map[String, TextStyle]
}
///|
pub fn TextFormatterConfig::new(
show_timestamp~ : Bool = true,
show_level~ : Bool = true,
show_target~ : Bool = true,
show_fields~ : Bool = true,
separator~ : String = " ",
field_separator~ : String = " ",
template~ : String = "",
color_mode~ : ColorMode = ColorMode::Never,
color_support~ : ColorSupport = ColorSupport::TrueColor,
style_markup~ : StyleMarkupMode = StyleMarkupMode::Full,
target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
style_tags~ : Map[String, TextStyle] = {},
show_timestamp? : Bool = true,
show_level? : Bool = true,
show_target? : Bool = true,
show_fields? : Bool = true,
separator? : String = " ",
field_separator? : String = " ",
template? : String = "",
color_mode? : ColorMode = ColorMode::Never,
color_support? : ColorSupport = ColorSupport::TrueColor,
style_markup? : StyleMarkupMode = StyleMarkupMode::Full,
target_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled,
fields_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled,
style_tags? : Map[String, TextStyle] = {},
) -> TextFormatterConfig {
{
show_timestamp,
@@ -57,15 +61,21 @@ pub fn TextFormatterConfig::new(
}
}
fn style_tag_registry_from_config(style_tags : Map[String, TextStyle]) -> StyleTagRegistry {
///|
fn style_tag_registry_from_config(
style_tags : Map[String, TextStyle],
) -> StyleTagRegistry {
let registry = style_tag_registry()
for name, style in style_tags {
ignore(registry.set_tag(name, style=style))
ignore(registry.set_tag(name, style~))
}
registry
}
pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextFormatter {
///|
pub fn TextFormatterConfig::to_formatter(
self : TextFormatterConfig,
) -> TextFormatter {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
@@ -87,18 +97,21 @@ pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextForm
)
}
///|
pub struct QueueConfig {
max_pending : Int
overflow : QueueOverflowPolicy
}
///|
pub fn QueueConfig::new(
max_pending : Int,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
overflow? : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> QueueConfig {
{ max_pending, overflow }
}
///|
pub struct SinkConfig {
kind : SinkKind
path : String
@@ -108,24 +121,19 @@ pub struct SinkConfig {
text_formatter : TextFormatterConfig
}
///|
pub fn SinkConfig::new(
kind~ : SinkKind = SinkKind::Console,
path~ : String = "",
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
text_formatter~ : TextFormatterConfig = default_text_formatter_config(),
kind? : SinkKind = SinkKind::Console,
path? : String = "",
append? : Bool = true,
auto_flush? : Bool = true,
rotation? : FileRotation? = None,
text_formatter? : TextFormatterConfig = default_text_formatter_config(),
) -> SinkConfig {
{
kind,
path,
append,
auto_flush,
rotation,
text_formatter,
}
{ kind, path, append, auto_flush, rotation, text_formatter }
}
///|
pub struct LoggerConfig {
min_level : @core.Level
target : String
@@ -134,34 +142,33 @@ pub struct LoggerConfig {
queue : QueueConfig?
}
///|
pub fn LoggerConfig::new(
min_level~ : @core.Level = @core.Level::Info,
target~ : String = "",
timestamp~ : Bool = false,
sink~ : SinkConfig = default_sink_config(),
queue~ : QueueConfig? = None,
min_level? : @core.Level = @core.Level::Info,
target? : String = "",
timestamp? : Bool = false,
sink? : SinkConfig = default_sink_config(),
queue? : QueueConfig? = None,
) -> LoggerConfig {
{
min_level,
target,
timestamp,
sink,
queue,
}
{ min_level, target, timestamp, sink, queue }
}
///|
pub fn default_text_formatter_config() -> TextFormatterConfig {
TextFormatterConfig::new()
}
///|
pub fn default_sink_config() -> SinkConfig {
SinkConfig::new()
}
///|
pub fn default_logger_config() -> LoggerConfig {
LoggerConfig::new()
}
///|
fn expect_object(
value : @json_parser.JsonValue,
context : String,
@@ -172,33 +179,40 @@ fn expect_object(
}
}
///|
fn get_string(
obj : Map[String, @json_parser.JsonValue],
key : String,
default~ : String = "",
default? : String = "",
) -> String raise ConfigError {
match obj.get(key) {
None => default
Some(value) => match value.as_string() {
Some(text) => text
None => raise ConfigError::InvalidConfig("Expected string at key " + key)
}
Some(value) =>
match value.as_string() {
Some(text) => text
None =>
raise ConfigError::InvalidConfig("Expected string at key " + key)
}
}
}
///|
fn get_optional_string(
obj : Map[String, @json_parser.JsonValue],
key : String,
) -> String? raise ConfigError {
match obj.get(key) {
None => None
Some(value) => match value.as_string() {
Some(text) => Some(text)
None => raise ConfigError::InvalidConfig("Expected string at key " + key)
}
Some(value) =>
match value.as_string() {
Some(text) => Some(text)
None =>
raise ConfigError::InvalidConfig("Expected string at key " + key)
}
}
}
///|
fn get_bool(
obj : Map[String, @json_parser.JsonValue],
key : String,
@@ -206,13 +220,15 @@ fn get_bool(
) -> Bool raise ConfigError {
match obj.get(key) {
None => default
Some(value) => match value.as_bool() {
Some(flag) => flag
None => raise ConfigError::InvalidConfig("Expected bool at key " + key)
}
Some(value) =>
match value.as_bool() {
Some(flag) => flag
None => raise ConfigError::InvalidConfig("Expected bool at key " + key)
}
}
}
///|
fn get_int(
obj : Map[String, @json_parser.JsonValue],
key : String,
@@ -220,13 +236,16 @@ fn get_int(
) -> Int raise ConfigError {
match obj.get(key) {
None => default
Some(value) => match value.as_number() {
Some(number) => number.to_int()
None => raise ConfigError::InvalidConfig("Expected number at key " + key)
}
Some(value) =>
match value.as_number() {
Some(number) => number.to_int()
None =>
raise ConfigError::InvalidConfig("Expected number at key " + key)
}
}
}
///|
fn parse_level(name : String) -> @core.Level raise ConfigError {
match name.to_upper() {
"TRACE" => @core.Level::Trace
@@ -238,15 +257,20 @@ fn parse_level(name : String) -> @core.Level raise ConfigError {
}
}
///|
fn parse_overflow(name : String) -> QueueOverflowPolicy raise ConfigError {
match name.to_upper() {
"DROPNEWEST" => QueueOverflowPolicy::DropNewest
"DROPPOLDEST" => QueueOverflowPolicy::DropOldest
"DROPOLDEST" => QueueOverflowPolicy::DropOldest
_ => raise ConfigError::InvalidConfig("Unsupported queue overflow policy: " + name)
_ =>
raise ConfigError::InvalidConfig(
"Unsupported queue overflow policy: " + name,
)
}
}
///|
fn parse_sink_kind(name : String) -> SinkKind raise ConfigError {
match name.to_upper() {
"CONSOLE" => SinkKind::Console
@@ -259,6 +283,7 @@ fn parse_sink_kind(name : String) -> SinkKind raise ConfigError {
}
}
///|
fn parse_color_mode(name : String) -> ColorMode raise ConfigError {
match name.to_upper() {
"NEVER" => ColorMode::Never
@@ -268,6 +293,7 @@ fn parse_color_mode(name : String) -> ColorMode raise ConfigError {
}
}
///|
fn parse_color_support(name : String) -> ColorSupport raise ConfigError {
match name.to_upper() {
"BASIC" => ColorSupport::Basic
@@ -276,15 +302,18 @@ fn parse_color_support(name : String) -> ColorSupport raise ConfigError {
}
}
///|
fn parse_style_markup_mode(name : String) -> StyleMarkupMode raise ConfigError {
match name.to_upper() {
"DISABLED" => StyleMarkupMode::Disabled
"BUILTIN" => StyleMarkupMode::Builtin
"FULL" => StyleMarkupMode::Full
_ => raise ConfigError::InvalidConfig("Unsupported style markup mode: " + name)
_ =>
raise ConfigError::InvalidConfig("Unsupported style markup mode: " + name)
}
}
///|
fn sink_kind_label(kind : SinkKind) -> String {
match kind {
SinkKind::Console => "console"
@@ -294,7 +323,10 @@ fn sink_kind_label(kind : SinkKind) -> String {
}
}
fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterConfig raise ConfigError {
///|
fn parse_text_formatter_config(
value : @json_parser.JsonValue,
) -> TextFormatterConfig raise ConfigError {
let obj = expect_object(value, "text_formatter")
TextFormatterConfig::new(
show_timestamp=get_bool(obj, "show_timestamp", default=true),
@@ -305,10 +337,18 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC
field_separator=get_string(obj, "field_separator", default=" "),
template=get_string(obj, "template", default=""),
color_mode=parse_color_mode(get_string(obj, "color_mode", default="never")),
color_support=parse_color_support(get_string(obj, "color_support", default="truecolor")),
style_markup=parse_style_markup_mode(get_string(obj, "style_markup", default="full")),
target_style_markup=parse_style_markup_mode(get_string(obj, "target_style_markup", default="disabled")),
fields_style_markup=parse_style_markup_mode(get_string(obj, "fields_style_markup", default="disabled")),
color_support=parse_color_support(
get_string(obj, "color_support", default="truecolor"),
),
style_markup=parse_style_markup_mode(
get_string(obj, "style_markup", default="full"),
),
target_style_markup=parse_style_markup_mode(
get_string(obj, "target_style_markup", default="disabled"),
),
fields_style_markup=parse_style_markup_mode(
get_string(obj, "fields_style_markup", default="disabled"),
),
style_tags=match obj.get("style_tags") {
None => {}
Some(inner) => parse_style_tags_config(inner)
@@ -316,6 +356,7 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC
)
}
///|
fn parse_text_style_config(
value : @json_parser.JsonValue,
context : String,
@@ -331,16 +372,25 @@ fn parse_text_style_config(
)
}
fn parse_style_tags_config(value : @json_parser.JsonValue) -> Map[String, TextStyle] raise ConfigError {
///|
fn parse_style_tags_config(
value : @json_parser.JsonValue,
) -> Map[String, TextStyle] raise ConfigError {
let obj = expect_object(value, "text_formatter.style_tags")
let style_tags : Map[String, TextStyle] = {}
for name, item in obj {
style_tags[name] = parse_text_style_config(item, "text_formatter.style_tags." + name)
style_tags[name] = parse_text_style_config(
item,
"text_formatter.style_tags." + name,
)
}
style_tags
}
fn parse_queue_config(value : @json_parser.JsonValue) -> QueueConfig raise ConfigError {
///|
fn parse_queue_config(
value : @json_parser.JsonValue,
) -> QueueConfig raise ConfigError {
let obj = expect_object(value, "queue")
QueueConfig::new(
get_int(obj, "max_pending", default=0),
@@ -348,7 +398,10 @@ fn parse_queue_config(value : @json_parser.JsonValue) -> QueueConfig raise Confi
)
}
fn parse_file_rotation_config(value : @json_parser.JsonValue) -> FileRotation raise ConfigError {
///|
fn parse_file_rotation_config(
value : @json_parser.JsonValue,
) -> FileRotation raise ConfigError {
let obj = expect_object(value, "sink.rotation")
file_rotation(
get_int(obj, "max_bytes", default=1),
@@ -356,7 +409,10 @@ fn parse_file_rotation_config(value : @json_parser.JsonValue) -> FileRotation ra
)
}
fn parse_sink_config(value : @json_parser.JsonValue) -> SinkConfig raise ConfigError {
///|
fn parse_sink_config(
value : @json_parser.JsonValue,
) -> SinkConfig raise ConfigError {
let obj = expect_object(value, "sink")
let kind = parse_sink_kind(get_string(obj, "kind", default="console"))
let formatter = match obj.get("text_formatter") {
@@ -365,14 +421,15 @@ fn parse_sink_config(value : @json_parser.JsonValue) -> SinkConfig raise ConfigE
}
let path = get_string(obj, "path", default="")
match kind {
SinkKind::File => if path == "" {
raise ConfigError::InvalidConfig("File sink requires non-empty path")
}
SinkKind::File =>
if path == "" {
raise ConfigError::InvalidConfig("File sink requires non-empty path")
}
_ => ()
}
SinkConfig::new(
kind=kind,
path=path,
kind~,
path~,
append=get_bool(obj, "append", default=true),
auto_flush=get_bool(obj, "auto_flush", default=true),
rotation=match obj.get("rotation") {
@@ -383,7 +440,10 @@ fn parse_sink_config(value : @json_parser.JsonValue) -> SinkConfig raise ConfigE
)
}
pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigError {
///|
pub fn parse_logger_config_text(
input : String,
) -> LoggerConfig raise ConfigError {
let root = @json_parser.parse(input) catch {
e => raise ConfigError::InvalidConfig("Invalid JSON: " + e.to_string())
}
@@ -403,17 +463,24 @@ pub fn parse_logger_config_text(input : String) -> LoggerConfig raise ConfigErro
)
}
///|
pub fn queue_config_to_json(queue : QueueConfig) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"max_pending": @json_parser.JsonValue::Number(queue.max_pending.to_double()),
"overflow": @json_parser.JsonValue::String(match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
}),
"overflow": @json_parser.JsonValue::String(
match queue.overflow {
QueueOverflowPolicy::DropNewest => "DropNewest"
QueueOverflowPolicy::DropOldest => "DropOldest"
},
),
})
}
pub fn stringify_queue_config(queue : QueueConfig, pretty~ : Bool = false) -> String {
///|
pub fn stringify_queue_config(
queue : QueueConfig,
pretty? : Bool = false,
) -> String {
let value = queue_config_to_json(queue)
if pretty {
@json_parser.stringify_pretty(value, 2)
@@ -422,7 +489,10 @@ pub fn stringify_queue_config(queue : QueueConfig, pretty~ : Bool = false) -> St
}
}
pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_parser.JsonValue {
///|
pub fn text_formatter_config_to_json(
config : TextFormatterConfig,
) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"show_timestamp": @json_parser.JsonValue::Bool(config.show_timestamp),
"show_level": @json_parser.JsonValue::Bool(config.show_level),
@@ -431,11 +501,21 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars
"separator": @json_parser.JsonValue::String(config.separator),
"field_separator": @json_parser.JsonValue::String(config.field_separator),
"template": @json_parser.JsonValue::String(config.template),
"color_mode": @json_parser.JsonValue::String(color_mode_label(config.color_mode)),
"color_support": @json_parser.JsonValue::String(color_support_label(config.color_support)),
"style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.style_markup)),
"target_style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.target_style_markup)),
"fields_style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.fields_style_markup)),
"color_mode": @json_parser.JsonValue::String(
color_mode_label(config.color_mode),
),
"color_support": @json_parser.JsonValue::String(
color_support_label(config.color_support),
),
"style_markup": @json_parser.JsonValue::String(
style_markup_mode_label(config.style_markup),
),
"target_style_markup": @json_parser.JsonValue::String(
style_markup_mode_label(config.target_style_markup),
),
"fields_style_markup": @json_parser.JsonValue::String(
style_markup_mode_label(config.fields_style_markup),
),
}
if config.style_tags.length() != 0 {
obj["style_tags"] = style_tags_config_to_json(config.style_tags)
@@ -443,6 +523,7 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars
@json_parser.JsonValue::Object(obj)
}
///|
fn text_style_config_to_json(style : TextStyle) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"bold": @json_parser.JsonValue::Bool(style.bold),
@@ -461,7 +542,10 @@ fn text_style_config_to_json(style : TextStyle) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object(obj)
}
fn style_tags_config_to_json(style_tags : Map[String, TextStyle]) -> @json_parser.JsonValue {
///|
fn style_tags_config_to_json(
style_tags : Map[String, TextStyle],
) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {}
for name, style in style_tags {
obj[name] = text_style_config_to_json(style)
@@ -469,9 +553,10 @@ fn style_tags_config_to_json(style_tags : Map[String, TextStyle]) -> @json_parse
@json_parser.JsonValue::Object(obj)
}
///|
pub fn stringify_text_formatter_config(
config : TextFormatterConfig,
pretty~ : Bool = false,
pretty? : Bool = false,
) -> String {
let value = text_formatter_config_to_json(config)
if pretty {
@@ -481,13 +566,19 @@ pub fn stringify_text_formatter_config(
}
}
pub fn file_rotation_config_to_json(config : FileRotation) -> @json_parser.JsonValue {
///|
pub fn file_rotation_config_to_json(
config : FileRotation,
) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"max_bytes": @json_parser.JsonValue::Number(config.max_bytes.to_double()),
"max_backups": @json_parser.JsonValue::Number(config.max_backups.to_double()),
"max_backups": @json_parser.JsonValue::Number(
config.max_backups.to_double(),
),
})
}
///|
pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"kind": @json_parser.JsonValue::String(sink_kind_label(config.kind)),
@@ -503,7 +594,11 @@ pub fn sink_config_to_json(config : SinkConfig) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object(obj)
}
pub fn stringify_sink_config(config : SinkConfig, pretty~ : Bool = false) -> String {
///|
pub fn stringify_sink_config(
config : SinkConfig,
pretty? : Bool = false,
) -> String {
let value = sink_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
@@ -512,6 +607,7 @@ pub fn stringify_sink_config(config : SinkConfig, pretty~ : Bool = false) -> Str
}
}
///|
pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"min_level": @json_parser.JsonValue::String(config.min_level.label()),
@@ -526,7 +622,11 @@ pub fn logger_config_to_json(config : LoggerConfig) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object(obj)
}
pub fn stringify_logger_config(config : LoggerConfig, pretty~ : Bool = false) -> String {
///|
pub fn stringify_logger_config(
config : LoggerConfig,
pretty? : Bool = false,
) -> String {
let value = logger_config_to_json(config)
if pretty {
@json_parser.stringify_pretty(value, 2)
+26 -5
View File
@@ -1,3 +1,4 @@
///|
fn string_to_c_bytes(str : String) -> Bytes {
let res : Array[Byte] = []
let len = str.length()
@@ -31,14 +32,18 @@ fn string_to_c_bytes(str : String) -> Bytes {
Bytes::from_array(res)
}
///|
#external
type NativeFileHandle
///|
#borrow(path, mode)
extern "C" fn file_open_ffi(path : Bytes, mode : Bytes) -> NativeFileHandle = "bitlogger_file_open"
///|
extern "C" fn file_is_null_ffi(handle : NativeFileHandle) -> Bool = "bitlogger_pointer_is_null"
///|
#borrow(buffer)
extern "C" fn file_write_ffi(
buffer : Bytes,
@@ -47,32 +52,37 @@ extern "C" fn file_write_ffi(
handle : NativeFileHandle,
) -> Int = "bitlogger_file_write"
///|
extern "C" fn file_flush_ffi(handle : NativeFileHandle) -> Int = "bitlogger_file_flush"
///|
extern "C" fn file_close_ffi(handle : NativeFileHandle) -> Int = "bitlogger_file_close"
///|
extern "C" fn file_seek_ffi(
handle : NativeFileHandle,
offset : Int,
origin : Int,
) -> Int = "bitlogger_file_seek"
///|
extern "C" fn file_tell_ffi(handle : NativeFileHandle) -> Int = "bitlogger_file_tell"
///|
#borrow(from_path, to_path)
extern "C" fn file_rename_ffi(
from_path : Bytes,
to_path : Bytes,
) -> Int = "bitlogger_file_rename"
extern "C" fn file_rename_ffi(from_path : Bytes, to_path : Bytes) -> Int = "bitlogger_file_rename"
///|
#borrow(path)
extern "C" fn file_remove_ffi(path : Bytes) -> Int = "bitlogger_file_remove"
///|
pub struct FileHandle {
path : String
raw : NativeFileHandle
}
///|
pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
let mode = if append { "ab" } else { "wb" }
let raw = file_open_ffi(string_to_c_bytes(path), string_to_c_bytes(mode))
@@ -83,20 +93,27 @@ pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
}
}
pub fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
///|
pub fn write_file_handle_internal(
handle : FileHandle,
content : String,
) -> Bool {
let bytes = string_to_c_bytes(content)
let written = file_write_ffi(bytes, 1, bytes.length() - 1, handle.raw)
written == bytes.length() - 1
}
///|
pub fn flush_file_handle_internal(handle : FileHandle) -> Bool {
file_flush_ffi(handle.raw) == 0
}
///|
pub fn close_file_handle_internal(handle : FileHandle) -> Bool {
file_close_ffi(handle.raw) == 0
}
///|
pub fn file_size_internal(handle : FileHandle) -> Int {
ignore(file_seek_ffi(handle.raw, 0, 2))
let size = file_tell_ffi(handle.raw)
@@ -107,18 +124,22 @@ pub fn file_size_internal(handle : FileHandle) -> Int {
}
}
///|
pub fn rename_file_internal(from_path : String, to_path : String) -> Bool {
file_rename_ffi(string_to_c_bytes(from_path), string_to_c_bytes(to_path)) == 0
}
///|
pub fn remove_file_internal(path : String) -> Bool {
file_remove_ffi(string_to_c_bytes(path)) == 0
}
///|
pub fn string_byte_length_internal(content : String) -> Int {
string_to_c_bytes(content).length() - 1
}
///|
pub fn native_files_supported_internal() -> Bool {
true
}
+14 -1
View File
@@ -1,7 +1,9 @@
///|
pub struct FileHandle {
path : String
}
///|
pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
ignore(append)
ignore(path)
@@ -10,42 +12,53 @@ pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
None
}
pub fn write_file_handle_internal(handle : FileHandle, content : String) -> Bool {
///|
pub fn write_file_handle_internal(
handle : FileHandle,
content : String,
) -> Bool {
ignore(handle)
ignore(content)
false
}
///|
pub fn flush_file_handle_internal(handle : FileHandle) -> Bool {
ignore(handle)
false
}
///|
pub fn close_file_handle_internal(handle : FileHandle) -> Bool {
ignore(handle)
false
}
///|
pub fn file_size_internal(handle : FileHandle) -> Int {
ignore(handle)
0
}
///|
pub fn rename_file_internal(from_path : String, to_path : String) -> Bool {
ignore(from_path)
ignore(to_path)
false
}
///|
pub fn remove_file_internal(path : String) -> Bool {
ignore(path)
false
}
///|
pub fn string_byte_length_internal(content : String) -> Int {
content.length()
}
///|
pub fn native_files_supported_internal() -> Bool {
false
}
+16 -16
View File
@@ -1,29 +1,27 @@
///|
pub type RecordPredicate = (@core.Record) -> Bool
///|
pub fn level_at_least(min_level : @core.Level) -> RecordPredicate {
fn(rec) {
rec.level.priority() >= min_level.priority()
}
fn(rec) { rec.level.priority() >= min_level.priority() }
}
///|
pub fn target_is(target : String) -> RecordPredicate {
fn(rec) {
rec.target == target
}
fn(rec) { rec.target == target }
}
///|
pub fn target_has_prefix(prefix : String) -> RecordPredicate {
fn(rec) {
rec.target.has_prefix(prefix)
}
fn(rec) { rec.target.has_prefix(prefix) }
}
///|
pub fn message_contains(fragment : String) -> RecordPredicate {
fn(rec) {
rec.message.contains(fragment)
}
fn(rec) { rec.message.contains(fragment) }
}
///|
pub fn has_field(key : String) -> RecordPredicate {
fn(rec) {
for field in rec.fields {
@@ -35,6 +33,7 @@ pub fn has_field(key : String) -> RecordPredicate {
}
}
///|
pub fn field_equals(key : String, value : String) -> RecordPredicate {
fn(rec) {
for field in rec.fields {
@@ -46,16 +45,16 @@ pub fn field_equals(key : String, value : String) -> RecordPredicate {
}
}
///|
pub fn not_(predicate : RecordPredicate) -> RecordPredicate {
fn(rec) {
!(predicate(rec))
}
fn(rec) { !predicate(rec) }
}
///|
pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
fn(rec) {
for predicate in predicates {
if !(predicate(rec)) {
if !predicate(rec) {
return false
}
}
@@ -63,6 +62,7 @@ pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
}
}
///|
pub fn any_of(predicates : Array[RecordPredicate]) -> RecordPredicate {
fn(rec) {
for predicate in predicates {
+322 -150
View File
@@ -1,22 +1,27 @@
///|
pub type RecordFormatter = (@core.Record) -> String
///|
pub(all) enum ColorMode {
Never
Auto
Always
}
///|
pub(all) enum ColorSupport {
Basic
TrueColor
}
///|
pub(all) enum StyleMarkupMode {
Disabled
Builtin
Full
}
///|
pub struct TextStyle {
fg : String?
bg : String?
@@ -26,29 +31,34 @@ pub struct TextStyle {
underline : Bool
}
///|
pub fn text_style(
fg~ : String? = None,
bg~ : String? = None,
bold~ : Bool = false,
dim~ : Bool = false,
italic~ : Bool = false,
underline~ : Bool = false,
fg? : String? = None,
bg? : String? = None,
bold? : Bool = false,
dim? : Bool = false,
italic? : Bool = false,
underline? : Bool = false,
) -> TextStyle {
{ fg, bg, bold, dim, italic, underline }
}
///|
pub struct StyleTagRegistry {
entries : Map[String, TextStyle]
}
///|
fn normalize_style_tag_name(name : String) -> String {
name.trim().to_lower().to_owned()
}
///|
pub fn style_tag_registry() -> StyleTagRegistry {
{ entries: {} }
}
///|
fn merge_text_style(base : TextStyle, overlay : TextStyle) -> TextStyle {
{
fg: match overlay.fg {
@@ -66,91 +76,113 @@ fn merge_text_style(base : TextStyle, overlay : TextStyle) -> TextStyle {
}
}
///|
pub fn StyleTagRegistry::set_tag(
self : StyleTagRegistry,
name : String,
style~ : TextStyle = text_style(),
fg~ : String? = None,
bg~ : String? = None,
bold~ : Bool = false,
dim~ : Bool = false,
italic~ : Bool = false,
underline~ : Bool = false,
style? : TextStyle = text_style(),
fg? : String? = None,
bg? : String? = None,
bold? : Bool = false,
dim? : Bool = false,
italic? : Bool = false,
underline? : Bool = false,
) -> StyleTagRegistry {
let overlay = text_style(fg=fg, bg=bg, bold=bold, dim=dim, italic=italic, underline=underline)
self.entries[normalize_style_tag_name(name)] = merge_text_style(style, overlay)
let overlay = text_style(fg~, bg~, bold~, dim~, italic~, underline~)
self.entries[normalize_style_tag_name(name)] = merge_text_style(
style, overlay,
)
self
}
pub fn StyleTagRegistry::get(self : StyleTagRegistry, name : String) -> TextStyle? {
///|
pub fn StyleTagRegistry::get(
self : StyleTagRegistry,
name : String,
) -> TextStyle? {
self.entries.get(normalize_style_tag_name(name))
}
pub fn StyleTagRegistry::contains(self : StyleTagRegistry, name : String) -> Bool {
///|
pub fn StyleTagRegistry::contains(
self : StyleTagRegistry,
name : String,
) -> Bool {
self.entries.contains(normalize_style_tag_name(name))
}
///|
pub fn StyleTagRegistry::define_alias(
self : StyleTagRegistry,
name : String,
target : String,
) -> StyleTagRegistry {
match self.get(target) {
Some(style) => self.set_tag(name, style=style)
None => match global_style_tag_registry().get(target) {
Some(style) => self.set_tag(name, style=style)
None => match builtin_text_style_for_tag(normalize_style_tag_name(target)) {
Some(style) => self.set_tag(name, style=style)
None => self
Some(style) => self.set_tag(name, style~)
None =>
match global_style_tag_registry().get(target) {
Some(style) => self.set_tag(name, style~)
None =>
match builtin_text_style_for_tag(normalize_style_tag_name(target)) {
Some(style) => self.set_tag(name, style~)
None => self
}
}
}
}
}
///|
pub fn default_style_tag_registry() -> StyleTagRegistry {
style_tag_registry()
.set_tag("black", fg=Some("black"))
.set_tag("red", fg=Some("red"))
.set_tag("green", fg=Some("green"))
.set_tag("yellow", fg=Some("yellow"))
.set_tag("blue", fg=Some("blue"))
.set_tag("magenta", fg=Some("magenta"))
.set_tag("cyan", fg=Some("cyan"))
.set_tag("white", fg=Some("white"))
.set_tag("bright_black", fg=Some("bright_black"))
.set_tag("bright_red", fg=Some("bright_red"))
.set_tag("bright_green", fg=Some("bright_green"))
.set_tag("bright_yellow", fg=Some("bright_yellow"))
.set_tag("bright_blue", fg=Some("bright_blue"))
.set_tag("bright_magenta", fg=Some("bright_magenta"))
.set_tag("bright_cyan", fg=Some("bright_cyan"))
.set_tag("bright_white", fg=Some("bright_white"))
.set_tag("accent", fg=Some("bright_cyan"), bold=true)
.set_tag("info", fg=Some("cyan"))
.set_tag("success", fg=Some("green"), bold=true)
.set_tag("warning", fg=Some("yellow"), bold=true)
.set_tag("danger", fg=Some("red"), bold=true)
.set_tag("muted", fg=Some("bright_black"), dim=true)
.set_tag("b", bold=true)
.set_tag("dim", dim=true)
.set_tag("i", italic=true)
.set_tag("u", underline=true)
.set_tag("black", fg=Some("black"))
.set_tag("red", fg=Some("red"))
.set_tag("green", fg=Some("green"))
.set_tag("yellow", fg=Some("yellow"))
.set_tag("blue", fg=Some("blue"))
.set_tag("magenta", fg=Some("magenta"))
.set_tag("cyan", fg=Some("cyan"))
.set_tag("white", fg=Some("white"))
.set_tag("bright_black", fg=Some("bright_black"))
.set_tag("bright_red", fg=Some("bright_red"))
.set_tag("bright_green", fg=Some("bright_green"))
.set_tag("bright_yellow", fg=Some("bright_yellow"))
.set_tag("bright_blue", fg=Some("bright_blue"))
.set_tag("bright_magenta", fg=Some("bright_magenta"))
.set_tag("bright_cyan", fg=Some("bright_cyan"))
.set_tag("bright_white", fg=Some("bright_white"))
.set_tag("accent", fg=Some("bright_cyan"), bold=true)
.set_tag("info", fg=Some("cyan"))
.set_tag("success", fg=Some("green"), bold=true)
.set_tag("warning", fg=Some("yellow"), bold=true)
.set_tag("danger", fg=Some("red"), bold=true)
.set_tag("muted", fg=Some("bright_black"), dim=true)
.set_tag("b", bold=true)
.set_tag("dim", dim=true)
.set_tag("i", italic=true)
.set_tag("u", underline=true)
}
let global_style_tag_registry_ref : Ref[StyleTagRegistry] = Ref(style_tag_registry())
///|
let global_style_tag_registry_ref : Ref[StyleTagRegistry] = Ref(
style_tag_registry(),
)
///|
pub fn global_style_tag_registry() -> StyleTagRegistry {
global_style_tag_registry_ref.val
}
///|
pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit {
global_style_tag_registry_ref.val = registry
}
///|
pub fn reset_global_style_tag_registry() -> Unit {
global_style_tag_registry_ref.val = style_tag_registry()
}
///|
priv struct InlineStyle {
fg_code : String?
bg_code : String?
@@ -162,24 +194,29 @@ priv struct InlineStyle {
underline : Bool
}
///|
priv struct StyledSegment {
text : String
style : InlineStyle
}
///|
priv struct StyleFrame {
tag : String
style : InlineStyle
}
///|
fn code_unit(ch : Char) -> Int {
ch.to_int()
}
///|
fn code_unit16(ch : UInt16) -> Int {
ch.to_int()
}
///|
pub struct TextFormatter {
show_timestamp : Bool
show_level : Bool
@@ -196,20 +233,21 @@ pub struct TextFormatter {
style_tags : StyleTagRegistry?
}
///|
pub fn text_formatter(
show_timestamp~ : Bool = true,
show_level~ : Bool = true,
show_target~ : Bool = true,
show_fields~ : Bool = true,
separator~ : String = " ",
field_separator~ : String = " ",
template~ : String = "",
color_mode~ : ColorMode = ColorMode::Never,
color_support~ : ColorSupport = ColorSupport::TrueColor,
style_markup~ : StyleMarkupMode = StyleMarkupMode::Full,
target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled,
style_tags~ : StyleTagRegistry? = None,
show_timestamp? : Bool = true,
show_level? : Bool = true,
show_target? : Bool = true,
show_fields? : Bool = true,
separator? : String = " ",
field_separator? : String = " ",
template? : String = "",
color_mode? : ColorMode = ColorMode::Never,
color_support? : ColorSupport = ColorSupport::TrueColor,
style_markup? : StyleMarkupMode = StyleMarkupMode::Full,
target_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled,
fields_style_markup? : StyleMarkupMode = StyleMarkupMode::Disabled,
style_tags? : StyleTagRegistry? = None,
) -> TextFormatter {
{
show_timestamp,
@@ -228,6 +266,7 @@ pub fn text_formatter(
}
}
///|
pub fn color_support_label(support : ColorSupport) -> String {
match support {
ColorSupport::Basic => "basic"
@@ -235,7 +274,11 @@ pub fn color_support_label(support : ColorSupport) -> String {
}
}
pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : ColorSupport) -> TextFormatter {
///|
pub fn TextFormatter::with_color_support(
self : TextFormatter,
color_support : ColorSupport,
) -> TextFormatter {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
@@ -245,7 +288,7 @@ pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : C
field_separator=self.field_separator,
template=self.template,
color_mode=self.color_mode,
color_support=color_support,
color_support~,
style_markup=self.style_markup,
target_style_markup=self.target_style_markup,
fields_style_markup=self.fields_style_markup,
@@ -253,6 +296,7 @@ pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : C
)
}
///|
pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String {
match mode {
StyleMarkupMode::Disabled => "disabled"
@@ -261,7 +305,11 @@ pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String {
}
}
pub fn TextFormatter::with_style_markup(self : TextFormatter, style_markup : StyleMarkupMode) -> TextFormatter {
///|
pub fn TextFormatter::with_style_markup(
self : TextFormatter,
style_markup : StyleMarkupMode,
) -> TextFormatter {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
@@ -272,17 +320,21 @@ pub fn TextFormatter::with_style_markup(self : TextFormatter, style_markup : Sty
template=self.template,
color_mode=self.color_mode,
color_support=self.color_support,
style_markup=style_markup,
style_markup~,
target_style_markup=self.target_style_markup,
fields_style_markup=self.fields_style_markup,
style_tags=self.style_tags,
)
}
pub fn TextFormatter::without_style_markup(self : TextFormatter) -> TextFormatter {
///|
pub fn TextFormatter::without_style_markup(
self : TextFormatter,
) -> TextFormatter {
self.with_style_markup(StyleMarkupMode::Disabled)
}
///|
pub fn TextFormatter::with_target_style_markup(
self : TextFormatter,
style_markup : StyleMarkupMode,
@@ -304,6 +356,7 @@ pub fn TextFormatter::with_target_style_markup(
)
}
///|
pub fn TextFormatter::with_fields_style_markup(
self : TextFormatter,
style_markup : StyleMarkupMode,
@@ -325,7 +378,11 @@ pub fn TextFormatter::with_fields_style_markup(
)
}
pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTagRegistry) -> TextFormatter {
///|
pub fn TextFormatter::with_style_tags(
self : TextFormatter,
style_tags : StyleTagRegistry,
) -> TextFormatter {
text_formatter(
show_timestamp=self.show_timestamp,
show_level=self.show_level,
@@ -343,6 +400,7 @@ pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTa
)
}
///|
pub fn color_mode_label(mode : ColorMode) -> String {
match mode {
ColorMode::Never => "never"
@@ -351,17 +409,20 @@ pub fn color_mode_label(mode : ColorMode) -> String {
}
}
///|
fn use_ansi_color(mode : ColorMode) -> Bool {
match mode {
ColorMode::Never => false
ColorMode::Always => true
ColorMode::Auto => match @env.get_env_var("NO_COLOR") {
Some(_) => false
None => true
}
ColorMode::Auto =>
match @env.get_env_var("NO_COLOR") {
Some(_) => false
None => true
}
}
}
///|
fn ansi_wrap(text : String, code : String, enabled : Bool) -> String {
if !enabled || text == "" {
text
@@ -370,7 +431,12 @@ fn ansi_wrap(text : String, code : String, enabled : Bool) -> String {
}
}
fn ansi_wrap_with_style(text : String, style : InlineStyle, enabled : Bool) -> String {
///|
fn ansi_wrap_with_style(
text : String,
style : InlineStyle,
enabled : Bool,
) -> String {
if !enabled || text == "" {
text
} else {
@@ -404,6 +470,7 @@ fn ansi_wrap_with_style(text : String, style : InlineStyle, enabled : Bool) -> S
}
}
///|
fn default_inline_style() -> InlineStyle {
{
fg_code: None,
@@ -417,6 +484,7 @@ fn default_inline_style() -> InlineStyle {
}
}
///|
fn named_color_code(tag : String) -> String? {
match tag {
"black" => Some("30")
@@ -439,6 +507,7 @@ fn named_color_code(tag : String) -> String? {
}
}
///|
fn named_bg_color_code(tag : String) -> String? {
match tag {
"black" => Some("40")
@@ -461,6 +530,7 @@ fn named_bg_color_code(tag : String) -> String? {
}
}
///|
fn basic_name_from_hex(value : String) -> String {
let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2))
let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4))
@@ -495,18 +565,17 @@ fn basic_name_from_hex(value : String) -> String {
} else {
"green"
}
} else if r > 96 && g < 96 {
"magenta"
} else if g > 96 && r < 96 {
"cyan"
} else {
if r > 96 && g < 96 {
"magenta"
} else if g > 96 && r < 96 {
"cyan"
} else {
"blue"
}
"blue"
}
}
}
///|
fn resolve_inline_color_code(
formatter : TextFormatter,
basic_name : String?,
@@ -514,21 +583,25 @@ fn resolve_inline_color_code(
) -> String {
match formatter.color_support {
ColorSupport::TrueColor => truecolor_code
ColorSupport::Basic => match basic_name {
Some(name) => named_code_from_basic_name(name).unwrap_or(truecolor_code)
None => truecolor_code
}
ColorSupport::Basic =>
match basic_name {
Some(name) => named_code_from_basic_name(name).unwrap_or(truecolor_code)
None => truecolor_code
}
}
}
///|
fn named_code_from_basic_name(name : String) -> String? {
named_color_code(name)
}
///|
fn named_bg_code_from_basic_name(name : String) -> String? {
named_bg_color_code(name)
}
///|
fn resolve_inline_bg_code(
formatter : TextFormatter,
basic_name : String?,
@@ -536,13 +609,16 @@ fn resolve_inline_bg_code(
) -> String {
match formatter.color_support {
ColorSupport::TrueColor => truecolor_code
ColorSupport::Basic => match basic_name {
Some(name) => named_bg_code_from_basic_name(name).unwrap_or(truecolor_code)
None => truecolor_code
}
ColorSupport::Basic =>
match basic_name {
Some(name) =>
named_bg_code_from_basic_name(name).unwrap_or(truecolor_code)
None => truecolor_code
}
}
}
///|
fn is_hex_color(value : String) -> Bool {
if value.length() != 7 {
return false
@@ -562,6 +638,7 @@ fn is_hex_color(value : String) -> Bool {
true
}
///|
fn hex_to_int(ch : UInt16) -> Int {
let code = code_unit16(ch)
if code >= code_unit('0') && code <= code_unit('9') {
@@ -573,10 +650,12 @@ fn hex_to_int(ch : UInt16) -> Int {
}
}
///|
fn hex_pair_to_int(high : UInt16, low : UInt16) -> Int {
hex_to_int(high) * 16 + hex_to_int(low)
}
///|
fn rgb_fg_code(value : String) -> String {
let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2))
let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4))
@@ -584,6 +663,7 @@ fn rgb_fg_code(value : String) -> String {
"38;2;\{r};\{g};\{b}"
}
///|
fn rgb_bg_code(value : String) -> String {
let r = hex_pair_to_int(value.unsafe_get(4), value.unsafe_get(5))
let g = hex_pair_to_int(value.unsafe_get(6), value.unsafe_get(7))
@@ -591,6 +671,7 @@ fn rgb_bg_code(value : String) -> String {
"48;2;\{r};\{g};\{b}"
}
///|
fn rgb_bg_code_from_hex(value : String) -> String {
let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2))
let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4))
@@ -598,6 +679,7 @@ fn rgb_bg_code_from_hex(value : String) -> String {
"48;2;\{r};\{g};\{b}"
}
///|
fn inline_style_from_text_style(
base : InlineStyle,
style : TextStyle,
@@ -609,15 +691,24 @@ fn inline_style_from_text_style(
let normalized = normalize_style_tag_name(value)
match named_color_code(normalized) {
Some(code) => Some((Some(code), Some(normalized)))
None => if is_hex_color(value) {
let basic_name = basic_name_from_hex(value)
Some((
Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(value))),
Some(basic_name),
))
} else {
None
}
None =>
if is_hex_color(value) {
let basic_name = basic_name_from_hex(value)
Some(
(
Some(
resolve_inline_color_code(
formatter,
Some(basic_name),
rgb_fg_code(value),
),
),
Some(basic_name),
),
)
} else {
None
}
}
}
}
@@ -634,36 +725,48 @@ fn inline_style_from_text_style(
}
Some((Some(code), Some(bg_name)))
}
None => if is_hex_color(value) {
let basic_name = basic_name_from_hex(value)
Some((
Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code_from_hex(value))),
Some(basic_name),
))
} else {
None
}
None =>
if is_hex_color(value) {
let basic_name = basic_name_from_hex(value)
Some(
(
Some(
resolve_inline_bg_code(
formatter,
Some(basic_name),
rgb_bg_code_from_hex(value),
),
),
Some(basic_name),
),
)
} else {
None
}
}
}
}
match fg {
Some((next_fg_code, next_fg_name)) => match bg {
Some((next_bg_code, next_bg_name)) => Some({
fg_code: next_fg_code,
bg_code: next_bg_code,
fg_basic_name: next_fg_name,
bg_basic_name: next_bg_name,
bold: base.bold || style.bold,
dim: base.dim || style.dim,
italic: base.italic || style.italic,
underline: base.underline || style.underline,
})
None => None
}
Some((next_fg_code, next_fg_name)) =>
match bg {
Some((next_bg_code, next_bg_name)) =>
Some({
fg_code: next_fg_code,
bg_code: next_bg_code,
fg_basic_name: next_fg_name,
bg_basic_name: next_bg_name,
bold: base.bold || style.bold,
dim: base.dim || style.dim,
italic: base.italic || style.italic,
underline: base.underline || style.underline,
})
None => None
}
None => None
}
}
///|
fn builtin_text_style_for_tag(tag : String) -> TextStyle? {
match normalize_style_tag_name(tag) {
"black" => Some(text_style(fg=Some("black")))
@@ -696,41 +799,63 @@ fn builtin_text_style_for_tag(tag : String) -> TextStyle? {
}
}
fn resolve_registered_text_style(tag : String, formatter : TextFormatter) -> TextStyle? {
///|
fn resolve_registered_text_style(
tag : String,
formatter : TextFormatter,
) -> TextStyle? {
let normalized = normalize_style_tag_name(tag)
match formatter.style_markup {
StyleMarkupMode::Builtin => return builtin_text_style_for_tag(normalized)
_ => ()
}
match formatter.style_tags {
Some(registry) => match registry.get(normalized) {
Some(style) => Some(style)
None => match global_style_tag_registry().get(normalized) {
Some(registry) =>
match registry.get(normalized) {
Some(style) => Some(style)
None =>
match global_style_tag_registry().get(normalized) {
Some(style) => Some(style)
None => builtin_text_style_for_tag(normalized)
}
}
None =>
match global_style_tag_registry().get(normalized) {
Some(style) => Some(style)
None => builtin_text_style_for_tag(normalized)
}
}
None => match global_style_tag_registry().get(normalized) {
Some(style) => Some(style)
None => builtin_text_style_for_tag(normalized)
}
}
}
fn apply_inline_tag(style : InlineStyle, tag : String, formatter : TextFormatter) -> InlineStyle? {
///|
fn apply_inline_tag(
style : InlineStyle,
tag : String,
formatter : TextFormatter,
) -> InlineStyle? {
let normalized = normalize_style_tag_name(tag)
if is_hex_color(tag) {
let basic_name = basic_name_from_hex(tag)
Some({
..style,
fg_code: Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(tag))),
fg_code: Some(
resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(tag)),
),
fg_basic_name: Some(basic_name),
})
} else if normalized.length() == 10 && normalized.has_prefix("bg:") && is_hex_color(normalized[3:].to_owned()) {
} else if normalized.length() == 10 &&
normalized.has_prefix("bg:") &&
is_hex_color(normalized[3:].to_owned()) {
let basic_name = basic_name_from_hex(normalized[3:].to_owned())
Some({
..style,
bg_code: Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code(normalized))),
bg_code: Some(
resolve_inline_bg_code(
formatter,
Some(basic_name),
rgb_bg_code(normalized),
),
),
bg_basic_name: Some(basic_name),
})
} else {
@@ -741,6 +866,7 @@ fn apply_inline_tag(style : InlineStyle, tag : String, formatter : TextFormatter
}
}
///|
fn push_plain_segment(
segments : Array[StyledSegment],
buffer : StringBuilder,
@@ -753,6 +879,7 @@ fn push_plain_segment(
}
}
///|
fn pop_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool {
let normalized = normalize_style_tag_name(tag)
if stack.length() <= 1 {
@@ -769,6 +896,7 @@ fn pop_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool {
false
}
///|
fn has_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool {
let normalized = normalize_style_tag_name(tag)
if stack.length() <= 1 {
@@ -782,7 +910,11 @@ fn has_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool {
false
}
fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[StyledSegment] {
///|
fn parse_inline_markup(
input : String,
formatter : TextFormatter,
) -> Array[StyledSegment] {
let segments : Array[StyledSegment] = []
let buffer = StringBuilder::new()
let stack : Array[StyleFrame] = [{ tag: "", style: default_inline_style() }]
@@ -845,7 +977,12 @@ fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[Style
segments
}
fn render_styled_text(text : String, formatter : TextFormatter, mode : StyleMarkupMode) -> String {
///|
fn render_styled_text(
text : String,
formatter : TextFormatter,
mode : StyleMarkupMode,
) -> String {
match mode {
StyleMarkupMode::Disabled => return text
_ => ()
@@ -860,10 +997,12 @@ fn render_styled_text(text : String, formatter : TextFormatter, mode : StyleMark
out.to_string()
}
///|
fn render_inline_markup(message : String, formatter : TextFormatter) -> String {
render_styled_text(message, formatter, formatter.style_markup)
}
///|
fn level_ansi_code(level : @core.Level) -> String {
match level {
@core.Level::Trace => "90"
@@ -874,6 +1013,7 @@ fn level_ansi_code(level : @core.Level) -> String {
}
}
///|
fn fields_to_json(fields : Array[@core.Field]) -> Json {
let obj : Map[String, Json] = {}
for item in fields {
@@ -882,58 +1022,89 @@ fn fields_to_json(fields : Array[@core.Field]) -> Json {
Json::object(obj)
}
///|
fn timestamp_text(rec : @core.Record, formatter : TextFormatter) -> String {
if formatter.show_timestamp && rec.timestamp_ms != 0UL {
ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode))
ansi_wrap(
rec.timestamp_ms.to_string(),
"90",
use_ansi_color(formatter.color_mode),
)
} else {
""
}
}
///|
fn level_text(rec : @core.Record, formatter : TextFormatter) -> String {
if formatter.show_level {
ansi_wrap(rec.level.label(), level_ansi_code(rec.level), use_ansi_color(formatter.color_mode))
ansi_wrap(
rec.level.label(),
level_ansi_code(rec.level),
use_ansi_color(formatter.color_mode),
)
} else {
""
}
}
///|
fn target_text(rec : @core.Record, formatter : TextFormatter) -> String {
if formatter.show_target && rec.target != "" {
let rendered = render_styled_text(rec.target, formatter, formatter.target_style_markup)
let rendered = render_styled_text(
rec.target,
formatter,
formatter.target_style_markup,
)
ansi_wrap(rendered, "34", use_ansi_color(formatter.color_mode))
} else {
""
}
}
///|
fn format_field_text(field : @core.Field, formatter : TextFormatter) -> String {
let value = render_styled_text(field.value, formatter, formatter.fields_style_markup)
let value = render_styled_text(
field.value,
formatter,
formatter.fields_style_markup,
)
"\{field.key}=\{value}"
}
///|
fn fields_text(rec : @core.Record, formatter : TextFormatter) -> String {
if formatter.show_fields && rec.fields.length() != 0 {
let content = rec.fields.map(fn(field) { format_field_text(field, formatter) }).join(formatter.field_separator)
let content = rec.fields
.map(fn(field) { format_field_text(field, formatter) })
.join(formatter.field_separator)
ansi_wrap(content, "35", use_ansi_color(formatter.color_mode))
} else {
""
}
}
///|
fn render_template(rec : @core.Record, formatter : TextFormatter) -> String {
formatter.template
.replace_all(old="{timestamp}", new=timestamp_text(rec, formatter))
.replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter))
.replace_all(old="{level}", new=level_text(rec, formatter))
.replace_all(old="{target}", new=target_text(rec, formatter))
.replace_all(old="{message}", new=render_inline_markup(rec.message, formatter))
.replace_all(old="{fields}", new=fields_text(rec, formatter))
.trim()
.to_owned()
.replace_all(old="{timestamp}", new=timestamp_text(rec, formatter))
.replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter))
.replace_all(old="{level}", new=level_text(rec, formatter))
.replace_all(old="{target}", new=target_text(rec, formatter))
.replace_all(
old="{message}",
new=render_inline_markup(rec.message, formatter),
)
.replace_all(old="{fields}", new=fields_text(rec, formatter))
.trim()
.to_owned()
}
pub fn format_text(rec : @core.Record, formatter~ : TextFormatter = text_formatter()) -> String {
///|
pub fn format_text(
rec : @core.Record,
formatter? : TextFormatter = text_formatter(),
) -> String {
if formatter.template != "" {
return render_template(rec, formatter)
}
@@ -957,6 +1128,7 @@ pub fn format_text(rec : @core.Record, formatter~ : TextFormatter = text_formatt
}
}
///|
pub fn format_json(rec : @core.Record) -> String {
let obj : Map[String, Json] = {
"level": Json::string(rec.level.label()),
+3 -3
View File
@@ -1,7 +1,7 @@
import {
"Nanaloveyuki/BitLogger/src/core" @core,
"maria/json_parser" @json_parser,
"moonbitlang/core/env" @env,
"Nanaloveyuki/BitLogger/src/core",
"maria/json_parser",
"moonbitlang/core/env",
"moonbitlang/core/json",
"moonbitlang/core/ref",
}
+33 -22
View File
@@ -1,21 +1,22 @@
///|
pub type RecordPatch = (@core.Record) -> @core.Record
///|
pub fn identity_patch() -> RecordPatch {
fn(rec) { rec }
}
///|
pub fn set_target(target : String) -> RecordPatch {
fn(rec) {
rec.with_target(target)
}
fn(rec) { rec.with_target(target) }
}
///|
pub fn prefix_message(prefix : String) -> RecordPatch {
fn(rec) {
rec.with_message("\{prefix}\{rec.message}")
}
fn(rec) { rec.with_message("\{prefix}\{rec.message}") }
}
///|
pub fn append_fields(extra_fields : Array[@core.Field]) -> RecordPatch {
fn(rec) {
if extra_fields.length() == 0 {
@@ -28,30 +29,40 @@ pub fn append_fields(extra_fields : Array[@core.Field]) -> RecordPatch {
}
}
pub fn redact_field(key : String, placeholder~ : String = "***") -> RecordPatch {
///|
pub fn redact_field(key : String, placeholder? : String = "***") -> RecordPatch {
fn(rec) {
rec.with_fields(rec.fields.map(fn(field) {
if field.key == key {
field.with_value(placeholder)
} else {
field
}
}))
rec.with_fields(
rec.fields.map(fn(field) {
if field.key == key {
field.with_value(placeholder)
} else {
field
}
}),
)
}
}
pub fn redact_fields(keys : Array[String], placeholder~ : String = "***") -> RecordPatch {
///|
pub fn redact_fields(
keys : Array[String],
placeholder? : String = "***",
) -> RecordPatch {
fn(rec) {
rec.with_fields(rec.fields.map(fn(field) {
if keys.contains(field.key) {
field.with_value(placeholder)
} else {
field
}
}))
rec.with_fields(
rec.fields.map(fn(field) {
if keys.contains(field.key) {
field.with_value(placeholder)
} else {
field
}
}),
)
}
}
///|
pub fn compose_patches(patches : Array[RecordPatch]) -> RecordPatch {
fn(rec) {
let mut current = rec
+55 -16
View File
@@ -1,3 +1,4 @@
///|
pub struct RuntimeFileState {
file : FileSinkState
queued : Bool
@@ -5,16 +6,20 @@ pub struct RuntimeFileState {
dropped_count : Int
}
///|
pub fn RuntimeFileState::new(
file : FileSinkState,
queued~ : Bool = false,
pending_count~ : Int = 0,
dropped_count~ : Int = 0,
queued? : Bool = false,
pending_count? : Int = 0,
dropped_count? : Int = 0,
) -> RuntimeFileState {
{ file, queued, pending_count, dropped_count }
}
fn file_sink_policy_to_json_value(policy : FileSinkPolicy) -> @json_parser.JsonValue {
///|
fn file_sink_policy_to_json_value(
policy : FileSinkPolicy,
) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"append": @json_parser.JsonValue::Bool(policy.append),
"auto_flush": @json_parser.JsonValue::Bool(policy.auto_flush),
@@ -26,11 +31,18 @@ fn file_sink_policy_to_json_value(policy : FileSinkPolicy) -> @json_parser.JsonV
@json_parser.JsonValue::Object(obj)
}
pub fn file_sink_policy_to_json(policy : FileSinkPolicy) -> @json_parser.JsonValue {
///|
pub fn file_sink_policy_to_json(
policy : FileSinkPolicy,
) -> @json_parser.JsonValue {
file_sink_policy_to_json_value(policy)
}
pub fn stringify_file_sink_policy(policy : FileSinkPolicy, pretty~ : Bool = false) -> String {
///|
pub fn stringify_file_sink_policy(
policy : FileSinkPolicy,
pretty? : Bool = false,
) -> String {
let value = file_sink_policy_to_json_value(policy)
if pretty {
@json_parser.stringify_pretty(value, 2)
@@ -39,16 +51,27 @@ pub fn stringify_file_sink_policy(policy : FileSinkPolicy, pretty~ : Bool = fals
}
}
fn file_sink_state_to_json_value(state : FileSinkState) -> @json_parser.JsonValue {
///|
fn file_sink_state_to_json_value(
state : FileSinkState,
) -> @json_parser.JsonValue {
let obj : Map[String, @json_parser.JsonValue] = {
"path": @json_parser.JsonValue::String(state.path),
"available": @json_parser.JsonValue::Bool(state.available),
"append": @json_parser.JsonValue::Bool(state.append),
"auto_flush": @json_parser.JsonValue::Bool(state.auto_flush),
"open_failures": @json_parser.JsonValue::Number(state.open_failures.to_double()),
"write_failures": @json_parser.JsonValue::Number(state.write_failures.to_double()),
"flush_failures": @json_parser.JsonValue::Number(state.flush_failures.to_double()),
"rotation_failures": @json_parser.JsonValue::Number(state.rotation_failures.to_double()),
"open_failures": @json_parser.JsonValue::Number(
state.open_failures.to_double(),
),
"write_failures": @json_parser.JsonValue::Number(
state.write_failures.to_double(),
),
"flush_failures": @json_parser.JsonValue::Number(
state.flush_failures.to_double(),
),
"rotation_failures": @json_parser.JsonValue::Number(
state.rotation_failures.to_double(),
),
}
match state.rotation {
None => obj["rotation"] = @json_parser.JsonValue::Null
@@ -57,11 +80,16 @@ fn file_sink_state_to_json_value(state : FileSinkState) -> @json_parser.JsonValu
@json_parser.JsonValue::Object(obj)
}
///|
pub fn file_sink_state_to_json(state : FileSinkState) -> @json_parser.JsonValue {
file_sink_state_to_json_value(state)
}
pub fn stringify_file_sink_state(state : FileSinkState, pretty~ : Bool = false) -> String {
///|
pub fn stringify_file_sink_state(
state : FileSinkState,
pretty? : Bool = false,
) -> String {
let value = file_sink_state_to_json_value(state)
if pretty {
@json_parser.stringify_pretty(value, 2)
@@ -70,16 +98,27 @@ pub fn stringify_file_sink_state(state : FileSinkState, pretty~ : Bool = false)
}
}
pub fn runtime_file_state_to_json(state : RuntimeFileState) -> @json_parser.JsonValue {
///|
pub fn runtime_file_state_to_json(
state : RuntimeFileState,
) -> @json_parser.JsonValue {
@json_parser.JsonValue::Object({
"file": file_sink_state_to_json_value(state.file),
"queued": @json_parser.JsonValue::Bool(state.queued),
"pending_count": @json_parser.JsonValue::Number(state.pending_count.to_double()),
"dropped_count": @json_parser.JsonValue::Number(state.dropped_count.to_double()),
"pending_count": @json_parser.JsonValue::Number(
state.pending_count.to_double(),
),
"dropped_count": @json_parser.JsonValue::Number(
state.dropped_count.to_double(),
),
})
}
pub fn stringify_runtime_file_state(state : RuntimeFileState, pretty~ : Bool = false) -> String {
///|
pub fn stringify_runtime_file_state(
state : RuntimeFileState,
pretty? : Bool = false,
) -> String {
let value = runtime_file_state_to_json(state)
if pretty {
@json_parser.stringify_pretty(value, 2)
+29 -14
View File
@@ -1,8 +1,10 @@
///|
pub struct FileRotation {
max_bytes : Int
max_backups : Int
}
///|
pub struct FileSinkState {
path : String
available : Bool
@@ -15,30 +17,33 @@ pub struct FileSinkState {
rotation_failures : Int
}
///|
pub struct FileSinkPolicy {
append : Bool
auto_flush : Bool
rotation : FileRotation?
}
///|
pub fn FileSinkPolicy::new(
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
append? : Bool = true,
auto_flush? : Bool = true,
rotation? : FileRotation? = None,
) -> FileSinkPolicy {
{ append, auto_flush, rotation }
}
///|
pub fn FileSinkState::new(
path : String,
available~ : Bool = false,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
open_failures~ : Int = 0,
write_failures~ : Int = 0,
flush_failures~ : Int = 0,
rotation_failures~ : Int = 0,
available? : Bool = false,
append? : Bool = true,
auto_flush? : Bool = true,
rotation? : FileRotation? = None,
open_failures? : Int = 0,
write_failures? : Int = 0,
flush_failures? : Int = 0,
rotation_failures? : Int = 0,
) -> FileSinkState {
{
path,
@@ -53,13 +58,23 @@ pub fn FileSinkState::new(
}
}
pub fn file_rotation(max_bytes : Int, max_backups~ : Int = 1) -> FileRotation {
///|
pub fn file_rotation(max_bytes : Int, max_backups? : Int = 1) -> FileRotation {
{
max_bytes: if max_bytes <= 0 { 1 } else { max_bytes },
max_backups: if max_backups <= 0 { 1 } else { max_backups },
max_bytes: if max_bytes <= 0 {
1
} else {
max_bytes
},
max_backups: if max_backups <= 0 {
1
} else {
max_backups
},
}
}
///|
pub(all) enum QueueOverflowPolicy {
DropNewest
DropOldest
+1
View File
@@ -0,0 +1 @@
trueVersion = "1.0.0"