From dd895ac2118aaed773cdece4ef85b05ebebd1fb3 Mon Sep 17 00:00:00 2001 From: Nanaloveyuki Date: Tue, 12 May 2026 15:56:30 +0800 Subject: [PATCH] :memo: Add filter predicate API docs --- docs/api/all-of.md | 82 ++++++++++++++++++++++++++++++++++++ docs/api/any-of.md | 82 ++++++++++++++++++++++++++++++++++++ docs/api/field-equals.md | 80 +++++++++++++++++++++++++++++++++++ docs/api/has-field.md | 79 ++++++++++++++++++++++++++++++++++ docs/api/index.md | 8 ++++ docs/api/level-at-least.md | 79 ++++++++++++++++++++++++++++++++++ docs/api/message-contains.md | 79 ++++++++++++++++++++++++++++++++++ docs/api/not.md | 76 +++++++++++++++++++++++++++++++++ docs/api/target-is.md | 79 ++++++++++++++++++++++++++++++++++ 9 files changed, 644 insertions(+) create mode 100644 docs/api/all-of.md create mode 100644 docs/api/any-of.md create mode 100644 docs/api/field-equals.md create mode 100644 docs/api/has-field.md create mode 100644 docs/api/level-at-least.md create mode 100644 docs/api/message-contains.md create mode 100644 docs/api/not.md create mode 100644 docs/api/target-is.md diff --git a/docs/api/all-of.md b/docs/api/all-of.md new file mode 100644 index 0000000..6557d6c --- /dev/null +++ b/docs/api/all-of.md @@ -0,0 +1,82 @@ +--- +name: all-of +group: api +category: filtering +update-time: 20260512 +description: Create a reusable record predicate that requires every nested predicate to pass. +key-word: + - combine + - filter + - predicate + - public +--- + +## All-of + +Create a `RecordPredicate` that returns `true` only when every predicate in the array returns `true`. This helper is the standard way to build strict multi-condition filters. + +### Interface + +```moonbit +pub fn all_of(predicates : Array[RecordPredicate]) -> RecordPredicate {} +``` + +#### input + +- `predicates : Array[RecordPredicate]` - Predicates that must all succeed for a record to match. + +#### output + +- `RecordPredicate` - Predicate that returns `true` only when every nested predicate returns `true`. + +### Explanation + +Detailed rules explaining key parameters and behaviors + +- Predicates are evaluated in array order. +- Evaluation stops early on the first predicate that returns `false`. +- If the array is empty, the combined predicate returns `true` because no condition failed. +- This helper is ideal for combining namespace, level, and field requirements into one reusable rule. + +### How to Use + +Here are some specific examples provided. + +#### When Require Several Conditions + +When routing should be both target- and level-aware: +```moonbit +let predicate = all_of([ + target_has_prefix("service.api"), + level_at_least(Level::Warn), +]) +``` + +In this example, records must satisfy both conditions before they pass. + +#### When Add Field Constraints + +When only contextual failures should remain: +```moonbit +let predicate = all_of([ + message_contains("failed"), + has_field("request_id"), + not_(field_equals("tenant", "internal")), +]) +``` + +In this example, the filter stays readable even though the rule has several parts. + +### Error Case + +e.g.: +- If `predicates` is empty, the returned predicate always evaluates to `true`. + +- If one nested predicate is too strict, the whole combination may reject more records than expected. + +### Notes + +1. Put the cheapest or most selective predicates earlier when evaluation cost matters. + +2. `all_of(...)` is usually easier to maintain than a custom inline predicate closure. + diff --git a/docs/api/any-of.md b/docs/api/any-of.md new file mode 100644 index 0000000..69b6246 --- /dev/null +++ b/docs/api/any-of.md @@ -0,0 +1,82 @@ +--- +name: any-of +group: api +category: filtering +update-time: 20260512 +description: Create a reusable record predicate that passes when any nested predicate matches. +key-word: + - combine + - filter + - predicate + - public +--- + +## Any-of + +Create a `RecordPredicate` that returns `true` when at least one predicate in the array returns `true`. This helper is useful for routing several independent cases through the same path. + +### Interface + +```moonbit +pub fn any_of(predicates : Array[RecordPredicate]) -> RecordPredicate {} +``` + +#### input + +- `predicates : Array[RecordPredicate]` - Predicates where any successful match should admit the record. + +#### output + +- `RecordPredicate` - Predicate that returns `true` when at least one nested predicate returns `true`. + +### Explanation + +Detailed rules explaining key parameters and behaviors + +- Predicates are evaluated in array order. +- Evaluation stops early on the first predicate that returns `true`. +- If the array is empty, the combined predicate returns `false` because no predicate matched. +- This helper is useful when several targets, levels, or field signatures should share one sink. + +### How to Use + +Here are some specific examples provided. + +#### When Accept Several Target Paths + +When multiple subsystems should share one route: +```moonbit +let predicate = any_of([ + target_is("audit"), + target_has_prefix("security"), +]) +``` + +In this example, either matching branch is enough for the record to pass. + +#### When Combine Different Diagnostic Conditions + +When several independent signals are interesting: +```moonbit +let predicate = any_of([ + level_at_least(Level::Error), + message_contains("timeout"), + field_equals("retryable", "true"), +]) +``` + +In this example, one satisfied condition is enough to keep the record visible. + +### Error Case + +e.g.: +- If `predicates` is empty, the returned predicate always evaluates to `false`. + +- If one nested predicate is too broad, it may shadow the intent of the other branches. + +### Notes + +1. Put the most common or cheapest success path earlier when evaluation cost matters. + +2. Use `any_of(...)` when a single sink should accept multiple independent match patterns. + diff --git a/docs/api/field-equals.md b/docs/api/field-equals.md new file mode 100644 index 0000000..b93a70e --- /dev/null +++ b/docs/api/field-equals.md @@ -0,0 +1,80 @@ +--- +name: field-equals +group: api +category: filtering +update-time: 20260512 +description: Create a reusable record predicate that matches a field by exact key and value. +key-word: + - field + - filter + - predicate + - public +--- + +## Field-equals + +Create a `RecordPredicate` that returns `true` when a record contains a field whose key and value both match exactly. Use it for stable attribute-based routing. + +### Interface + +```moonbit +pub fn field_equals(key : String, value : String) -> RecordPredicate {} +``` + +#### input + +- `key : String` - Field key to inspect. +- `value : String` - Exact value expected for that key. + +#### output + +- `RecordPredicate` - Predicate that matches records containing a field with the expected key and value. + +### Explanation + +Detailed rules explaining key parameters and behaviors + +- Matching requires both `field.key == key` and `field.value == value`. +- The predicate returns `true` on the first matching field. +- This helper is useful for routing records by environment, tenant, operation name, or fixed tags. +- It is stricter than `has_field(...)` because presence alone is not enough. + +### How to Use + +Here are some specific examples provided. + +#### When Keep One Tenant Stream + +When log routing should isolate one tenant: +```moonbit +let logger = Logger::new(console_sink()) + .with_filter(field_equals("tenant", "acme")) +``` + +In this example, records for other tenants are excluded. + +#### When Combine With Exact Target Matching + +When field and target must both match: +```moonbit +let predicate = all_of([ + target_is("billing"), + field_equals("region", "cn"), +]) +``` + +In this example, only billing records tagged for the `cn` region remain. + +### Error Case + +e.g.: +- If `key` or `value` is empty, matching still uses exact equality and may produce no results unless records contain the same empty string. + +- If a record contains the key with multiple values, any one exact match is enough for the predicate to return `true`. + +### Notes + +1. Prefer exact field matching over message substring matching for long-term routing rules. + +2. Keep field naming stable across producers if this predicate is reused in shared configs. + diff --git a/docs/api/has-field.md b/docs/api/has-field.md new file mode 100644 index 0000000..e8ada1b --- /dev/null +++ b/docs/api/has-field.md @@ -0,0 +1,79 @@ +--- +name: has-field +group: api +category: filtering +update-time: 20260512 +description: Create a reusable record predicate that checks whether a field key exists. +key-word: + - field + - filter + - predicate + - public +--- + +## Has-field + +Create a `RecordPredicate` that returns `true` when a record contains at least one field with the given key. Use it when presence alone matters more than a specific field value. + +### Interface + +```moonbit +pub fn has_field(key : String) -> RecordPredicate {} +``` + +#### input + +- `key : String` - Field key that should exist in the record. + +#### output + +- `RecordPredicate` - Predicate that matches records containing a field whose key equals `key`. + +### Explanation + +Detailed rules explaining key parameters and behaviors + +- The predicate scans `rec.fields` in order and stops on the first matching key. +- Only the key is checked; the field value is ignored. +- This helper works well for optional metadata such as `request_id`, `tenant`, or `trace_id`. +- It can be combined with `field_equals(...)` when some records need stricter field matching. + +### How to Use + +Here are some specific examples provided. + +#### When Keep Records Carrying Correlation Data + +When only records with tracing context should pass: +```moonbit +let logger = Logger::new(console_sink()) + .with_filter(has_field("trace_id")) +``` + +In this example, records without `trace_id` are filtered out. + +#### When Combine With Severity Rules + +When contextual records should also meet a level threshold: +```moonbit +let predicate = all_of([ + has_field("request_id"), + level_at_least(Level::Warn), +]) +``` + +In this example, only warning-or-higher records with request context remain. + +### Error Case + +e.g.: +- If `key` is empty, the predicate only matches fields whose key is also empty. + +- If the same key appears multiple times, the predicate still returns `true` after the first match. + +### Notes + +1. Use this helper when field presence is meaningful even if the actual value changes every time. + +2. Field ordering does not change the result, only the early return cost. + diff --git a/docs/api/index.md b/docs/api/index.md index 2b60255..8355f3d 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -51,7 +51,15 @@ BitLogger API navigation. ## Predicates and helpers +- [level-at-least.md](./level-at-least.md) +- [target-is.md](./target-is.md) - [target-has-prefix.md](./target-has-prefix.md) +- [message-contains.md](./message-contains.md) +- [has-field.md](./has-field.md) +- [field-equals.md](./field-equals.md) +- [not.md](./not.md) +- [all-of.md](./all-of.md) +- [any-of.md](./any-of.md) ## Config build flow diff --git a/docs/api/level-at-least.md b/docs/api/level-at-least.md new file mode 100644 index 0000000..ccbcfa0 --- /dev/null +++ b/docs/api/level-at-least.md @@ -0,0 +1,79 @@ +--- +name: level-at-least +group: api +category: filtering +update-time: 20260512 +description: Create a reusable record predicate that keeps records at or above a minimum level. +key-word: + - level + - filter + - predicate + - public +--- + +## Level-at-least + +Create a `RecordPredicate` that returns `true` when a record level is greater than or equal to a minimum threshold. This is the main helper for severity-based filtering. + +### Interface + +```moonbit +pub fn level_at_least(min_level : Level) -> RecordPredicate {} +``` + +#### input + +- `min_level : Level` - The lowest level that should remain enabled. + +#### output + +- `RecordPredicate` - Predicate that keeps records whose `priority()` is at least the given minimum. + +### Explanation + +Detailed rules explaining key parameters and behaviors + +- Matching is based on `Level::priority()`, not string comparison. +- Records at the exact same level as `min_level` are included. +- This helper is useful when a sink or child logger needs a stricter threshold than the parent logger. +- It composes cleanly with `all_of(...)` and `any_of(...)`. + +### How to Use + +Here are some specific examples provided. + +#### When Keep Warnings And Errors + +When a logger should keep only important records: +```moonbit +let logger = Logger::new(console_sink()) + .with_filter(level_at_least(Level::Warn)) +``` + +In this example, `Warn` and `Error` records continue through the filter. + +#### When Combine With Target Routing + +When both severity and namespace matter: +```moonbit +let predicate = all_of([ + target_has_prefix("service.api"), + level_at_least(Level::Info), +]) +``` + +In this example, the predicate can be reused by multiple loggers or sinks. + +### Error Case + +e.g.: +- If `min_level` is `Level::Trace`, the predicate effectively allows all normal log records. + +- If `min_level` is higher than the record level, the predicate returns `false` without modifying the record. + +### Notes + +1. Use this helper when you want filtering logic to remain explicit instead of embedding level checks inline. + +2. This predicate is separate from a logger's own `min_level`, so both can be combined. + diff --git a/docs/api/message-contains.md b/docs/api/message-contains.md new file mode 100644 index 0000000..901511c --- /dev/null +++ b/docs/api/message-contains.md @@ -0,0 +1,79 @@ +--- +name: message-contains +group: api +category: filtering +update-time: 20260512 +description: Create a reusable record predicate that matches records by message fragment. +key-word: + - message + - filter + - predicate + - public +--- + +## Message-contains + +Create a `RecordPredicate` that returns `true` when a log message contains the given fragment. This helper is mainly useful for diagnostics, temporary routing, and focused debugging. + +### Interface + +```moonbit +pub fn message_contains(fragment : String) -> RecordPredicate {} +``` + +#### input + +- `fragment : String` - Substring expected to appear in `rec.message`. + +#### output + +- `RecordPredicate` - Predicate that matches records whose message contains the fragment. + +### Explanation + +Detailed rules explaining key parameters and behaviors + +- Matching uses `String::contains(...)` on the full message text. +- The predicate is case-sensitive unless the message was normalized earlier. +- This helper is convenient for ad hoc triage when targets or fields are not enough. +- It should usually complement more stable filters rather than replace them in long-term configs. + +### How to Use + +Here are some specific examples provided. + +#### When Watch A Temporary Error Pattern + +When a debugging session focuses on one phrase: +```moonbit +let logger = Logger::new(console_sink()) + .with_filter(message_contains("retry failed")) +``` + +In this example, only messages carrying that fragment remain visible. + +#### When Combine With Target Filtering + +When the same message should be isolated inside one subsystem: +```moonbit +let predicate = all_of([ + target_has_prefix("worker.sync"), + message_contains("timeout"), +]) +``` + +In this example, unrelated timeout messages from other targets are ignored. + +### Error Case + +e.g.: +- If `fragment` is empty, the predicate effectively matches every message because every string contains an empty substring. + +- If messages are localized or reformatted frequently, fragment-based matching can become brittle. + +### Notes + +1. Prefer field- or target-based filtering for stable production rules. + +2. Message substring filters are most valuable during debugging and migration periods. + diff --git a/docs/api/not.md b/docs/api/not.md new file mode 100644 index 0000000..6c4f17b --- /dev/null +++ b/docs/api/not.md @@ -0,0 +1,76 @@ +--- +name: not +group: api +category: filtering +update-time: 20260512 +description: Create a reusable record predicate that negates another predicate. +key-word: + - negate + - filter + - predicate + - public +--- + +## Not + +Create a `RecordPredicate` that returns the logical inverse of another predicate. This helper is the standard way to express exclusion rules without rewriting predicate bodies. + +### Interface + +```moonbit +pub fn not_(predicate : RecordPredicate) -> RecordPredicate {} +``` + +#### input + +- `predicate : RecordPredicate` - Existing predicate to invert. + +#### output + +- `RecordPredicate` - Predicate that returns `true` when the original predicate returns `false`. + +### Explanation + +Detailed rules explaining key parameters and behaviors + +- `not_(...)` delegates evaluation to the original predicate and flips the boolean result. +- It does not change the record or short-circuit upstream logic outside the wrapped predicate. +- This helper is useful for excluding known noisy targets, fields, or message patterns. +- It composes well with `all_of(...)` and `any_of(...)` for more expressive routing rules. + +### How to Use + +Here are some specific examples provided. + +#### When Exclude One Target Prefix + +When everything except one noisy subsystem should pass: +```moonbit +let logger = Logger::new(console_sink()) + .with_filter(not_(target_has_prefix("debug.spam"))) +``` + +In this example, records under `debug.spam...` are filtered out. + +#### When Invert A Field Match + +When one tenant should be excluded from a shared stream: +```moonbit +let predicate = not_(field_equals("tenant", "internal")) +``` + +In this example, all non-internal tenant records continue through the filter. + +### Error Case + +e.g.: +- If the wrapped predicate always returns `true`, the negated predicate always returns `false`. + +- If the wrapped predicate is too broad, the inversion may unintentionally admit unrelated records. + +### Notes + +1. Prefer `not_(...)` over duplicating inverse logic inline because the intent is easier to read. + +2. Negation is most maintainable when the wrapped predicate is itself narrow and well named. + diff --git a/docs/api/target-is.md b/docs/api/target-is.md new file mode 100644 index 0000000..1dc882f --- /dev/null +++ b/docs/api/target-is.md @@ -0,0 +1,79 @@ +--- +name: target-is +group: api +category: filtering +update-time: 20260512 +description: Create a reusable record predicate that matches a single exact target. +key-word: + - target + - filter + - predicate + - public +--- + +## Target-is + +Create a `RecordPredicate` that returns `true` only when a record target exactly matches the expected string. Use it when one specific logical source should pass through a filter. + +### Interface + +```moonbit +pub fn target_is(target : String) -> RecordPredicate {} +``` + +#### input + +- `target : String` - Exact target value expected from the record. + +#### output + +- `RecordPredicate` - Predicate that matches records whose `rec.target` equals `target`. + +### Explanation + +Detailed rules explaining key parameters and behaviors + +- Matching uses direct string equality. +- This helper does not perform prefix or fuzzy matching. +- It is useful for isolating one subsystem when targets are intentionally stable. +- Combine it with level predicates when a single target needs stricter routing rules. + +### How to Use + +Here are some specific examples provided. + +#### When Keep One Exact Target + +When only one service target should pass: +```moonbit +let logger = Logger::new(console_sink()) + .with_filter(target_is("gateway.http")) +``` + +In this example, records from sibling targets such as `gateway.ws` are excluded. + +#### When Route A Single Target To A Sink + +When a split path needs an exact match: +```moonbit +let predicate = all_of([ + target_is("audit"), + level_at_least(Level::Info), +]) +``` + +In this example, the filter remains predictable because it does not rely on naming prefixes. + +### Error Case + +e.g.: +- If `target` is empty, only records whose target is also an empty string will match. + +- If record targets are generated dynamically, exact matching may be too strict and lead to no matches. + +### Notes + +1. Prefer `target_has_prefix(...)` when targets are hierarchical and you want subtree matching. + +2. Exact target filters are easiest to maintain when target naming is stable across the project. +