Skip to content

Commit c3d7bcf

Browse files
committed
fix(search): normalize ansi and whitespace matching
Normalize ANSI-heavy log search in backend and frontend paths. - backend: strip ANSI escapes and collapse whitespace in fitsForSearch - frontend: make highlight query whitespace tolerant across node boundaries - tests: add regression coverage for ANSI + mixed whitespace cases
1 parent ef2704c commit c3d7bcf

File tree

4 files changed

+50
-6
lines changed

4 files changed

+50
-6
lines changed

application/backend/app/containerdb/containerdb.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package containerdb
33
import (
44
"fmt"
55
"os"
6+
"regexp"
67
"strings"
78
"sync"
89
"time"
@@ -207,6 +208,7 @@ var (
207208
logCleanupMu sync.Mutex
208209
nextCleanup time.Time
209210
isCleanupRunning bool
211+
ansiEscapeRegex = regexp.MustCompile(`[\x1B\x9B][[\]()#;?]*(?:(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><])`)
210212
)
211213

212214
func MaybeScheduleCleanup(host string, container string) {
@@ -275,6 +277,11 @@ func PutLogMessage(db *leveldb.DB, host string, container string, message_item [
275277
}
276278

277279
func fitsForSearch(logLine string, message string, caseSensetivity bool) bool {
280+
logLine = ansiEscapeRegex.ReplaceAllString(logLine, "")
281+
message = ansiEscapeRegex.ReplaceAllString(message, "")
282+
logLine = strings.Join(strings.Fields(logLine), " ")
283+
message = strings.Join(strings.Fields(message), " ")
284+
278285
if !caseSensetivity {
279286
logLine = strings.ToLower(logLine)
280287
message = strings.ToLower(message)

application/backend/app/containerdb/containerdb_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,19 @@ func TestGetLogs(t *testing.T) {
9898
t.Error("Invalid last logItem datetime: ", logs[3][0])
9999
}
100100
}
101+
102+
func TestFitsForSearchWithANSI(t *testing.T) {
103+
logLine := "\x1b[37mWARNING\x1b[2m AzL4Y8oR KsTdiwHodbZ0i \tmOK2Wz \x1b[0m"
104+
105+
if !fitsForSearch(logLine, "WARNING Az", true) {
106+
t.Error("Expected query to match when ANSI escape codes are present")
107+
}
108+
109+
if !fitsForSearch(logLine, "KsTdiwHodbZ0i m", true) {
110+
t.Error("Expected query to match across tab-separated boundary")
111+
}
112+
113+
if fitsForSearch(logLine, "NOT_PRESENT", true) {
114+
t.Error("Expected non-existing query not to match")
115+
}
116+
}

application/frontend/src/Views/Logs/highlight.test.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,22 @@ async function run() {
9090
assert.equal(matches[0].textContent, "WARNING\nDETAIL");
9191
});
9292

93+
// Realistic rendered ANSI HTML with nested tags and mixed whitespace.
94+
runCase("mixed whitespace across ansi segments", () => {
95+
setupDom(`
96+
<p class="message"><span style="color:#A50">WARNING<b> AzL4Y8oR KsTdiwHodbZ0i \tmOK2Wz aF6UXv5KjPaqfO rk4ND9eAdluoci YyBTR 1Yz7A09 uSOcF6OUYB VUBGZGjWuJ \t<span style="color:#0A0">bTNSP</span></b></span> \tn4WFZFy92 nV2IJ4SA0RPZ JjNiiH1N yOEN Cfy 3DJO5uv wL2einh eF4yPL \t1gISzRyK1JR \tajoJ m4uY6Jpk2WA HXZGfuae6pG \tvy7RFkJZL0Az \t<span style="color:#A0A">sLvq</span></p>
97+
`);
98+
99+
findSearchTextInLogs(".message", "WARNING Az", true);
100+
assert.equal(document.querySelectorAll(".searchedText").length, 1);
101+
102+
findSearchTextInLogs(".message", "KsTdiwHodbZ0i m", true);
103+
assert.equal(document.querySelectorAll(".searchedText").length, 1);
104+
105+
findSearchTextInLogs(".message", "AzL4Y8oR KsTdiwHodbZ0i m", true);
106+
assert.equal(document.querySelectorAll(".searchedText").length, 1);
107+
});
108+
93109
// Performance-oriented scenario: many rendered log rows.
94110
runCase("high-volume highlighting remains responsive", () => {
95111
const rowsCount = 800;

application/frontend/src/utils/highlight.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
const highlightStateByNode = new WeakMap();
22

33
export const findSearchTextInLogs = (sel, searchText, caseSens) => {
4-
if (!sel || !searchText) {
5-
return;
6-
}
4+
const normalizedSearchText =
5+
typeof searchText === "string" ? searchText.trim() : "";
6+
7+
if (!sel || !normalizedSearchText) return;
78

89
const nodes = document.querySelectorAll(sel);
910
if (!nodes.length) {
1011
return;
1112
}
1213

1314
const escapeRegExp = (str = "") => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15+
const buildSearchPattern = (str = "") => {
16+
// Make whitespace robust against ANSI-driven node boundaries (\n, \t, multi-space).
17+
return escapeRegExp(str).replace(/\s+/g, "\\s+");
18+
};
1419
const regex = caseSens
15-
? new RegExp(escapeRegExp(searchText), "g")
16-
: new RegExp(escapeRegExp(searchText), "gi");
17-
const queryKey = `${caseSens ? "1" : "0"}:${searchText}`;
20+
? new RegExp(buildSearchPattern(normalizedSearchText), "g")
21+
: new RegExp(buildSearchPattern(normalizedSearchText), "gi");
22+
const queryKey = `${caseSens ? "1" : "0"}:${normalizedSearchText}`;
1823

1924
const unwrapHighlights = (root, hadHighlights) => {
2025
if (!hadHighlights) {

0 commit comments

Comments
 (0)