📝 Document logger composition APIs

This commit is contained in:
Nanaloveyuki
2026-05-12 13:08:21 +08:00
parent 0b93af9261
commit 3dbf848a53
7 changed files with 732 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
---
name: logger-child
group: api
category: logging
update-time: 20260512
description: Derive a child logger by composing the current target with a child target segment.
key-word:
- logger
- target
- child
- public
---
## Logger-child
Create a child logger by composing the current target with another target segment. This is the standard API for hierarchical target naming such as `app.worker` or `service.http.client`.
### Interface
```moonbit
pub fn[S] Logger::child(self : Logger[S], target : String) -> Logger[S] {}
```
#### input
- `self : Logger[S]` - Parent logger whose target should be extended.
- `target : String` - Child target segment or suffix.
#### output
- `Logger[S]` - A new logger whose default target is the composed child path.
---
e.g.:
```moonbit
pub fn[S] Logger::child(self : Logger[S], target : String) -> Logger[S] {}
```
#### input
- `self : Logger[S]` - Parent logger.
- `target : String` - Child suffix.
#### output
- `Logger[S]` - Logger with combined target.
---
### Explanation
Detailed rules explaining key parameters and behaviors
- If the parent target is empty, the child target becomes the full target.
- If the child target is empty, the parent target is preserved.
- If both are non-empty, they are joined with `.`.
- Sink, min level, and timestamp settings are preserved in the returned logger.
### How to Use
Here are some specific examples provided.
#### When Need Hierarchical Target Naming
When subsystem logs should stay grouped under a shared namespace:
```moonbit
let logger = Logger::new(console_sink(), target="service")
let worker = logger.child("worker")
```
In this example, `worker` emits records under `service.worker`.
#### When Build Deeply Scoped Loggers Step By Step
When deeper target composition should remain readable:
```moonbit
let http = Logger::new(console_sink(), target="app")
.child("http")
.child("client")
```
In this example, the final logger target becomes `app.http.client`.
### Error Case
e.g.:
- If `target` is empty, the returned logger keeps the original parent target.
- If callers need complete replacement rather than composition, `with_target(...)` should be used instead.
### Notes
Notes are here.
1. This is the preferred API for hierarchical logger naming.
2. Composition uses `.` as the separator between parent and child segments.
3. The original logger is not mutated.
4. This API works well with `with_context_fields(...)` to align target and metadata scopes.
+106
View File
@@ -0,0 +1,106 @@
---
name: logger-with-filter
group: api
category: logging
update-time: 20260512
description: Wrap a logger with predicate-based filtering so only matching records reach the sink.
key-word:
- logger
- filter
- predicate
- public
---
## Logger-with-filter
Attach a `RecordPredicate` to a logger so only matching records are forwarded to the wrapped sink. This is the main API for routing-by-predicate at the logger layer without rewriting sink implementations.
### Interface
```moonbit
pub fn[S] Logger::with_filter(self : Logger[S], predicate : (Record) -> Bool) -> Logger[FilterSink[S]] {}
```
#### input
- `self : Logger[S]` - Base logger to wrap.
- `predicate : (Record) -> Bool` - Record predicate that decides whether a record should pass.
#### output
- `Logger[FilterSink[S]]` - A new logger that only forwards matching records.
---
e.g.:
```moonbit
pub fn[S] Logger::with_filter(self : Logger[S], predicate : (Record) -> Bool) -> Logger[FilterSink[S]] {}
```
#### input
- `self : Logger[S]` - Logger to filter.
- `predicate : (Record) -> Bool` - Acceptance rule.
#### output
- `Logger[FilterSink[S]]` - Logger with predicate filtering.
---
### Explanation
Detailed rules explaining key parameters and behaviors
- Filtering happens after the record is constructed, so predicates can inspect target, message, level, timestamp, and fields.
- The original logger is not mutated; a wrapped logger is returned.
- Filter logic composes naturally with helper predicates such as `target_has_prefix(...)`, `message_contains(...)`, and `all_of(...)`.
- Filtering is a drop/no-drop decision and does not transform the record itself.
### How to Use
Here are some specific examples provided.
#### When Keep Only One Target Namespace
When a logger should only emit records from a specific subsystem:
```moonbit
let logger = Logger::new(console_sink(), target="service")
.with_filter(target_has_prefix("service"))
```
In this example, records outside the `service` target prefix are filtered out.
And target-based rules stay reusable across different loggers.
#### When Combine Several Filter Rules
When filtering depends on multiple conditions:
```moonbit
let logger = Logger::new(console_sink(), target="api")
.with_filter(all_of([
level_at_least(Level::Warn),
message_contains("timeout"),
]))
```
In this example, only warning-or-higher timeout records are forwarded.
### Error Case
e.g.:
- If the predicate always returns `false`, the logger silently drops every record.
- If the predicate always returns `true`, the wrapper behaves like a pass-through filter.
### Notes
Notes are here.
1. Use this API for selection logic, not record mutation.
2. Prefer helper predicates for readability and reuse.
3. Filtering occurs before sink write, so it is cheaper than post-processing output.
4. If you need record modification too, combine this API with `with_patch(...)`.
+101
View File
@@ -0,0 +1,101 @@
---
name: logger-with-min-level
group: api
category: logging
update-time: 20260512
description: Replace the logger minimum enabled level so lower-severity records are skipped earlier.
key-word:
- logger
- level
- filter
- public
---
## Logger-with-min-level
Replace the logger's minimum enabled level. This API controls the first gate checked by `log(...)` and the convenience level methods.
### Interface
```moonbit
pub fn[S] Logger::with_min_level(self : Logger[S], min_level : Level) -> Logger[S] {}
```
#### input
- `self : Logger[S]` - Base logger whose level threshold should change.
- `min_level : Level` - New minimum enabled level.
#### output
- `Logger[S]` - A new logger value carrying the updated threshold.
---
e.g.:
```moonbit
pub fn[S] Logger::with_min_level(self : Logger[S], min_level : Level) -> Logger[S] {}
```
#### input
- `self : Logger[S]` - Current logger.
- `min_level : Level` - New threshold.
#### output
- `Logger[S]` - Level-updated logger.
---
### Explanation
Detailed rules explaining key parameters and behaviors
- `log(...)` checks `is_enabled(level)` before constructing and writing a record.
- Lower-severity records below `min_level` are skipped without reaching the sink.
- This API replaces the logger threshold and does not add a wrapper sink.
- The returned logger keeps the same sink, target, and timestamp settings.
### How to Use
Here are some specific examples provided.
#### When Raise Noise Floor In Production
When only warning and error records should be emitted:
```moonbit
let logger = Logger::new(console_sink())
.with_min_level(Level::Warn)
```
In this example, `trace`, `debug`, and `info` calls are skipped.
#### When Derive A More Verbose Local Logger
When one branch of code should keep a different threshold:
```moonbit
let base = Logger::new(console_sink(), min_level=Level::Info)
let debug_logger = base.with_min_level(Level::Debug)
```
In this example, the sink is reused while the threshold changes per logger value.
### Error Case
e.g.:
- If `min_level` is set too high, expected lower-severity diagnostics may disappear.
- If callers need richer predicate logic than a simple threshold, `with_filter(...)` should be used instead.
### Notes
Notes are here.
1. This API is the cheapest built-in severity gate.
2. Use it before adding more complex filtering rules.
3. It composes cleanly with target, patch, and context-field helpers.
4. The original logger is unchanged because a new value is returned.
+106
View File
@@ -0,0 +1,106 @@
---
name: logger-with-patch
group: api
category: logging
update-time: 20260512
description: Wrap a logger with record transformation logic before the record reaches the sink.
key-word:
- logger
- patch
- transform
- public
---
## Logger-with-patch
Attach a `RecordPatch` to a logger so each record is transformed before it is written to the sink. This API is the standard way to rewrite target, message, or fields without changing caller code.
### Interface
```moonbit
pub fn[S] Logger::with_patch(self : Logger[S], patch : RecordPatch) -> Logger[PatchSink[S]] {}
```
#### input
- `self : Logger[S]` - Base logger to wrap.
- `patch : RecordPatch` - Record-to-record transformer applied before writing.
#### output
- `Logger[PatchSink[S]]` - A new logger that rewrites emitted records.
---
e.g.:
```moonbit
pub fn[S] Logger::with_patch(self : Logger[S], patch : RecordPatch) -> Logger[PatchSink[S]] {}
```
#### input
- `self : Logger[S]` - Logger to transform.
- `patch : RecordPatch` - Rewrite logic.
#### output
- `Logger[PatchSink[S]]` - Logger with transformation behavior.
---
### Explanation
Detailed rules explaining key parameters and behaviors
- Patches can rewrite `target`, `message`, and `fields` because they receive the full `Record`.
- This API does not change logging level gating; level checks still happen at the logger level.
- `compose_patches(...)` can be used to build ordered patch pipelines.
- This is the correct place for redaction, enrichment, and message prefixing.
### How to Use
Here are some specific examples provided.
#### When Need Redaction Before Output
When sensitive data must be rewritten before reaching any sink:
```moonbit
let logger = Logger::new(console_sink(), target="auth")
.with_patch(redact_fields(["token", "password"]))
```
In this example, matching field values are replaced before the sink sees them.
And caller code does not need custom redaction logic per log call.
#### When Need Combined Enrichment And Message Rewrite
When you want several transformations in a stable order:
```moonbit
let logger = Logger::new(console_sink(), target="svc")
.with_patch(compose_patches([
prefix_message("[safe] "),
append_fields([field("service", "svc")]),
]))
```
In this example, patch order is explicit and deterministic.
### Error Case
e.g.:
- If a patch returns the original record unchanged, the wrapper behaves like a pass-through transformer.
- If multiple patches rewrite the same field or property, the later patch wins according to composition order.
### Notes
Notes are here.
1. Use patches for transformation, not filtering decisions.
2. Prefer helper patches such as `prefix_message(...)`, `append_fields(...)`, and `redact_fields(...)` for common cases.
3. Patch composition order matters.
4. Combine with `with_filter(...)` when both selection and rewriting are required.
+112
View File
@@ -0,0 +1,112 @@
---
name: logger-with-queue
group: api
category: logging
update-time: 20260512
description: Wrap a synchronous logger with an explicit bounded queue and overflow policy.
key-word:
- logger
- queue
- buffering
- public
---
## Logger-with-queue
Wrap a logger with a `QueuedSink[S]` so records are first stored in an explicit queue and later drained to the wrapped sink. This API is useful when you want bounded backlog behavior without introducing the async runtime adapter package.
### Interface
```moonbit
pub fn[S] Logger::with_queue(
self : Logger[S],
max_pending~ : Int = 0,
overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest,
) -> Logger[QueuedSink[S]] {}
```
#### input
- `self : Logger[S]` - Base logger to wrap.
- `max_pending : Int` - Maximum queued records before overflow behavior is applied.
- `overflow : QueueOverflowPolicy` - Queue overflow strategy such as `DropNewest` or `DropOldest`.
#### output
- `Logger[QueuedSink[S]]` - A logger using an explicit synchronous queue wrapper.
---
e.g.:
```moonbit
pub fn[S] Logger::with_queue(self : Logger[S], max_pending~ : Int = 0, overflow~ : QueueOverflowPolicy = QueueOverflowPolicy::DropNewest) -> Logger[QueuedSink[S]] {}
```
#### input
- `max_pending : Int` - Queue bound.
- `overflow : QueueOverflowPolicy` - Backlog overflow behavior.
#### output
- `Logger[QueuedSink[S]]` - Queue-wrapped logger.
---
### Explanation
Detailed rules explaining key parameters and behaviors
- This queue wrapper is synchronous and explicit. It is not the same as `bitlogger_async` runtime scheduling.
- Callers can inspect `pending_count()` and `dropped_count()` through the wrapped sink.
- `flush()` or `drain(...)` is required to move queued data to the underlying sink.
- Overflow policy determines whether new or old records are discarded when the queue is full.
### How to Use
Here are some specific examples provided.
#### When Need Explicit Manual Drain
When records should accumulate and flush at controlled points:
```moonbit
let logger = Logger::new(console_sink(), target="queue")
.with_queue(max_pending=2, overflow=QueueOverflowPolicy::DropOldest)
logger.info("one")
logger.info("two")
ignore(logger.sink.flush())
```
In this example, records stay queued until explicitly flushed.
And backlog size stays bounded.
#### When Need Bounded Overload Behavior
When you need defined behavior under burst load:
```moonbit
let logger = Logger::new(console_sink())
.with_queue(max_pending=100, overflow=QueueOverflowPolicy::DropNewest)
```
In this example, queue pressure is limited instead of growing without bound.
### Error Case
e.g.:
- If `max_pending` is too small, records may be dropped frequently under bursts.
- If callers never flush or drain the queue, queued records remain pending and do not reach the sink.
### Notes
Notes are here.
1. Use this API when you want explicit bounded buffering without `bitlogger_async`.
2. `with_queue(...)` preserves the normal synchronous logger call style.
3. Always define when and where the queue will be drained.
4. For background queue draining, prefer `bitlogger_async` instead.
+104
View File
@@ -0,0 +1,104 @@
---
name: logger-with-target
group: api
category: logging
update-time: 20260512
description: Replace the default target carried by a logger so later records inherit a new target namespace.
key-word:
- logger
- target
- namespace
- public
---
## Logger-with-target
Replace the logger's default target. This API is the simplest way to retarget a logger instance so later log calls inherit a new namespace without passing `target?` on every call.
### Interface
```moonbit
pub fn[S] Logger::with_target(self : Logger[S], target : String) -> Logger[S] {}
```
#### input
- `self : Logger[S]` - Base logger whose default target should be replaced.
- `target : String` - New default target namespace.
#### output
- `Logger[S]` - A new logger value carrying the updated target.
---
e.g.:
```moonbit
pub fn[S] Logger::with_target(self : Logger[S], target : String) -> Logger[S] {}
```
#### input
- `self : Logger[S]` - Current logger.
- `target : String` - Replacement target.
#### output
- `Logger[S]` - Retargeted logger.
---
### Explanation
Detailed rules explaining key parameters and behaviors
- The returned logger keeps the same sink, min level, and timestamp settings.
- This API replaces the default target instead of composing it.
- Per-call `target?` arguments can still override the default target on individual log calls.
- The original logger value is not mutated.
### How to Use
Here are some specific examples provided.
#### When Need A Stable Namespace For One Subsystem
When one logger should always emit under a fixed target:
```moonbit
let logger = Logger::new(console_sink())
.with_target("service.auth")
logger.info("started")
```
In this example, later records inherit `service.auth` unless a call overrides the target explicitly.
#### When Reuse One Sink Across Different Namespaces
When multiple subsystem loggers share the same sink:
```moonbit
let base = Logger::new(console_sink())
let worker = base.with_target("worker")
let api = base.with_target("api")
```
In this example, target routing stays explicit without duplicating sink construction.
### Error Case
e.g.:
- If `target` is empty, the logger remains valid and later records default to an empty target.
- If callers actually need parent-child target composition, `child(...)` is the better API.
### Notes
Notes are here.
1. Use this API for replacement, not hierarchical composition.
2. It is useful when one sink serves several target namespaces.
3. Per-call `target?` still has higher priority for that one log record.
4. Pair it with `child(...)` when both replacement and composition are needed in different stages.
+101
View File
@@ -0,0 +1,101 @@
---
name: logger-with-timestamp
group: api
category: logging
update-time: 20260512
description: Enable or disable automatic timestamp capture for records emitted by a logger.
key-word:
- logger
- timestamp
- record
- public
---
## Logger-with-timestamp
Enable or disable automatic timestamp capture on log emission. This API controls whether `Logger::log(...)` records the current time through `@env.now()` or leaves the timestamp at `0UL`.
### Interface
```moonbit
pub fn[S] Logger::with_timestamp(self : Logger[S], enabled~ : Bool = true) -> Logger[S] {}
```
#### input
- `self : Logger[S]` - Base logger whose timestamp behavior should change.
- `enabled : Bool` - Whether emitted records should capture current time automatically.
#### output
- `Logger[S]` - A new logger value with updated timestamp behavior.
---
e.g.:
```moonbit
pub fn[S] Logger::with_timestamp(self : Logger[S], enabled~ : Bool = true) -> Logger[S] {}
```
#### input
- `self : Logger[S]` - Current logger.
- `enabled : Bool` - Timestamp toggle.
#### output
- `Logger[S]` - Logger with updated timestamp behavior.
---
### Explanation
Detailed rules explaining key parameters and behaviors
- When enabled, `log(...)` captures `@env.now()` and stores it in the record.
- When disabled, emitted records use `0UL` as the timestamp value.
- This setting affects later emitted records only.
- Formatter or sink behavior stays unchanged; they just receive records with or without real timestamps.
### How to Use
Here are some specific examples provided.
#### When Need Real Event Time In Records
When downstream formatting or JSON output should include event time:
```moonbit
let logger = Logger::new(console_sink())
.with_timestamp()
```
In this example, each emitted record captures current time automatically.
#### When Need Deterministic Or Minimal Records
When timestamps should be disabled for tests or reduced output:
```moonbit
let logger = Logger::new(console_sink())
.with_timestamp(enabled=false)
```
In this example, records are emitted without runtime time capture.
### Error Case
e.g.:
- If timestamps are disabled, formatters that expect meaningful time values may show empty or zero-like timestamp output.
- If callers need timestamps only for one record, per-call customization is not provided here and a separate logger value may be clearer.
### Notes
Notes are here.
1. This API controls record creation, not formatter display policy.
2. It works well together with text formatters that optionally show timestamps.
3. The helper is useful for tests, deterministic snapshots, and production timing.
4. The returned logger preserves the same sink, target, and min level settings.