mirror of
https://github.com/Nanaloveyuki/BitLogger.git
synced 2026-09-07 18:03:47 +00:00
Compare commits
3
Commits
aab7a94ae2
...
cd358f8787
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd358f8787 | ||
|
|
3e1c93cc86 | ||
|
|
b4d69675fd |
@@ -0,0 +1,2 @@
|
||||
# Normalize repository text files and check them out with LF on every platform.
|
||||
* text=auto eol=lf
|
||||
@@ -17,7 +17,7 @@ BitLogger 的文档按使用路径组织:先完成一个可运行的日志输
|
||||
```bash
|
||||
moon new log-demo
|
||||
cd log-demo
|
||||
moon add Nanaloveyuki/BitLogger@0.8.0
|
||||
moon add Nanaloveyuki/BitLogger@0.8.1
|
||||
```
|
||||
|
||||
### 2. 写入第一条结构化日志
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ The documentation is organized around complete usage flows. Start with an execut
|
||||
```bash
|
||||
moon new log-demo
|
||||
cd log-demo
|
||||
moon add Nanaloveyuki/BitLogger@0.8.0
|
||||
moon add Nanaloveyuki/BitLogger@0.8.1
|
||||
```
|
||||
|
||||
### 2. Import And Log
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: file-sink-async
|
||||
group: api
|
||||
category: sink
|
||||
update-time: 20260825
|
||||
description: Create a file sink from an async context using the native async filesystem interface.
|
||||
key-word:
|
||||
- file
|
||||
- sink
|
||||
- async
|
||||
- public
|
||||
---
|
||||
|
||||
## File-sink-async
|
||||
|
||||
Create a `FileSink` from an async context. The constructor opens the file with the native `moonbitlang/async/fs` interface and keeps the same rotation, policy, and failure-counter surface as `file_sink(...)`.
|
||||
|
||||
### Interface
|
||||
|
||||
```moonbit
|
||||
pub async fn file_sink_async(
|
||||
path : String,
|
||||
append~ : Bool = true,
|
||||
auto_flush~ : Bool = true,
|
||||
rotation~ : FileRotation? = None,
|
||||
formatter~ : RecordFormatter = fn(rec) { format_text(rec) },
|
||||
) -> FileSink {}
|
||||
```
|
||||
|
||||
#### input
|
||||
|
||||
- `path : String` - Destination file path.
|
||||
- `append : Bool` - Whether writes append rather than truncate on the first open.
|
||||
- `auto_flush : Bool` - Whether each async write requests an async file sync.
|
||||
- `rotation : FileRotation?` - Optional size-based rotation policy.
|
||||
- `formatter : RecordFormatter` - Formatter used to render each record.
|
||||
|
||||
#### output
|
||||
|
||||
- `FileSink` - A file sink ready for use by an async logger or direct async sink writes.
|
||||
|
||||
### Explanation
|
||||
|
||||
- Use this constructor when the caller is already running inside an async event loop.
|
||||
- File writes, flushes, file-size checks, renames, removals, and rotation reopening use the async filesystem path.
|
||||
- The returned sink implements `Sink::write_async(...)`; the synchronous `write(...)` surface remains available for synchronous callers.
|
||||
- On non-native targets, the sink follows the existing unavailable-backend behavior.
|
||||
|
||||
### How to Use
|
||||
|
||||
```moonbit
|
||||
let sink = file_sink_async("app.log", auto_flush=false)
|
||||
let logger = async_logger(sink)
|
||||
```
|
||||
|
||||
### Error Case
|
||||
|
||||
- If the initial async open fails, the returned sink is unavailable and increments `open_failures()`.
|
||||
- Later async writes record write and rotation failures through the existing sink counters.
|
||||
|
||||
### Notes
|
||||
|
||||
1. Use `file_sink(...)` for synchronous construction outside an async event loop.
|
||||
|
||||
2. Pair this API with `native_files_supported()` when code must also compile for non-native targets.
|
||||
@@ -98,3 +98,5 @@ e.g.:
|
||||
3. Non-native targets can still compile code referencing this API, but callers should treat actual file availability and successful writes as target-sensitive runtime behavior.
|
||||
|
||||
4. See [target-verification.md](./target-verification.md) for the current verification boundary between design intent and locally re-checked targets.
|
||||
|
||||
5. Use [`file_sink_async(...)`](./file-sink-async.md) when constructing a file sink from an async event loop.
|
||||
|
||||
@@ -153,6 +153,7 @@ BitLogger API navigation.
|
||||
- [patch-sink.md](./patch-sink.md)
|
||||
- [patch-sink-type.md](./patch-sink-type.md)
|
||||
- [file-sink.md](./file-sink.md)
|
||||
- [file-sink-async.md](./file-sink-async.md)
|
||||
- [file-sink-available.md](./file-sink-available.md)
|
||||
- [file-sink-flush.md](./file-sink-flush.md)
|
||||
- [file-sink-close.md](./file-sink-close.md)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
## BitLogger Update Changes
|
||||
|
||||
version 1.2.1(0.8.1)
|
||||
|
||||
### Changes
|
||||
|
||||
- Add deterministic QuickCheck properties for bounded history retention, queued overflow and drain behavior, patch composition, and logger config round-trips.
|
||||
|
||||
### Verification
|
||||
|
||||
- moon fmt --check
|
||||
- moon check --deny-warn
|
||||
- moon test --deny-warn across native, wasm, wasm-gc, and js targets
|
||||
- pnpm run docs:build
|
||||
|
||||
### Notes
|
||||
|
||||
- QuickCheck seeds and shrink budgets are fixed for reproducible failures.
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Versioned BitLogger change summaries.
|
||||
|
||||
- [1.2.1](./1.2.1(0.8.1).md)
|
||||
- [1.2.0](./1.2.0(0.8.0).md)
|
||||
- [1.1.3](./1.1.3(0.7.3).md)
|
||||
- [1.1.2](./1.1.2(0.7.2).md)
|
||||
|
||||
@@ -5,7 +5,7 @@ Use this flow for command-line tools and services that need a human-readable sta
|
||||
## Install And Import
|
||||
|
||||
```bash
|
||||
moon add Nanaloveyuki/BitLogger@0.8.0
|
||||
moon add Nanaloveyuki/BitLogger@0.8.1
|
||||
```
|
||||
|
||||
```moonbit
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
```bash
|
||||
moon new log-demo
|
||||
cd log-demo
|
||||
moon add Nanaloveyuki/BitLogger@0.8.0
|
||||
moon add Nanaloveyuki/BitLogger@0.8.1
|
||||
```
|
||||
|
||||
在应用的 `moon.pkg` 中加入:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name = "Nanaloveyuki/BitLogger"
|
||||
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
|
||||
import {
|
||||
"moonbitlang/async@0.20.2",
|
||||
"moonbitlang/async@0.21.0",
|
||||
}
|
||||
|
||||
readme = "src/README.mbt.md"
|
||||
|
||||
@@ -132,6 +132,7 @@ pub struct AsyncLogger[S] {
|
||||
flush_policy : AsyncFlushPolicy
|
||||
sink : S
|
||||
flush_callback : (S) -> Unit raise
|
||||
flush_async_callback : (async (S) -> Unit)?
|
||||
context_fields : Array[@bitlogger.Field]
|
||||
filter : (@bitlogger.Record) -> Bool
|
||||
patch : @bitlogger.RecordPatch
|
||||
@@ -160,6 +161,7 @@ pub fn[S] async_logger(
|
||||
flush_policy: config.flush,
|
||||
sink,
|
||||
flush_callback: flush,
|
||||
flush_async_callback: None,
|
||||
context_fields: [],
|
||||
filter: fn(_) { true },
|
||||
patch: @bitlogger.identity_patch(),
|
||||
@@ -171,6 +173,14 @@ pub fn[S] async_logger(
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn[S] with_async_flush_callback_internal(
|
||||
logger : AsyncLogger[S],
|
||||
callback : async (S) -> Unit,
|
||||
) -> AsyncLogger[S] {
|
||||
{ ..logger, flush_async_callback: Some(callback) }
|
||||
}
|
||||
|
||||
///|
|
||||
fn async_logger_phase_is_closed(phase : AsyncLifecyclePhase) -> Bool {
|
||||
match phase {
|
||||
@@ -535,8 +545,11 @@ pub async fn[S] AsyncLogger::shutdown(
|
||||
}
|
||||
|
||||
///|
|
||||
fn[S] run_flush_callback(logger : AsyncLogger[S]) -> Unit raise {
|
||||
(logger.flush_callback)(logger.sink)
|
||||
async fn[S] run_flush_callback(logger : AsyncLogger[S]) -> Unit {
|
||||
match logger.flush_async_callback {
|
||||
Some(callback) => callback(logger.sink)
|
||||
None => (logger.flush_callback)(logger.sink)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
@@ -546,7 +559,7 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
|
||||
err if err is AsyncLoggerClosed => break
|
||||
err => raise err
|
||||
}
|
||||
logger.sink.write(rec)
|
||||
logger.sink.write_async(rec)
|
||||
if logger.pending_count.val > 0 {
|
||||
logger.pending_count.val -= 1
|
||||
}
|
||||
@@ -557,7 +570,7 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
|
||||
}
|
||||
match next {
|
||||
Some(next) => {
|
||||
logger.sink.write(next)
|
||||
logger.sink.write_async(next)
|
||||
if logger.pending_count.val > 0 {
|
||||
logger.pending_count.val -= 1
|
||||
}
|
||||
@@ -575,7 +588,7 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
|
||||
}
|
||||
match waited {
|
||||
Some(next) => {
|
||||
logger.sink.write(next)
|
||||
logger.sink.write_async(next)
|
||||
if logger.pending_count.val > 0 {
|
||||
logger.pending_count.val -= 1
|
||||
}
|
||||
@@ -598,6 +611,7 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
|
||||
}
|
||||
|
||||
///|
|
||||
#warnings("-fragile_catch_all")
|
||||
pub async fn[S : @bitlogger.Sink] AsyncLogger::run(
|
||||
self : AsyncLogger[S],
|
||||
) -> Unit {
|
||||
@@ -619,14 +633,20 @@ pub async fn[S : @bitlogger.Sink] AsyncLogger::run(
|
||||
pub fn build_async_logger(
|
||||
config : AsyncLoggerBuildConfig,
|
||||
) -> AsyncLogger[@bitlogger.RuntimeSink] {
|
||||
let logger = @bitlogger.build_logger(config.logger)
|
||||
async_logger(
|
||||
let logger = @bitlogger.build_logger_for_async(config.logger)
|
||||
let logger = async_logger(
|
||||
logger.sink,
|
||||
config=config.async_config,
|
||||
min_level=logger.min_level,
|
||||
target=logger.target,
|
||||
flush=fn(sink) { ignore(sink.flush_progress()) },
|
||||
).with_timestamp(enabled=logger.timestamp)
|
||||
)
|
||||
let flush_async : async (@bitlogger.RuntimeSink) -> Unit = sink => {
|
||||
sink.flush_async()
|
||||
}
|
||||
with_async_flush_callback_internal(logger, flush_async).with_timestamp(
|
||||
enabled=logger.timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
|
||||
@@ -5,7 +5,7 @@ async test "native async file lifecycle flushes rotates and restarts" {
|
||||
} else {
|
||||
let path = "logs/bitlogger-async-native-e2e.log"
|
||||
let first_flushes : Ref[Int] = Ref(0)
|
||||
let first_sink = @bitlogger.file_sink(
|
||||
let first_sink = @bitlogger.file_sink_async(
|
||||
path,
|
||||
append=false,
|
||||
auto_flush=false,
|
||||
@@ -47,7 +47,7 @@ async test "native async file lifecycle flushes rotates and restarts" {
|
||||
inspect(first_sink.close(), content="true")
|
||||
|
||||
let restart_flushes : Ref[Int] = Ref(0)
|
||||
let restarted_sink = @bitlogger.file_sink(
|
||||
let restarted_sink = @bitlogger.file_sink_async(
|
||||
path,
|
||||
append=true,
|
||||
auto_flush=false,
|
||||
|
||||
@@ -77,6 +77,7 @@ pub struct AsyncLogger[S] {
|
||||
flush_policy : @utils.AsyncFlushPolicy
|
||||
sink : S
|
||||
flush_callback : (S) -> Unit raise
|
||||
flush_async_callback : (async (S) -> Unit)?
|
||||
context_fields : Array[@core.Field]
|
||||
filter : (@core.Record) -> Bool
|
||||
patch : (@core.Record) -> @core.Record
|
||||
|
||||
@@ -45,6 +45,7 @@ pub struct FileSink {
|
||||
priv default_auto_flush : Bool
|
||||
priv rotation : Ref[FileRotation?]
|
||||
priv default_rotation : FileRotation?
|
||||
priv deferred : Bool
|
||||
priv open_failures : Ref[Int]
|
||||
priv write_failures : Ref[Int]
|
||||
priv flush_failures : Ref[Int]
|
||||
@@ -65,7 +66,64 @@ pub fn file_sink(
|
||||
rotation? : FileRotation? = None,
|
||||
formatter? : RecordFormatter = fn(rec) { @formatting.format_text(rec) },
|
||||
) -> FileSink {
|
||||
let handle = @utils.open_file_handle_internal(path, append)
|
||||
new_file_sink_internal(
|
||||
path,
|
||||
append,
|
||||
auto_flush,
|
||||
rotation,
|
||||
formatter,
|
||||
@utils.open_file_handle_internal(path, append),
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn file_sink_async(
|
||||
path : String,
|
||||
append? : Bool = true,
|
||||
auto_flush? : Bool = true,
|
||||
rotation? : FileRotation? = None,
|
||||
formatter? : RecordFormatter = fn(rec) { @formatting.format_text(rec) },
|
||||
) -> FileSink {
|
||||
new_file_sink_internal(
|
||||
path,
|
||||
append,
|
||||
auto_flush,
|
||||
rotation,
|
||||
formatter,
|
||||
@utils.open_file_handle_async_internal(path, append),
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
#doc(hidden)
|
||||
pub fn file_sink_deferred(
|
||||
path : String,
|
||||
append? : Bool = true,
|
||||
auto_flush? : Bool = true,
|
||||
rotation? : FileRotation? = None,
|
||||
formatter? : RecordFormatter = fn(rec) { @formatting.format_text(rec) },
|
||||
) -> FileSink {
|
||||
new_file_sink_internal(
|
||||
path,
|
||||
append,
|
||||
auto_flush,
|
||||
rotation,
|
||||
formatter,
|
||||
@utils.deferred_file_handle_internal(path, append),
|
||||
deferred=true,
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
fn new_file_sink_internal(
|
||||
path : String,
|
||||
append : Bool,
|
||||
auto_flush : Bool,
|
||||
rotation : FileRotation?,
|
||||
formatter : RecordFormatter,
|
||||
handle : FileHandle?,
|
||||
deferred? : Bool = false,
|
||||
) -> FileSink {
|
||||
{
|
||||
path,
|
||||
append: Ref(append),
|
||||
@@ -76,6 +134,7 @@ pub fn file_sink(
|
||||
default_auto_flush: auto_flush,
|
||||
rotation: Ref(rotation),
|
||||
default_rotation: rotation,
|
||||
deferred,
|
||||
open_failures: Ref(if handle is Some(_) { 0 } else { 1 }),
|
||||
write_failures: Ref(0),
|
||||
flush_failures: Ref(0),
|
||||
@@ -103,6 +162,20 @@ pub fn FileSink::flush(self : FileSink) -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn FileSink::flush_async(self : FileSink) -> Bool {
|
||||
match self.handle.val {
|
||||
None => false
|
||||
Some(handle) => {
|
||||
let ok = @utils.flush_file_handle_async_internal(handle)
|
||||
if !ok {
|
||||
self.flush_failures.val += 1
|
||||
}
|
||||
ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn FileSink::append_mode(self : FileSink) -> Bool {
|
||||
self.append.val
|
||||
@@ -274,6 +347,23 @@ pub fn FileSink::state(self : FileSink) -> FileSinkState {
|
||||
pub fn FileSink::reopen(self : FileSink, append? : Bool? = None) -> Bool {
|
||||
let append_mode = append.unwrap_or(self.append.val)
|
||||
self.append.val = append_mode
|
||||
if self.deferred {
|
||||
match self.handle.val {
|
||||
None => ()
|
||||
Some(handle) => {
|
||||
ignore(@utils.close_file_handle_internal(handle))
|
||||
self.handle.val = None
|
||||
}
|
||||
}
|
||||
let reopened = @utils.deferred_file_handle_internal(self.path, append_mode)
|
||||
self.handle.val = reopened
|
||||
if reopened is Some(_) {
|
||||
true
|
||||
} else {
|
||||
self.open_failures.val += 1
|
||||
false
|
||||
}
|
||||
} else {
|
||||
match self.handle.val {
|
||||
None => ()
|
||||
Some(handle) => {
|
||||
@@ -289,6 +379,7 @@ pub fn FileSink::reopen(self : FileSink, append? : Bool? = None) -> Bool {
|
||||
self.open_failures.val += 1
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
@@ -333,6 +424,29 @@ fn rename_file_if_present(
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
async fn remove_file_if_present_async(path : String) -> Bool {
|
||||
if !@utils.file_exists_async_internal(path) {
|
||||
true
|
||||
} else {
|
||||
@utils.remove_file_async_internal(path) ||
|
||||
!@utils.file_exists_async_internal(path)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
async fn rename_file_if_present_async(
|
||||
from_path : String,
|
||||
to_path : String,
|
||||
) -> Bool {
|
||||
if !@utils.file_exists_async_internal(from_path) {
|
||||
true
|
||||
} else {
|
||||
@utils.rename_file_async_internal(from_path, to_path) ||
|
||||
!@utils.file_exists_async_internal(from_path)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
|
||||
let closed = match sink.handle.val {
|
||||
@@ -396,6 +510,73 @@ fn rotate_if_needed_internal(sink : FileSink, next_line_bytes : Int) -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
async fn rotate_file_sink_async_internal(
|
||||
sink : FileSink,
|
||||
rotation : FileRotation,
|
||||
) -> Bool {
|
||||
let closed = match sink.handle.val {
|
||||
None => true
|
||||
Some(handle) => {
|
||||
let ok = @utils.close_file_handle_internal(handle)
|
||||
sink.handle.val = None
|
||||
ok
|
||||
}
|
||||
}
|
||||
if !closed {
|
||||
return false
|
||||
}
|
||||
if rotation.max_backups > 0 {
|
||||
if !remove_file_if_present_async(
|
||||
rotated_file_path(sink.path, rotation.max_backups),
|
||||
) {
|
||||
return false
|
||||
}
|
||||
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)
|
||||
if !rename_file_if_present_async(from_path, to_path) {
|
||||
return false
|
||||
}
|
||||
continue index - 1
|
||||
}
|
||||
if !rename_file_if_present_async(sink.path, rotated_file_path(sink.path, 1)) {
|
||||
return false
|
||||
}
|
||||
} else if !remove_file_if_present_async(sink.path) {
|
||||
return false
|
||||
}
|
||||
sink.handle.val = @utils.open_file_handle_async_internal(sink.path, false)
|
||||
sink.handle.val is Some(_)
|
||||
}
|
||||
|
||||
///|
|
||||
async fn rotate_if_needed_async_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 = @utils.file_size_i64_async_internal(handle)
|
||||
let next_line = next_line_bytes.to_int64()
|
||||
if size + next_line <= rotation_max_bytes_internal(rotation) {
|
||||
true
|
||||
} else {
|
||||
let rotated = rotate_file_sink_async_internal(sink, rotation)
|
||||
if !rotated {
|
||||
sink.rotation_failures.val += 1
|
||||
}
|
||||
rotated
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl @sink_graph.Sink for FileSink with fn write(self, rec : Record) {
|
||||
match self.handle.val {
|
||||
@@ -429,3 +610,37 @@ pub impl @sink_graph.Sink for FileSink with fn write(self, rec : Record) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl @sink_graph.Sink for FileSink with fn write_async(self, rec : Record) {
|
||||
match self.handle.val {
|
||||
None => self.write_failures.val += 1
|
||||
Some(_) => {
|
||||
let line = "\{(self.formatter)(rec)}\n"
|
||||
let can_write = rotate_if_needed_async_internal(
|
||||
self,
|
||||
@utils.string_byte_length_internal(line),
|
||||
)
|
||||
if can_write {
|
||||
match self.handle.val {
|
||||
None => self.write_failures.val += 1
|
||||
Some(active) => {
|
||||
let wrote = @utils.write_file_handle_async_internal(active, line)
|
||||
if wrote {
|
||||
if self.auto_flush.val {
|
||||
let flushed = @utils.flush_file_handle_async_internal(active)
|
||||
if !flushed {
|
||||
self.flush_failures.val += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.write_failures.val += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.write_failures.val += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,14 @@ test "file sink counts an injected partial backup-chain failure" {
|
||||
rec.message
|
||||
})
|
||||
if seed.is_available() {
|
||||
seed.write(@core.Record::new(@core.Level::Info, "1234567890"))
|
||||
seed.write(@core.Record::new(@core.Level::Info, "abcdefghij"))
|
||||
@sink_graph.Sink::write(
|
||||
seed,
|
||||
@core.Record::new(@core.Level::Info, "1234567890"),
|
||||
)
|
||||
@sink_graph.Sink::write(
|
||||
seed,
|
||||
@core.Record::new(@core.Level::Info, "abcdefghij"),
|
||||
)
|
||||
inspect(seed.rotation_failures(), content="0")
|
||||
inspect(seed.close(), content="true")
|
||||
|
||||
@@ -33,7 +39,10 @@ test "file sink counts an injected partial backup-chain failure" {
|
||||
}
|
||||
},
|
||||
}
|
||||
sink.write(@core.Record::new(@core.Level::Info, "klmnopqrst"))
|
||||
@sink_graph.Sink::write(
|
||||
sink,
|
||||
@core.Record::new(@core.Level::Info, "klmnopqrst"),
|
||||
)
|
||||
|
||||
inspect(sink.rotation_failures(), content="1")
|
||||
inspect(sink.state().rotation_failures, content="1")
|
||||
@@ -65,8 +74,14 @@ test "file sink counts an injected oldest-backup removal failure" {
|
||||
rec.message
|
||||
})
|
||||
if seed.is_available() {
|
||||
seed.write(@core.Record::new(@core.Level::Info, "1234567890"))
|
||||
seed.write(@core.Record::new(@core.Level::Info, "abcdefghij"))
|
||||
@sink_graph.Sink::write(
|
||||
seed,
|
||||
@core.Record::new(@core.Level::Info, "1234567890"),
|
||||
)
|
||||
@sink_graph.Sink::write(
|
||||
seed,
|
||||
@core.Record::new(@core.Level::Info, "abcdefghij"),
|
||||
)
|
||||
inspect(seed.close(), content="true")
|
||||
|
||||
let oldest = @utils.open_file_handle_internal(test_path + ".2", true)
|
||||
@@ -95,7 +110,10 @@ test "file sink counts an injected oldest-backup removal failure" {
|
||||
@utils.rename_file_internal(from_path, to_path)
|
||||
},
|
||||
}
|
||||
sink.write(@core.Record::new(@core.Level::Info, "klmnopqrst"))
|
||||
@sink_graph.Sink::write(
|
||||
sink,
|
||||
@core.Record::new(@core.Level::Info, "klmnopqrst"),
|
||||
)
|
||||
|
||||
inspect(sink.rotation_failures(), content="1")
|
||||
inspect(sink.write_failures(), content="1")
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
// Values
|
||||
pub fn file_sink(String, append? : Bool, auto_flush? : Bool, rotation? : @file_model.FileRotation?, formatter? : (@core.Record) -> String) -> FileSink
|
||||
|
||||
pub async fn file_sink_async(String, append? : Bool, auto_flush? : Bool, rotation? : @file_model.FileRotation?, formatter? : (@core.Record) -> String) -> FileSink
|
||||
|
||||
pub fn native_files_supported() -> Bool
|
||||
|
||||
// Errors
|
||||
@@ -24,6 +26,7 @@ pub fn FileSink::clear_rotation(Self) -> Unit
|
||||
pub fn FileSink::close(Self) -> Bool
|
||||
pub fn FileSink::default_policy(Self) -> @file_model.FileSinkPolicy
|
||||
pub fn FileSink::flush(Self) -> Bool
|
||||
pub async fn FileSink::flush_async(Self) -> Bool
|
||||
pub fn FileSink::flush_failures(Self) -> Int
|
||||
pub fn FileSink::is_available(Self) -> Bool
|
||||
pub fn FileSink::open_failures(Self) -> Int
|
||||
|
||||
@@ -916,7 +916,7 @@ fn parse_inline_markup(
|
||||
formatter : TextFormatter,
|
||||
) -> Array[StyledSegment] {
|
||||
let segments : Array[StyledSegment] = []
|
||||
let buffer = StringBuilder::new()
|
||||
let buffer = StringBuilder()
|
||||
let stack : Array[StyleFrame] = [{ tag: "", style: default_inline_style() }]
|
||||
let chars = input.to_array()
|
||||
let current_style = fn() { stack[stack.length() - 1].style }
|
||||
@@ -990,7 +990,7 @@ fn render_styled_text(
|
||||
let enabled = use_ansi_color(formatter.color_mode)
|
||||
let scoped = formatter.with_style_markup(mode)
|
||||
let segments = parse_inline_markup(text, scoped)
|
||||
let out = StringBuilder::new()
|
||||
let out = StringBuilder()
|
||||
for segment in segments {
|
||||
out.write_string(ansi_wrap_with_style(segment.text, segment.style, enabled))
|
||||
}
|
||||
|
||||
@@ -20,3 +20,7 @@ import {
|
||||
import {
|
||||
"Nanaloveyuki/BitLogger/src/utils",
|
||||
} for "wbtest"
|
||||
|
||||
import {
|
||||
"moonbitlang/core/quickcheck",
|
||||
} for "test"
|
||||
|
||||
@@ -75,6 +75,8 @@ pub fn file_rotation_i64(Int64, max_backups? : Int) -> @file_model.FileRotation
|
||||
|
||||
pub fn file_sink(String, append? : Bool, auto_flush? : Bool, rotation? : @file_model.FileRotation?, formatter? : (@core.Record) -> String) -> @file_runtime.FileSink
|
||||
|
||||
pub async fn file_sink_async(String, append? : Bool, auto_flush? : Bool, rotation? : @file_model.FileRotation?, formatter? : (@core.Record) -> String) -> @file_runtime.FileSink
|
||||
|
||||
pub fn file_sink_policy_to_json(@file_model.FileSinkPolicy) -> Json
|
||||
|
||||
pub fn file_sink_state_to_json(@file_model.FileSinkState) -> Json
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
///|
|
||||
fn pbt_level(seed : Int) -> Level {
|
||||
match (seed & 0x3fffffff) % 5 {
|
||||
0 => Level::Trace
|
||||
1 => Level::Debug
|
||||
2 => Level::Info
|
||||
3 => Level::Warn
|
||||
_ => Level::Error
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_nonnegative(seed : Int, modulus : Int) -> Int {
|
||||
(seed & 0x3fffffff) % modulus
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_bit(seed : Int, bit : Int) -> Bool {
|
||||
(seed & bit) != 0
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_fields_equal(left : Array[Field], right : Array[Field]) -> Bool {
|
||||
guard left.length() == right.length() else { return false }
|
||||
for i in 0..<left.length() {
|
||||
guard left[i].key == right[i].key && left[i].value == right[i].value else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_records_equal(left : Record, right : Record) -> Bool {
|
||||
left.level.label() == right.level.label() &&
|
||||
left.timestamp_ms == right.timestamp_ms &&
|
||||
left.target == right.target &&
|
||||
left.message == right.message &&
|
||||
pbt_fields_equal(left.fields, right.fields)
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_history_matches(
|
||||
sink : HistorySink,
|
||||
expected : Array[(String, String)],
|
||||
expected_dropped : Int,
|
||||
) -> Bool {
|
||||
guard sink.count() == expected.length() else { return false }
|
||||
guard sink.dropped_count() == expected_dropped else { return false }
|
||||
let snapshot = sink.snapshot()
|
||||
guard snapshot.length() == expected.length() else { return false }
|
||||
for i in 0..<expected.length() {
|
||||
let (message, id) = expected[i]
|
||||
let record = snapshot[i]
|
||||
guard record.message == message else { return false }
|
||||
guard record.fields.length() == 1 else { return false }
|
||||
guard record.fields[0].key == "id" else { return false }
|
||||
guard record.fields[0].value == id else { return false }
|
||||
}
|
||||
// The public snapshot must not expose the sink's retained field arrays.
|
||||
if snapshot.length() > 0 {
|
||||
snapshot[0].fields.push(field("mutated", "outside"))
|
||||
guard sink.snapshot()[0].fields.length() == 1 else { return false }
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
///|
|
||||
test "quickcheck: history sink agrees with a bounded model" {
|
||||
@quickcheck.check(
|
||||
(input : (Int, Array[(Int, Int)])) => {
|
||||
let (capacity_seed, operations) = input
|
||||
let requested_capacity = capacity_seed % 8
|
||||
let capacity = if requested_capacity <= 0 {
|
||||
1
|
||||
} else {
|
||||
requested_capacity
|
||||
}
|
||||
let sink = history_sink(capacity=requested_capacity)
|
||||
let mut expected : Array[(String, String)] = []
|
||||
let mut expected_dropped = 0
|
||||
for operation in operations {
|
||||
let (kind, value) = operation
|
||||
if (kind & 7) == 0 {
|
||||
sink.clear()
|
||||
expected = []
|
||||
expected_dropped = 0
|
||||
} else {
|
||||
let id = value.to_string()
|
||||
let message = "message:" + id
|
||||
let record = Record::new(Level::Info, message, fields=[
|
||||
field("id", id),
|
||||
])
|
||||
@sink_graph.Sink::write(sink, record)
|
||||
if expected.length() >= capacity {
|
||||
ignore(expected.remove(0))
|
||||
expected_dropped += 1
|
||||
}
|
||||
expected.push((message, id))
|
||||
}
|
||||
guard pbt_history_matches(sink, expected, expected_dropped) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
pbt_history_matches(sink, expected, expected_dropped)
|
||||
},
|
||||
count=220,
|
||||
max_size=80,
|
||||
max_shrinks=200,
|
||||
seed=0x51A7UL,
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_queue_limit(seed : Int) -> Int {
|
||||
match seed & 3 {
|
||||
0 => -1
|
||||
1 => 0
|
||||
2 => 1
|
||||
_ => 3
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_drain_model(
|
||||
expected : Array[String],
|
||||
delivered : Array[String],
|
||||
max_items : Int,
|
||||
) -> Int {
|
||||
if max_items == 0 {
|
||||
return 0
|
||||
}
|
||||
let limit = if max_items < 0 { expected.length() } else { max_items }
|
||||
let count = if limit > expected.length() { expected.length() } else { limit }
|
||||
for _ in 0..<count {
|
||||
delivered.push(expected.remove(0))
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_queue_matches(
|
||||
sink : QueuedSink[CallbackSink],
|
||||
expected : Array[String],
|
||||
expected_dropped : Int,
|
||||
delivered : Ref[Array[String]],
|
||||
expected_delivered : Array[String],
|
||||
) -> Bool {
|
||||
sink.pending_count() == expected.length() &&
|
||||
sink.dropped_count() == expected_dropped &&
|
||||
delivered.val == expected_delivered
|
||||
}
|
||||
|
||||
///|
|
||||
test "quickcheck: queued sink agrees with an overflow and drain model" {
|
||||
@quickcheck.check(
|
||||
(input : (Int, Int, Array[(Int, Int)])) => {
|
||||
let (max_pending_seed, policy_seed, operations) = input
|
||||
let max_pending = pbt_nonnegative(max_pending_seed, 8)
|
||||
let policy = if pbt_bit(policy_seed, 1) {
|
||||
QueueOverflowPolicy::DropOldest
|
||||
} else {
|
||||
QueueOverflowPolicy::DropNewest
|
||||
}
|
||||
let delivered : Ref[Array[String]] = Ref([])
|
||||
let sink = queued_sink(
|
||||
callback_sink(fn(record) { delivered.val.push(record.message) }),
|
||||
max_pending~,
|
||||
overflow=policy,
|
||||
)
|
||||
let expected : Array[String] = []
|
||||
let expected_delivered : Array[String] = []
|
||||
let mut expected_dropped = 0
|
||||
for operation in operations {
|
||||
let (kind, value) = operation
|
||||
match kind & 7 {
|
||||
0 | 1 | 2 | 3 => {
|
||||
let message = "message:" + value.to_string()
|
||||
let record = Record::new(Level::Info, message)
|
||||
let full = max_pending > 0 && expected.length() >= max_pending
|
||||
if full {
|
||||
expected_dropped += 1
|
||||
match policy {
|
||||
QueueOverflowPolicy::DropNewest => ()
|
||||
QueueOverflowPolicy::DropOldest => {
|
||||
ignore(expected.remove(0))
|
||||
expected.push(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
expected.push(message)
|
||||
}
|
||||
@sink_graph.Sink::write(sink, record)
|
||||
}
|
||||
4 => {
|
||||
let limit = pbt_queue_limit(value)
|
||||
let expected_count = pbt_drain_model(
|
||||
expected, expected_delivered, limit,
|
||||
)
|
||||
let actual_count = sink.drain(max_items=limit)
|
||||
guard actual_count == expected_count else { return false }
|
||||
}
|
||||
5 => {
|
||||
let expected_count = pbt_drain_model(
|
||||
expected, expected_delivered, -1,
|
||||
)
|
||||
guard sink.flush() == expected_count else { return false }
|
||||
}
|
||||
_ => {
|
||||
let expected_count = pbt_drain_model(
|
||||
expected, expected_delivered, 0,
|
||||
)
|
||||
guard expected_count == 0 else { return false }
|
||||
guard sink.drain(max_items=0) == 0 else { return false }
|
||||
}
|
||||
}
|
||||
guard pbt_queue_matches(
|
||||
sink, expected, expected_dropped, delivered, expected_delivered,
|
||||
) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
pbt_queue_matches(
|
||||
sink, expected, expected_dropped, delivered, expected_delivered,
|
||||
)
|
||||
},
|
||||
count=220,
|
||||
max_size=80,
|
||||
max_shrinks=200,
|
||||
seed=0xA11CEUL,
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_patch_for(operation : (Int, String)) -> RecordPatch {
|
||||
let (kind, value) = operation
|
||||
match kind & 7 {
|
||||
0 => identity_patch()
|
||||
1 => set_target("target." + value)
|
||||
2 => prefix_message("[" + value + "] ")
|
||||
3 => append_fields([field("extra", value)])
|
||||
4 => redact_field("secret", placeholder=value)
|
||||
_ => redact_fields(["secret", "token"], placeholder=value)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "quickcheck: composed patches match sequential application" {
|
||||
@quickcheck.check(
|
||||
(input : (Array[(Int, String)], (Int, String, Array[(String, String)]))) => {
|
||||
let (operations, record_input) = input
|
||||
let (level_seed, message, raw_fields) = record_input
|
||||
let base = Record::new(
|
||||
pbt_level(level_seed),
|
||||
message,
|
||||
timestamp_ms=123UL,
|
||||
target="service",
|
||||
fields=raw_fields.map(entry => field(entry.0, entry.1)),
|
||||
)
|
||||
let patches : Array[RecordPatch] = operations.map(pbt_patch_for)
|
||||
let mut sequential = base
|
||||
for patch in patches {
|
||||
sequential = patch(sequential)
|
||||
}
|
||||
let composed = compose_patches(patches)(base)
|
||||
pbt_records_equal(sequential, composed)
|
||||
},
|
||||
count=220,
|
||||
max_size=70,
|
||||
max_shrinks=200,
|
||||
seed=0xC0DEUL,
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_color_mode(seed : Int) -> ColorMode {
|
||||
match pbt_nonnegative(seed, 3) {
|
||||
0 => ColorMode::Never
|
||||
1 => ColorMode::Auto
|
||||
_ => ColorMode::Always
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_color_support(seed : Int) -> ColorSupport {
|
||||
if pbt_bit(seed, 1) {
|
||||
ColorSupport::Basic
|
||||
} else {
|
||||
ColorSupport::TrueColor
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_markup_mode(seed : Int) -> StyleMarkupMode {
|
||||
match pbt_nonnegative(seed, 3) {
|
||||
0 => StyleMarkupMode::Disabled
|
||||
1 => StyleMarkupMode::Builtin
|
||||
_ => StyleMarkupMode::Full
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn pbt_sink_kind(seed : Int) -> SinkKind {
|
||||
match pbt_nonnegative(seed, 4) {
|
||||
0 => SinkKind::Console
|
||||
1 => SinkKind::JsonConsole
|
||||
2 => SinkKind::TextConsole
|
||||
_ => SinkKind::File
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
test "quickcheck: serialized logger configs parse to the same canonical text" {
|
||||
@quickcheck.check(
|
||||
(input : (Int, String, Bool, (Int, Int, Int, String))) => {
|
||||
let (seed, target, timestamp, sink_input) = input
|
||||
let (sink_seed, queue_seed, flags, suffix) = sink_input
|
||||
let kind = pbt_sink_kind(sink_seed)
|
||||
let path = match kind {
|
||||
SinkKind::File =>
|
||||
if suffix == "" {
|
||||
"logs/generated.log"
|
||||
} else {
|
||||
"logs/" + suffix
|
||||
}
|
||||
_ => suffix
|
||||
}
|
||||
let style_tags : Map[String, TextStyle] = if pbt_bit(flags, 16) {
|
||||
{ "accent": text_style(fg=Some(suffix), bold=pbt_bit(flags, 32)) }
|
||||
} else {
|
||||
Map([])
|
||||
}
|
||||
let formatter = TextFormatterConfig::new(
|
||||
show_timestamp=pbt_bit(flags, 1),
|
||||
show_level=pbt_bit(flags, 2),
|
||||
show_target=pbt_bit(flags, 4),
|
||||
show_fields=pbt_bit(flags, 8),
|
||||
separator=if suffix == "" { " " } else { suffix },
|
||||
field_separator=if target == "" { "," } else { target },
|
||||
template="[{level}] {target} {message}",
|
||||
color_mode=pbt_color_mode(seed),
|
||||
color_support=pbt_color_support(sink_seed),
|
||||
style_markup=pbt_markup_mode(flags),
|
||||
target_style_markup=pbt_markup_mode(seed),
|
||||
fields_style_markup=pbt_markup_mode(queue_seed),
|
||||
style_tags~,
|
||||
)
|
||||
let rotation = if pbt_bit(sink_seed, 1) {
|
||||
Some(
|
||||
file_rotation(
|
||||
1 + pbt_nonnegative(queue_seed, 64),
|
||||
max_backups=1 + pbt_nonnegative(flags, 4),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let sink = SinkConfig::new(
|
||||
kind~,
|
||||
path~,
|
||||
append=pbt_bit(flags, 1),
|
||||
auto_flush=pbt_bit(flags, 2),
|
||||
rotation~,
|
||||
text_formatter=formatter,
|
||||
)
|
||||
let queue = if pbt_bit(queue_seed, 1) {
|
||||
Some(
|
||||
QueueConfig::new(
|
||||
queue_seed % 17,
|
||||
overflow=if pbt_bit(queue_seed, 2) {
|
||||
QueueOverflowPolicy::DropOldest
|
||||
} else {
|
||||
QueueOverflowPolicy::DropNewest
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let config = LoggerConfig::new(
|
||||
min_level=pbt_level(seed),
|
||||
target~,
|
||||
timestamp~,
|
||||
sink~,
|
||||
queue~,
|
||||
)
|
||||
let serialized = stringify_logger_config(config)
|
||||
(fn() -> Bool raise ConfigError {
|
||||
let parsed = parse_logger_config_text(serialized)
|
||||
stringify_logger_config(parsed) == serialized
|
||||
})() catch {
|
||||
_ => false
|
||||
}
|
||||
},
|
||||
count=180,
|
||||
max_size=60,
|
||||
max_shrinks=200,
|
||||
seed=0xBEEF1234UL,
|
||||
)
|
||||
}
|
||||
@@ -68,6 +68,7 @@ pub fn RuntimeSink::file_state(Self) -> @file_model.FileSinkState
|
||||
pub fn RuntimeSink::file_state_or_none(Self) -> @file_model.FileSinkState?
|
||||
pub fn RuntimeSink::file_write_failures(Self) -> Int
|
||||
pub fn RuntimeSink::flush(Self) -> Int
|
||||
pub async fn RuntimeSink::flush_async(Self) -> Unit
|
||||
pub fn RuntimeSink::flush_progress(Self) -> RuntimeSinkProgress
|
||||
pub fn RuntimeSink::pending_count(Self) -> Int
|
||||
pub impl @sink_graph.Sink for RuntimeSink
|
||||
|
||||
@@ -81,6 +81,20 @@ pub impl @sink_graph.Sink for RuntimeSink with fn write(self, rec) {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl @sink_graph.Sink for RuntimeSink with fn write_async(self, rec) {
|
||||
match self {
|
||||
Console(sink) => sink.write(rec)
|
||||
JsonConsole(sink) => sink.write(rec)
|
||||
TextConsole(sink) => sink.write(rec)
|
||||
File(sink) => sink.write_async(rec)
|
||||
QueuedConsole(sink) => sink.write(rec)
|
||||
QueuedJsonConsole(sink) => sink.write(rec)
|
||||
QueuedTextConsole(sink) => sink.write(rec)
|
||||
QueuedFile(sink) => sink.write(rec)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn[S : @sink_graph.Sink] queued_sink_close_internal(
|
||||
sink : QueuedSink[S],
|
||||
@@ -202,6 +216,23 @@ pub fn RuntimeSink::flush(self : RuntimeSink) -> Int {
|
||||
runtime_sink_progress_compat_count(self.flush_progress())
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn RuntimeSink::flush_async(self : RuntimeSink) -> Unit {
|
||||
match self {
|
||||
Console(_) => ()
|
||||
JsonConsole(_) => ()
|
||||
TextConsole(_) => ()
|
||||
File(sink) => ignore(sink.flush_async())
|
||||
QueuedConsole(sink) => ignore(sink.flush_async())
|
||||
QueuedJsonConsole(sink) => ignore(sink.flush_async())
|
||||
QueuedTextConsole(sink) => ignore(sink.flush_async())
|
||||
QueuedFile(sink) => {
|
||||
ignore(sink.flush_async())
|
||||
ignore(sink.sink.flush_async())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn RuntimeSink::drain_progress(
|
||||
self : RuntimeSink,
|
||||
@@ -671,7 +702,10 @@ pub fn RuntimeSink::file_runtime_state(self : RuntimeSink) -> RuntimeFileState?
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn build_runtime_sink(config : SinkConfig) -> RuntimeSink {
|
||||
fn build_runtime_sink_internal(
|
||||
config : SinkConfig,
|
||||
defer_file_open : Bool,
|
||||
) -> RuntimeSink {
|
||||
match config.kind {
|
||||
SinkKind::Console => RuntimeSink::Console(@sink_graph.console_sink())
|
||||
SinkKind::JsonConsole =>
|
||||
@@ -682,6 +716,20 @@ pub fn build_runtime_sink(config : SinkConfig) -> RuntimeSink {
|
||||
)
|
||||
SinkKind::File =>
|
||||
RuntimeSink::File(
|
||||
if defer_file_open {
|
||||
@file_runtime.file_sink_deferred(
|
||||
config.path,
|
||||
append=config.append,
|
||||
auto_flush=config.auto_flush,
|
||||
rotation=config.rotation,
|
||||
formatter=fn(rec) {
|
||||
@formatting.format_text(
|
||||
rec,
|
||||
formatter=config.text_formatter.to_formatter(),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
@file_runtime.file_sink(
|
||||
config.path,
|
||||
append=config.append,
|
||||
@@ -693,11 +741,23 @@ pub fn build_runtime_sink(config : SinkConfig) -> RuntimeSink {
|
||||
formatter=config.text_formatter.to_formatter(),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn build_runtime_sink(config : SinkConfig) -> RuntimeSink {
|
||||
build_runtime_sink_internal(config, false)
|
||||
}
|
||||
|
||||
///|
|
||||
#doc(hidden)
|
||||
pub fn build_runtime_sink_for_async(config : SinkConfig) -> RuntimeSink {
|
||||
build_runtime_sink_internal(config, true)
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn apply_queue_config(
|
||||
sink : RuntimeSink,
|
||||
|
||||
+20
-2
@@ -64,8 +64,15 @@ pub fn Logger::dropped_count(self : Logger[RuntimeSink]) -> Int {
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn build_logger(config : LoggerConfig) -> Logger[RuntimeSink] {
|
||||
let sink = @runtime.build_runtime_sink(config.sink)
|
||||
fn build_logger_internal(
|
||||
config : LoggerConfig,
|
||||
defer_file_open : Bool,
|
||||
) -> Logger[RuntimeSink] {
|
||||
let sink = if defer_file_open {
|
||||
@runtime.build_runtime_sink_for_async(config.sink)
|
||||
} else {
|
||||
@runtime.build_runtime_sink(config.sink)
|
||||
}
|
||||
let actual_sink = match config.queue {
|
||||
None => sink
|
||||
Some(queue) => @runtime.apply_queue_config(sink, queue)
|
||||
@@ -75,6 +82,17 @@ pub fn build_logger(config : LoggerConfig) -> Logger[RuntimeSink] {
|
||||
)
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn build_logger(config : LoggerConfig) -> Logger[RuntimeSink] {
|
||||
build_logger_internal(config, false)
|
||||
}
|
||||
|
||||
///|
|
||||
#doc(hidden)
|
||||
pub fn build_logger_for_async(config : LoggerConfig) -> Logger[RuntimeSink] {
|
||||
build_logger_internal(config, true)
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn parse_and_build_logger(
|
||||
input : String,
|
||||
|
||||
@@ -51,6 +51,7 @@ pub struct BufferedSink[S] {
|
||||
flush_limit : Int
|
||||
}
|
||||
pub fn[S : Sink] BufferedSink::flush(Self[S]) -> Unit
|
||||
pub async fn[S : Sink] BufferedSink::flush_async(Self[S]) -> Unit
|
||||
pub fn[S] BufferedSink::pending_count(Self[S]) -> Int
|
||||
pub impl[S : Sink] Sink for BufferedSink[S]
|
||||
|
||||
@@ -124,8 +125,10 @@ pub struct QueuedSink[S] {
|
||||
dropped_count : @ref.Ref[Int]
|
||||
}
|
||||
pub fn[S : Sink] QueuedSink::drain(Self[S], max_items? : Int) -> Int
|
||||
pub async fn[S : Sink] QueuedSink::drain_async(Self[S], max_items? : Int) -> Int
|
||||
pub fn[S] QueuedSink::dropped_count(Self[S]) -> Int
|
||||
pub fn[S : Sink] QueuedSink::flush(Self[S]) -> Int
|
||||
pub async fn[S : Sink] QueuedSink::flush_async(Self[S]) -> Int
|
||||
pub fn[S] QueuedSink::pending_count(Self[S]) -> Int
|
||||
pub impl[S] Sink for QueuedSink[S]
|
||||
|
||||
@@ -142,4 +145,5 @@ pub using @queue_model {type QueueOverflowPolicy}
|
||||
// Traits
|
||||
pub(open) trait Sink {
|
||||
fn write(Self, @core.Record) -> Unit
|
||||
async fn write_async(Self, @core.Record) -> Unit = _
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ type RecordPatch = @record_ops.RecordPatch
|
||||
///|
|
||||
pub(open) trait Sink {
|
||||
fn write(Self, Record) -> Unit
|
||||
async fn write_async(Self, Record) -> Unit = _
|
||||
}
|
||||
|
||||
///|
|
||||
impl Sink with fn write_async(self : Self, rec : Record) {
|
||||
self.write(rec)
|
||||
}
|
||||
|
||||
///|
|
||||
@@ -63,6 +69,18 @@ pub impl[S : Sink] Sink for ContextSink[S] with fn write(self, rec) {
|
||||
self.sink.write(rec.with_fields(merged))
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl[S : Sink] Sink for ContextSink[S] with fn write_async(self, rec) {
|
||||
let merged = if self.context_fields.length() == 0 {
|
||||
rec.fields
|
||||
} else if rec.fields.length() == 0 {
|
||||
self.context_fields
|
||||
} else {
|
||||
self.context_fields + rec.fields
|
||||
}
|
||||
self.sink.write_async(rec.with_fields(merged))
|
||||
}
|
||||
|
||||
///|
|
||||
pub struct JsonConsoleSink {
|
||||
_dummy : Unit
|
||||
@@ -148,6 +166,15 @@ pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with fn write(self, rec)
|
||||
self.right.write(rec.copy())
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with fn write_async(
|
||||
self,
|
||||
rec,
|
||||
) {
|
||||
self.left.write_async(rec)
|
||||
self.right.write_async(rec.copy())
|
||||
}
|
||||
|
||||
///|
|
||||
pub struct SplitSink[A, B] {
|
||||
left : A
|
||||
@@ -182,6 +209,18 @@ pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with fn write(self, rec) {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with fn write_async(
|
||||
self,
|
||||
rec,
|
||||
) {
|
||||
if (self.predicate)(rec) {
|
||||
self.left.write_async(rec)
|
||||
} else {
|
||||
self.right.write_async(rec)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub struct CallbackSink {
|
||||
callback : (Record) -> Unit
|
||||
@@ -299,6 +338,29 @@ pub impl[S : Sink] Sink for BufferedSink[S] with fn write(self, rec) {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn[S : Sink] BufferedSink::flush_async(
|
||||
self : BufferedSink[S],
|
||||
) -> Unit {
|
||||
if self.buffer.val.length() == 0 {
|
||||
()
|
||||
} else {
|
||||
let pending = self.buffer.val
|
||||
self.buffer.val = []
|
||||
for rec in pending {
|
||||
self.sink.write_async(rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl[S : Sink] Sink for BufferedSink[S] with fn write_async(self, rec) {
|
||||
self.buffer.val.push(rec)
|
||||
if self.buffer.val.length() >= self.flush_limit {
|
||||
self.flush_async()
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub type QueueOverflowPolicy = @queue_model.QueueOverflowPolicy
|
||||
|
||||
@@ -358,11 +420,38 @@ pub fn[S : Sink] QueuedSink::drain(
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn[S : Sink] QueuedSink::drain_async(
|
||||
self : QueuedSink[S],
|
||||
max_items? : Int = -1,
|
||||
) -> Int {
|
||||
if max_items == 0 {
|
||||
return 0
|
||||
}
|
||||
let limit = if max_items < 0 { self.pending_count() } else { max_items }
|
||||
for drained = 0; drained < limit; {
|
||||
match self.queue.pop() {
|
||||
None => break drained
|
||||
Some(rec) => {
|
||||
self.sink.write_async(rec)
|
||||
continue drained + 1
|
||||
}
|
||||
}
|
||||
} nobreak {
|
||||
limit
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn[S : Sink] QueuedSink::flush(self : QueuedSink[S]) -> Int {
|
||||
self.drain()
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn[S : Sink] QueuedSink::flush_async(self : QueuedSink[S]) -> Int {
|
||||
self.drain_async()
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl[S] Sink for QueuedSink[S] with fn write(self, rec) {
|
||||
let full = self.max_pending > 0 && self.pending_count() >= self.max_pending
|
||||
@@ -398,6 +487,13 @@ pub impl[S : Sink] Sink for FilterSink[S] with fn write(self, rec) {
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl[S : Sink] Sink for FilterSink[S] with fn write_async(self, rec) {
|
||||
if (self.predicate)(rec) {
|
||||
self.sink.write_async(rec)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub struct PatchSink[S] {
|
||||
sink : S
|
||||
@@ -413,3 +509,8 @@ pub fn[S] patch_sink(sink : S, patch : RecordPatch) -> PatchSink[S] {
|
||||
pub impl[S : Sink] Sink for PatchSink[S] with fn write(self, rec) {
|
||||
self.sink.write((self.patch)(rec))
|
||||
}
|
||||
|
||||
///|
|
||||
pub impl[S : Sink] Sink for PatchSink[S] with fn write_async(self, rec) {
|
||||
self.sink.write_async((self.patch)(rec))
|
||||
}
|
||||
|
||||
@@ -64,3 +64,20 @@ pub fn file_sink(
|
||||
) -> FileSink {
|
||||
@file_runtime.file_sink(path, append~, auto_flush~, rotation~, formatter~)
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn file_sink_async(
|
||||
path : String,
|
||||
append? : Bool = true,
|
||||
auto_flush? : Bool = true,
|
||||
rotation? : FileRotation? = None,
|
||||
formatter? : RecordFormatter = fn(rec) { format_text(rec) },
|
||||
) -> FileSink {
|
||||
@file_runtime.file_sink_async(
|
||||
path,
|
||||
append~,
|
||||
auto_flush~,
|
||||
rotation~,
|
||||
formatter~,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,106 +1,137 @@
|
||||
///|
|
||||
fn string_to_c_bytes(str : String) -> Bytes {
|
||||
let res : Array[Byte] = []
|
||||
let len = str.length()
|
||||
let mut i = 0
|
||||
while i < len {
|
||||
let mut c = str.code_unit_at(i).to_int()
|
||||
if 0xD800 <= c && c <= 0xDBFF {
|
||||
c -= 0xD800
|
||||
i = i + 1
|
||||
let l = str.code_unit_at(i).to_int() - 0xDC00
|
||||
c = (c << 10) + l + 0x10000
|
||||
}
|
||||
if c < 0x80 {
|
||||
res.push(c.to_byte())
|
||||
} else if c < 0x800 {
|
||||
res.push((0xc0 + (c >> 6)).to_byte())
|
||||
res.push((0x80 + (c & 0x3f)).to_byte())
|
||||
} else if c < 0x10000 {
|
||||
res.push((0xe0 + (c >> 12)).to_byte())
|
||||
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
|
||||
res.push((0x80 + (c & 0x3f)).to_byte())
|
||||
} else {
|
||||
res.push((0xf0 + (c >> 18)).to_byte())
|
||||
res.push((0x80 + ((c >> 12) & 0x3f)).to_byte())
|
||||
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
|
||||
res.push((0x80 + (c & 0x3f)).to_byte())
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
res.push((0).to_byte())
|
||||
Bytes::from_array(res)
|
||||
pub struct FileHandle {
|
||||
priv path : String
|
||||
priv append : Bool
|
||||
priv initialized : Ref[Bool]
|
||||
priv position : Ref[Int64]
|
||||
priv closed : Ref[Bool]
|
||||
}
|
||||
|
||||
///|
|
||||
#external
|
||||
type NativeFileHandle
|
||||
fn run_async_bool_internal(operation : async () -> Bool) -> Bool {
|
||||
let result = Ref(false)
|
||||
@async.run_async_main(() => result.val = operation())
|
||||
result.val
|
||||
}
|
||||
|
||||
///|
|
||||
#borrow(path, mode)
|
||||
extern "C" fn file_open_ffi(path : Bytes, mode : Bytes) -> NativeFileHandle = "bitlogger_file_open"
|
||||
fn run_async_i64_internal(operation : async () -> Int64) -> Int64 {
|
||||
let result = Ref(0L)
|
||||
@async.run_async_main(() => result.val = operation())
|
||||
result.val
|
||||
}
|
||||
|
||||
///|
|
||||
extern "C" fn file_is_null_ffi(handle : NativeFileHandle) -> Bool = "bitlogger_pointer_is_null"
|
||||
fn new_file_handle_internal(
|
||||
path : String,
|
||||
append : Bool,
|
||||
initialized : Bool,
|
||||
) -> FileHandle {
|
||||
{
|
||||
path,
|
||||
append,
|
||||
initialized: Ref(initialized),
|
||||
position: Ref(0L),
|
||||
closed: Ref(false),
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
#borrow(buffer)
|
||||
extern "C" fn file_write_ffi(
|
||||
buffer : Bytes,
|
||||
size : Int,
|
||||
count : Int,
|
||||
handle : NativeFileHandle,
|
||||
) -> Int = "bitlogger_file_write"
|
||||
async fn probe_open_file_handle_async_internal(
|
||||
path : String,
|
||||
append : Bool,
|
||||
) -> Bool {
|
||||
try {
|
||||
let create_mode = if append {
|
||||
@fs.OpenOrCreate
|
||||
} else {
|
||||
@fs.CreateOrTruncate
|
||||
}
|
||||
let file = @fs.open(path, mode=@fs.WriteOnly, append~, create_mode~)
|
||||
file.close()
|
||||
true
|
||||
} catch {
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
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_i64_ffi(handle : NativeFileHandle) -> Int64 = "bitlogger_file_tell_i64"
|
||||
|
||||
///|
|
||||
#borrow(from_path, to_path)
|
||||
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 async fn open_file_handle_async_internal(
|
||||
path : String,
|
||||
append : Bool,
|
||||
) -> FileHandle? {
|
||||
if probe_open_file_handle_async_internal(path, append) {
|
||||
Some(new_file_handle_internal(path, append, true))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
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))
|
||||
if file_is_null_ffi(raw) {
|
||||
None
|
||||
let opened = run_async_bool_internal(() => {
|
||||
probe_open_file_handle_async_internal(path, append)
|
||||
})
|
||||
if opened {
|
||||
Some(new_file_handle_internal(path, append, true))
|
||||
} else {
|
||||
Some({ raw, path })
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn deferred_file_handle_internal(
|
||||
path : String,
|
||||
append : Bool,
|
||||
) -> FileHandle? {
|
||||
Some(new_file_handle_internal(path, append, false))
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn write_file_handle_async_internal(
|
||||
handle : FileHandle,
|
||||
content : String,
|
||||
) -> Bool {
|
||||
if handle.closed.val {
|
||||
return false
|
||||
}
|
||||
let bytes = @utf8.encode(content)
|
||||
try {
|
||||
let file = @fs.open(
|
||||
handle.path,
|
||||
mode=@fs.WriteOnly,
|
||||
append=handle.append,
|
||||
create_mode=if handle.initialized.val {
|
||||
@fs.OpenOrCreate
|
||||
} else if handle.append {
|
||||
@fs.OpenOrCreate
|
||||
} else {
|
||||
@fs.CreateOrTruncate
|
||||
},
|
||||
)
|
||||
defer file.close()
|
||||
if handle.append {
|
||||
file.write(bytes)
|
||||
} else {
|
||||
file.write_at(bytes[:], position=handle.position.val)
|
||||
handle.position.val += bytes.length().to_int64()
|
||||
}
|
||||
handle.initialized.val = true
|
||||
true
|
||||
} catch {
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn file_exists_internal(path : String) -> Bool {
|
||||
let raw = file_open_ffi(string_to_c_bytes(path), string_to_c_bytes("rb"))
|
||||
if file_is_null_ffi(raw) {
|
||||
false
|
||||
} else {
|
||||
ignore(file_close_ffi(raw))
|
||||
true
|
||||
run_async_bool_internal(() => @fs.exists(path))
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn file_exists_async_internal(path : String) -> Bool {
|
||||
@fs.exists(path) catch {
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,45 +140,102 @@ 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
|
||||
run_async_bool_internal(() => {
|
||||
write_file_handle_async_internal(handle, content)
|
||||
})
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn flush_file_handle_async_internal(handle : FileHandle) -> Bool {
|
||||
if handle.closed.val {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
let file = @fs.open(handle.path, mode=@fs.WriteOnly)
|
||||
file.sync()
|
||||
file.close()
|
||||
true
|
||||
} catch {
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn flush_file_handle_internal(handle : FileHandle) -> Bool {
|
||||
file_flush_ffi(handle.raw) == 0
|
||||
if handle.closed.val {
|
||||
false
|
||||
} else {
|
||||
// Each write owns and closes its File, so there is no buffered handle to flush here.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn close_file_handle_internal(handle : FileHandle) -> Bool {
|
||||
file_close_ffi(handle.raw) == 0
|
||||
if handle.closed.val {
|
||||
false
|
||||
} else {
|
||||
handle.closed.val = true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn file_size_i64_async_internal(handle : FileHandle) -> Int64 {
|
||||
if handle.closed.val {
|
||||
return 0L
|
||||
}
|
||||
try {
|
||||
let file = @fs.open(handle.path, mode=@fs.ReadOnly)
|
||||
let size = file.size()
|
||||
file.close()
|
||||
size
|
||||
} catch {
|
||||
_ => 0L
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn file_size_i64_internal(handle : FileHandle) -> Int64 {
|
||||
ignore(file_seek_ffi(handle.raw, 0, 2))
|
||||
let size = file_tell_i64_ffi(handle.raw)
|
||||
if size < 0L {
|
||||
0L
|
||||
} else {
|
||||
size
|
||||
run_async_i64_internal(() => file_size_i64_async_internal(handle))
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn rename_file_async_internal(
|
||||
from_path : String,
|
||||
to_path : String,
|
||||
) -> Bool {
|
||||
try {
|
||||
@fs.rename(from_path, to_path)
|
||||
true
|
||||
} catch {
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
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
|
||||
run_async_bool_internal(() => rename_file_async_internal(from_path, to_path))
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn remove_file_async_internal(path : String) -> Bool {
|
||||
try {
|
||||
@fs.remove(path)
|
||||
true
|
||||
} catch {
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn remove_file_internal(path : String) -> Bool {
|
||||
file_remove_ffi(string_to_c_bytes(path)) == 0
|
||||
run_async_bool_internal(() => remove_file_async_internal(path))
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn string_byte_length_internal(content : String) -> Int {
|
||||
string_to_c_bytes(content).length() - 1
|
||||
@utf8.encode(content).length()
|
||||
}
|
||||
|
||||
///|
|
||||
|
||||
@@ -1,28 +1,106 @@
|
||||
///|
|
||||
pub struct FileHandle {
|
||||
path : String
|
||||
priv dummy : Unit
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
|
||||
ignore(append)
|
||||
ignore(path)
|
||||
let _unused : FileHandle = { path: "" }
|
||||
let _unused : FileHandle = { dummy: () }
|
||||
ignore(_unused)
|
||||
None
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn deferred_file_handle_internal(
|
||||
path : String,
|
||||
append : Bool,
|
||||
) -> FileHandle? {
|
||||
ignore(append)
|
||||
ignore(path)
|
||||
None
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn file_exists_internal(path : String) -> Bool {
|
||||
ignore(path)
|
||||
false
|
||||
}
|
||||
|
||||
///|
|
||||
async fn stub_async_noop_internal() -> Unit {
|
||||
ignore(@fs.unimplemented)
|
||||
@async.pause()
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn open_file_handle_async_internal(
|
||||
path : String,
|
||||
append : Bool,
|
||||
) -> FileHandle? {
|
||||
stub_async_noop_internal()
|
||||
ignore(append)
|
||||
ignore(path)
|
||||
None
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn file_exists_async_internal(path : String) -> Bool {
|
||||
stub_async_noop_internal()
|
||||
ignore(path)
|
||||
false
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn write_file_handle_async_internal(
|
||||
handle : FileHandle,
|
||||
content : String,
|
||||
) -> Bool {
|
||||
stub_async_noop_internal()
|
||||
ignore(handle)
|
||||
ignore(content)
|
||||
false
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn flush_file_handle_async_internal(handle : FileHandle) -> Bool {
|
||||
stub_async_noop_internal()
|
||||
ignore(handle)
|
||||
false
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn file_size_i64_async_internal(handle : FileHandle) -> Int64 {
|
||||
stub_async_noop_internal()
|
||||
ignore(handle)
|
||||
0L
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn rename_file_async_internal(
|
||||
from_path : String,
|
||||
to_path : String,
|
||||
) -> Bool {
|
||||
stub_async_noop_internal()
|
||||
ignore(from_path)
|
||||
ignore(to_path)
|
||||
false
|
||||
}
|
||||
|
||||
///|
|
||||
pub async fn remove_file_async_internal(path : String) -> Bool {
|
||||
stub_async_noop_internal()
|
||||
ignore(path)
|
||||
false
|
||||
}
|
||||
|
||||
///|
|
||||
pub fn write_file_handle_internal(
|
||||
handle : FileHandle,
|
||||
content : String,
|
||||
) -> Bool {
|
||||
ignore(handle.dummy)
|
||||
ignore(handle)
|
||||
ignore(content)
|
||||
false
|
||||
|
||||
+3
-1
@@ -2,14 +2,16 @@ import {
|
||||
"Nanaloveyuki/BitLogger/src/core",
|
||||
"Nanaloveyuki/BitLogger/src/formatting",
|
||||
"Nanaloveyuki/BitLogger/src/record_ops",
|
||||
"moonbitlang/async",
|
||||
"moonbitlang/async/fs",
|
||||
"moonbitlang/core/env",
|
||||
"moonbitlang/core/encoding/utf8",
|
||||
"moonbitlang/core/json",
|
||||
"moonbitlang/core/ref",
|
||||
"moonbitlang/core/string",
|
||||
}
|
||||
|
||||
options(
|
||||
"native-stub": [ "stub.c" ],
|
||||
targets: {
|
||||
"file_backend_native.mbt": [ "native", "llvm" ],
|
||||
"file_backend_stub.mbt": [ "js", "wasm", "wasm-gc" ],
|
||||
|
||||
@@ -23,12 +23,20 @@ pub fn compose_patches(Array[(@core.Record) -> @core.Record]) -> (@core.Record)
|
||||
|
||||
pub fn default_style_tag_registry() -> @formatting.StyleTagRegistry
|
||||
|
||||
pub fn deferred_file_handle_internal(String, Bool) -> FileHandle?
|
||||
|
||||
pub fn field_equals(String, String) -> (@core.Record) -> Bool
|
||||
|
||||
pub async fn file_exists_async_internal(String) -> Bool
|
||||
|
||||
pub fn file_exists_internal(String) -> Bool
|
||||
|
||||
pub async fn file_size_i64_async_internal(FileHandle) -> Int64
|
||||
|
||||
pub fn file_size_i64_internal(FileHandle) -> Int64
|
||||
|
||||
pub async fn flush_file_handle_async_internal(FileHandle) -> Bool
|
||||
|
||||
pub fn flush_file_handle_internal(FileHandle) -> Bool
|
||||
|
||||
pub fn format_json(@core.Record) -> String
|
||||
@@ -49,6 +57,8 @@ pub fn native_files_supported_internal() -> Bool
|
||||
|
||||
pub fn not_((@core.Record) -> Bool) -> (@core.Record) -> Bool
|
||||
|
||||
pub async fn open_file_handle_async_internal(String, Bool) -> FileHandle?
|
||||
|
||||
pub fn open_file_handle_internal(String, Bool) -> FileHandle?
|
||||
|
||||
pub fn prefix_message(String) -> (@core.Record) -> @core.Record
|
||||
@@ -57,8 +67,12 @@ pub fn redact_field(String, placeholder? : String) -> (@core.Record) -> @core.Re
|
||||
|
||||
pub fn redact_fields(Array[String], placeholder? : String) -> (@core.Record) -> @core.Record
|
||||
|
||||
pub async fn remove_file_async_internal(String) -> Bool
|
||||
|
||||
pub fn remove_file_internal(String) -> Bool
|
||||
|
||||
pub async fn rename_file_async_internal(String, String) -> Bool
|
||||
|
||||
pub fn rename_file_internal(String, String) -> Bool
|
||||
|
||||
pub fn reset_global_style_tag_registry() -> Unit
|
||||
@@ -81,13 +95,15 @@ pub fn text_formatter(show_timestamp? : Bool, show_level? : Bool, show_target? :
|
||||
|
||||
pub fn text_style(fg? : String?, bg? : String?, bold? : Bool, dim? : Bool, italic? : Bool, underline? : Bool) -> @formatting.TextStyle
|
||||
|
||||
pub async fn write_file_handle_async_internal(FileHandle, String) -> Bool
|
||||
|
||||
pub fn write_file_handle_internal(FileHandle, String) -> Bool
|
||||
|
||||
// Errors
|
||||
|
||||
// Types and methods
|
||||
pub struct FileHandle {
|
||||
path : String
|
||||
// private fields
|
||||
}
|
||||
|
||||
// Type aliases
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef _CRT_SECURE_NO_WARNINGS
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
int32_t bitlogger_pointer_is_null(void *ptr) {
|
||||
return ptr == 0;
|
||||
}
|
||||
|
||||
void *bitlogger_file_open(const char *path, const char *mode) {
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
return fopen(path, mode);
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
}
|
||||
|
||||
int32_t bitlogger_file_write(const char *buffer, int32_t size, int32_t count, void *handle) {
|
||||
return (int32_t)fwrite(buffer, (size_t)size, (size_t)count, (FILE *)handle);
|
||||
}
|
||||
|
||||
int32_t bitlogger_file_flush(void *handle) {
|
||||
return fflush((FILE *)handle);
|
||||
}
|
||||
|
||||
int32_t bitlogger_file_close(void *handle) {
|
||||
return fclose((FILE *)handle);
|
||||
}
|
||||
|
||||
int32_t bitlogger_file_seek(void *handle, int32_t offset, int32_t origin) {
|
||||
return fseek((FILE *)handle, offset, origin);
|
||||
}
|
||||
|
||||
int64_t bitlogger_file_tell_i64(void *handle) {
|
||||
#if defined(_WIN32)
|
||||
__int64 position = _ftelli64((FILE *)handle);
|
||||
#else
|
||||
long long position = ftello((FILE *)handle);
|
||||
#endif
|
||||
return position < 0 ? -1 : (int64_t)position;
|
||||
}
|
||||
|
||||
int32_t bitlogger_file_rename(const char *from_path, const char *to_path) {
|
||||
return rename(from_path, to_path);
|
||||
}
|
||||
|
||||
int32_t bitlogger_file_remove(const char *path) {
|
||||
return remove(path);
|
||||
}
|
||||
Reference in New Issue
Block a user