Use case
When using LogFox with an app that derives the log tag from the calling class (Timber's DebugTree, SLF4J-android, etc.), a single logical class produces multiple distinct tags depending on whether the log call lives in a nested function or lambda.
Example from a real project: SyncRepository produces
SyncRepository
SyncRepository$fullSync
SyncRepository$deltaSync
To watch the whole class today I need three separate filter rows, and the list grows whenever a new helper function appears.
Current behavior
UserFilter.tag is matched by exact string equality:
https://github.com/F0x1d/LogFox/blob/master/feature/filters/api/src/main/kotlin/com/f0x1d/logfox/feature/filters/api/model/LogLinesExt.kt#L52-L60
private fun String?.equalsOrTrueIfNull(other: String) =
if (this == null) true else other == this
This means "Sync" never matches "SyncRepository", and "SyncRepository" never matches "SyncRepository$fullSync".
Proposal
Borrow the convention used by VSCode, JetBrains IDEs, and most desktop find/replace UIs: a single Regex toggle per filter rule. Behavior:
- Regex off (default for new filters): the field is treated as a literal substring (
contains), not exact equality. This matches what users expect from a text search box.
- Regex on: the field is compiled as a
Regex and matched against the value.
Existing user impact
This is intentionally a zero-regression migration — every existing filter keeps matching exactly the same set of log lines after the upgrade. The new behavior is only visible to filters created or edited after the change. Details in the Migration section below.
API sketch
data class UserFilter(
...
val tag: String? = null,
val tagIsRegex: Boolean = false,
val content: String? = null,
val contentIsRegex: Boolean = false,
...
) {
// Compiled once per filter instance; null when tag is null, blank, or
// not in regex mode, OR when the pattern is invalid (see below).
@Transient
val tagRegex: Regex? by lazy {
if (tagIsRegex && !tag.isNullOrEmpty()) {
runCatching { tag.toRegex() }.getOrNull()
} else null
}
}
private fun UserFilter.tagSuits(other: String) = when {
tag == null -> true
tagIsRegex -> tagRegex?.containsMatchIn(other) ?: false // bad regex => no match
else -> other.contains(tag)
}
A few things worth calling out:
- Compile once, not per log line. Log lines arrive in the thousands; compiling
tag.toRegex() on every match would burn GC. The Regex instance is cached on the UserFilter (or in a tiny LRU keyed by filter id, depending on what fits the existing architecture better).
- Invalid regex handling.
tag.toRegex() can throw PatternSyntaxException. The edit screen should validate the pattern as the user types and refuse to save with a clear error; even so, the matching path wraps the compile in runCatching so a malformed pattern that somehow survives just makes the filter match nothing instead of crashing the log pipeline.
- The toggle UI can be a small
[.*] button next to the Tag field, similar to the existing search bar.
Migration
Switching the default from exact-equality to contains is a behavior change, so existing filters need to be migrated to preserve their meaning. Two options:
-
One-shot DB migration to regex mode with escaped patterns. For every existing non-null tag, set tag = "^" + Regex.escape(tag) + "$" and tagIsRegex = true. This reproduces the current exact-equality semantics exactly, including for tags that contain regex metacharacters like $, ., (, etc. (the Regex.escape step is critical — a tag like SyncRepository$fullSync cannot just be wrapped with ^...$ because the $ would be interpreted as an end-of-line anchor and the filter would silently break).
-
Keep an internal EXACT_LEGACY mode for pre-migration filters, where matching still uses ==. New filters use the new model. Slightly more code paths to maintain but no rewriting of existing rows.
I'd lean toward option 1 — one DB migration, one mental model going forward. Either way, the user-visible behavior of existing filters does not change.
Why
- One filter row instead of N for
SyncRepository* style auto-derived tags
- Matches the search-toggle UX users already know from IDEs and editors
- Same toggle naturally extends to
content (which already contains-matches) and package if you want to keep them symmetric, but those can come in follow-up PRs
Happy to send a PR if this direction is acceptable.
🤖 Generated with Claude Code
Use case
When using LogFox with an app that derives the log tag from the calling class (Timber's
DebugTree, SLF4J-android, etc.), a single logical class produces multiple distinct tags depending on whether the log call lives in a nested function or lambda.Example from a real project:
SyncRepositoryproducesTo watch the whole class today I need three separate filter rows, and the list grows whenever a new helper function appears.
Current behavior
UserFilter.tagis matched by exact string equality:https://github.com/F0x1d/LogFox/blob/master/feature/filters/api/src/main/kotlin/com/f0x1d/logfox/feature/filters/api/model/LogLinesExt.kt#L52-L60
This means "Sync" never matches "SyncRepository", and "SyncRepository" never matches "SyncRepository$fullSync".
Proposal
Borrow the convention used by VSCode, JetBrains IDEs, and most desktop find/replace UIs: a single Regex toggle per filter rule. Behavior:
contains), not exact equality. This matches what users expect from a text search box.Regexand matched against the value.Existing user impact
This is intentionally a zero-regression migration — every existing filter keeps matching exactly the same set of log lines after the upgrade. The new behavior is only visible to filters created or edited after the change. Details in the Migration section below.
API sketch
A few things worth calling out:
tag.toRegex()on every match would burn GC. TheRegexinstance is cached on theUserFilter(or in a tiny LRU keyed by filter id, depending on what fits the existing architecture better).tag.toRegex()can throwPatternSyntaxException. The edit screen should validate the pattern as the user types and refuse to save with a clear error; even so, the matching path wraps the compile inrunCatchingso a malformed pattern that somehow survives just makes the filter match nothing instead of crashing the log pipeline.[.*]button next to the Tag field, similar to the existing search bar.Migration
Switching the default from exact-equality to
containsis a behavior change, so existing filters need to be migrated to preserve their meaning. Two options:One-shot DB migration to regex mode with escaped patterns. For every existing non-null
tag, settag = "^" + Regex.escape(tag) + "$"andtagIsRegex = true. This reproduces the current exact-equality semantics exactly, including for tags that contain regex metacharacters like$,.,(, etc. (theRegex.escapestep is critical — a tag likeSyncRepository$fullSynccannot just be wrapped with^...$because the$would be interpreted as an end-of-line anchor and the filter would silently break).Keep an internal
EXACT_LEGACYmode for pre-migration filters, where matching still uses==. New filters use the new model. Slightly more code paths to maintain but no rewriting of existing rows.I'd lean toward option 1 — one DB migration, one mental model going forward. Either way, the user-visible behavior of existing filters does not change.
Why
SyncRepository*style auto-derived tagscontent(which already contains-matches) andpackageif you want to keep them symmetric, but those can come in follow-up PRsHappy to send a PR if this direction is acceptable.
🤖 Generated with Claude Code