diff --git a/web/src/composables/useLogs/useHistogram.ts b/web/src/composables/useLogs/useHistogram.ts
index d8ece93d83..1d4cc4a9d1 100644
--- a/web/src/composables/useLogs/useHistogram.ts
+++ b/web/src/composables/useLogs/useHistogram.ts
@@ -119,7 +119,7 @@ export const useHistogram = () => {
if (searchObj.meta.logsVisualizeToggle === "logs") {
title +=
- "(" +
+ " (" +
scanSizeLabel +
": " +
formatSizeFromMB(searchObj.data.queryResults.scan_size) +
diff --git a/web/src/locales/languages/de.json b/web/src/locales/languages/de.json
index ce780a4c39..f8bbf2325e 100644
--- a/web/src/locales/languages/de.json
+++ b/web/src/locales/languages/de.json
@@ -276,7 +276,8 @@
"actionLabel": "Aktion",
"transformLabel": "Verwandeln",
"addToDashboard": "Zum Dashboard hinzufügen",
- "selectStream": "Wählen Sie Stream"
+ "selectStream": "Wählen Sie Stream",
+ "pattern_summary": "Es werden 1 bis 50 von {totalEvents} -Ereignissen und {patternsFound} -Mustern angezeigt, die in {logsAnalyzed} -Ereignissen in {totalTime} ms gefunden wurden."
},
"menu": {
"home": "Startseite",
diff --git a/web/src/locales/languages/en.json b/web/src/locales/languages/en.json
index c5b4dd2b15..eadb9d6587 100644
--- a/web/src/locales/languages/en.json
+++ b/web/src/locales/languages/en.json
@@ -276,7 +276,8 @@
"actionLabel": "Action",
"transformLabel": "Transform",
"addToDashboard": "Add To Dashboard",
- "selectStream": "Select Stream"
+ "selectStream": "Select Stream",
+ "pattern_summary": "Showing 1 to 50 out of {totalEvents} events & {patternsFound} patterns found in {logsAnalyzed} events in {totalTime} ms."
},
"menu": {
"home": "Home",
diff --git a/web/src/locales/languages/es.json b/web/src/locales/languages/es.json
index b04d6270d6..2b5f8aec63 100644
--- a/web/src/locales/languages/es.json
+++ b/web/src/locales/languages/es.json
@@ -276,7 +276,8 @@
"actionLabel": "Acción",
"transformLabel": "Transformar",
"addToDashboard": "Agregar al panel",
- "selectStream": "Selecciona Stream"
+ "selectStream": "Selecciona Stream",
+ "pattern_summary": "Se muestran de 1 a 50 de los eventos y {patternsFound} patrones de {totalEvents} encontrados en eventos de {logsAnalyzed} en {totalTime} ms."
},
"menu": {
"home": "Inicio",
diff --git a/web/src/locales/languages/fr.json b/web/src/locales/languages/fr.json
index e2f28e971f..2f44e1ed1f 100644
--- a/web/src/locales/languages/fr.json
+++ b/web/src/locales/languages/fr.json
@@ -276,7 +276,8 @@
"actionLabel": "Action",
"transformLabel": "Transformez",
"addToDashboard": "Ajouter au tableau de bord",
- "selectStream": "Sélectionnez Stream"
+ "selectStream": "Sélectionnez Stream",
+ "pattern_summary": "Affichage de 1 à 50 événements sur {TotalEvents} et des modèles {PatternsFound} trouvés dans les événements {LogsAnalyzed} en {TotalTime} ms."
},
"menu": {
"home": "Accueil",
diff --git a/web/src/locales/languages/it.json b/web/src/locales/languages/it.json
index de6cb935b2..759fd46c6d 100644
--- a/web/src/locales/languages/it.json
+++ b/web/src/locales/languages/it.json
@@ -276,7 +276,8 @@
"actionLabel": "Azione",
"transformLabel": "Trasforma",
"addToDashboard": "Aggiungi al pannello di controllo",
- "selectStream": "Seleziona Stream"
+ "selectStream": "Seleziona Stream",
+ "pattern_summary": "Mostrando da 1 a 50 eventi su {totalEvents} e {patternsFound} pattern trovati in {logsAnalyzed} eventi in {totalTime} ms."
},
"menu": {
"home": "Home",
diff --git a/web/src/locales/languages/ja.json b/web/src/locales/languages/ja.json
index 54ec9476c6..575f081368 100644
--- a/web/src/locales/languages/ja.json
+++ b/web/src/locales/languages/ja.json
@@ -276,7 +276,8 @@
"actionLabel": "アクション",
"transformLabel": "トランスフォーム",
"addToDashboard": "ダッシュボードに追加",
- "selectStream": "[ストリーム] を選択"
+ "selectStream": "[ストリーム] を選択",
+ "pattern_summary": "{totalTime} ミリ秒で {logsAnalyzed} イベントで見つかった {totalEvents} イベントと {PatternsFound} パターンのうち 1 ~ 50 件を表示しています。"
},
"menu": {
"home": "ホーム",
diff --git a/web/src/locales/languages/ko.json b/web/src/locales/languages/ko.json
index 6680f821d7..d9d974f477 100644
--- a/web/src/locales/languages/ko.json
+++ b/web/src/locales/languages/ko.json
@@ -276,7 +276,8 @@
"actionLabel": "액션",
"transformLabel": "트랜스폼",
"addToDashboard": "대시보드에 추가",
- "selectStream": "스트림 선택"
+ "selectStream": "스트림 선택",
+ "pattern_summary": "{TotalTime} 밀리초 내에 {logsAnallets} 이벤트에서 발견된 {TotalEvents} 이벤트 및 {PatternsFound} 패턴 중 1~50개를 표시합니다."
},
"menu": {
"home": "홈",
diff --git a/web/src/locales/languages/nl.json b/web/src/locales/languages/nl.json
index bf171ad59c..b659d2aa23 100644
--- a/web/src/locales/languages/nl.json
+++ b/web/src/locales/languages/nl.json
@@ -276,7 +276,8 @@
"actionLabel": "Actie",
"transformLabel": "Transformeren",
"addToDashboard": "Aan dashboard toevoegen",
- "selectStream": "Selecteer Stream"
+ "selectStream": "Selecteer Stream",
+ "pattern_summary": "Toont 1 tot 50 van de {totalEvents} -gebeurtenissen en {PatternsFound} -patronen gevonden in {logsAnalyzed} -gebeurtenissen in {totalTime} ms."
},
"menu": {
"home": "Home",
diff --git a/web/src/locales/languages/pt.json b/web/src/locales/languages/pt.json
index 6752b9b448..817b535c0e 100644
--- a/web/src/locales/languages/pt.json
+++ b/web/src/locales/languages/pt.json
@@ -276,7 +276,8 @@
"actionLabel": "Ação",
"transformLabel": "Transformar",
"addToDashboard": "Adicionar ao painel",
- "selectStream": "Selecione Stream"
+ "selectStream": "Selecione Stream",
+ "pattern_summary": "Mostrando de 1 a 50 dos eventos {totalEvents} e padrões {patternsFound} encontrados em eventos {logsAnalyzed} em {totalTime} ms."
},
"menu": {
"home": "Início",
diff --git a/web/src/locales/languages/tr.json b/web/src/locales/languages/tr.json
index 2925c6bf3a..bc646511c1 100644
--- a/web/src/locales/languages/tr.json
+++ b/web/src/locales/languages/tr.json
@@ -276,7 +276,8 @@
"actionLabel": "Eylem",
"transformLabel": "Dönüştürün",
"addToDashboard": "Kontrol Paneline Ekle",
- "selectStream": "Akışı Seç"
+ "selectStream": "Akışı Seç",
+ "pattern_summary": "{totalTime} ms içinde {logsAnalyzed} olaylarında bulunan {totalEvents} olay ve {patternsFound} modelinden 1 ila 50 arası gösteriliyor."
},
"menu": {
"home": "Ana Sayfa",
diff --git a/web/src/locales/languages/zh.json b/web/src/locales/languages/zh.json
index 61a6ee3ddf..f5177e5d13 100644
--- a/web/src/locales/languages/zh.json
+++ b/web/src/locales/languages/zh.json
@@ -276,7 +276,8 @@
"actionLabel": "行动",
"transformLabel": "转型",
"addToDashboard": "添加到控制面板",
- "selectStream": "选择直播"
+ "selectStream": "选择直播",
+ "pattern_summary": "在 {totalTime} 毫秒内显示在 {LogsAnalyzed} 事件中找到的 {TotalEvents} 事件和 {PatternsFound} 模式中的 1 到 50 个。"
},
"menu": {
"home": "主页",
diff --git a/web/src/plugins/logs/SearchResult.vue b/web/src/plugins/logs/SearchResult.vue
index 7087c2dfe7..90fe08d386 100644
--- a/web/src/plugins/logs/SearchResult.vue
+++ b/web/src/plugins/logs/SearchResult.vue
@@ -38,7 +38,7 @@ along with this program. If not, see .
/>
- {{ noOfRecordsTitle }}
+ {{ searchObj.meta.logsVisualizeToggle === 'patterns' ? patternSummaryText : noOfRecordsTitle }}
Error while
: 'table-container--with-histogram',
]"
>
-
-
-
import("./patterns/PatternStatistics.vue"),
- ),
PatternList: defineAsyncComponent(
() => import("./patterns/PatternList.vue"),
),
@@ -501,6 +492,22 @@ export default defineComponent({
},
},
methods: {
+ formatPatternSummary(stats: any, totalEvents: number, histogramMs: number) {
+ const patternsFound = stats?.total_patterns_found || 0;
+ const logsAnalyzed = (stats?.total_logs_analyzed || 0).toLocaleString();
+ const totalEventsStr = totalEvents ? totalEvents.toLocaleString() : logsAnalyzed;
+
+ // Combine histogram time + pattern extraction time
+ const patternMs = stats?.extraction_time_ms || 0;
+ const totalTimeMs = histogramMs + patternMs;
+
+ return this.$t("search.pattern_summary", {
+ totalEvents: totalEventsStr,
+ patternsFound: patternsFound,
+ logsAnalyzed: logsAnalyzed,
+ totalTime: totalTimeMs
+ });
+ },
handleColumnSizesUpdate(newColSizes: any) {
const prevColSizes =
this.searchObj.data.resultGrid?.colSizes[
@@ -689,6 +696,7 @@ export default defineComponent({
const $q = useQuasar();
const searchListContainer = ref(null);
const noOfRecordsTitle = ref("");
+ const patternSummaryText = ref("");
const scrollPosition = ref(0);
const rowsPerPageOptions = [10, 25, 50, 100];
const disableMoreErrorDetails = ref(false);
@@ -1054,14 +1062,16 @@ export default defineComponent({
const extractConstantsFromPattern = (template: string): string[] => {
// Extract longest non-variable strings from pattern template
// Pattern template has format like: "INFO action <*> at 14:47.1755283"
- // We want continuous strings between <*> that are longer than 10 chars
+ // Variables can be: <*>, <:IDENTIFIERS>, <:TIMESTAMP>, <:UNIX_TIMESTAMP>, etc.
+ // We want continuous strings between these variables that are longer than 10 chars
const constants: string[] = [];
- const parts = template.split("<*>");
+
+ // Split by all variable markers using regex
+ // Matches: <*>, <:WORD>, etc.
+ const parts = template.split(/<[*:][^>]*>/);
for (const part of parts) {
const trimmed = part.trim();
- // For now, use the string as-is without sanitization
- // const sanitized = sanitizeForMatchAll(trimmed);
// Only include strings longer than 10 characters
if (trimmed.length > 10) {
constants.push(trimmed);
@@ -1090,10 +1100,15 @@ export default defineComponent({
// Build multiple match_all() clauses, one for each constant
// Each match_all takes a single string
const matchAllClauses = constants.map((constant) => {
- // Escape backslashes first, then single quotes in the constant
+ // Escape special characters for match_all query
+ // Order matters: backslash must be escaped first
const escapedConstant = constant
- .replace(/\\/g, "\\\\")
- .replace(/'/g, "\\'");
+ .replace(/\\/g, "\\\\") // Escape backslashes
+ .replace(/'/g, "\\'") // Escape single quotes
+ .replace(/"/g, '\\"') // Escape double quotes
+ .replace(/\n/g, "\\n") // Escape newlines
+ .replace(/\r/g, "\\r") // Escape carriage returns
+ .replace(/\t/g, "\\t"); // Escape tabs
return `match_all('${escapedConstant}')`;
});
@@ -1111,6 +1126,9 @@ export default defineComponent({
// Set the filter to be added to the query
searchObj.data.stream.addToFilter = filterExpression;
+
+ // Switch to logs view to show the filtered results
+ searchObj.meta.logsVisualizeToggle = "logs";
};
const getRowIndex = (next: boolean, prev: boolean, oldIndex: number) => {
@@ -1374,6 +1392,7 @@ export default defineComponent({
extractFTSFields,
useLocalWrapContent,
noOfRecordsTitle,
+ patternSummaryText,
scrollPosition,
rowsPerPageOptions,
pageNumberInput,
@@ -1429,6 +1448,9 @@ export default defineComponent({
updateTitle() {
return this.searchObj.data.histogram.chartParams.title;
},
+ updatePatternSummary() {
+ return this.patternsState?.patterns?.statistics;
+ },
reDrawChartData() {
return this.searchObj.data.histogram;
},
@@ -1451,6 +1473,20 @@ export default defineComponent({
updateTitle() {
this.noOfRecordsTitle = this.searchObj.data.histogram.chartParams.title;
},
+ updatePatternSummary() {
+ if (this.patternsState?.patterns?.statistics) {
+ // Reuse the same summary logic from PatternStatistics component
+ const stats = this.patternsState.patterns.statistics;
+ const totalEvents = this.searchObj.data.queryResults?.total || stats.total_logs_analyzed || 0;
+ const histogramMs = this.searchObj.data.queryResults?.took || 0;
+
+ this.patternSummaryText = this.formatPatternSummary(
+ stats,
+ totalEvents,
+ histogramMs
+ );
+ }
+ },
reDrawChartData: {
deep: true,
handler: function () {
diff --git a/web/src/plugins/logs/patterns/PatternCard.vue b/web/src/plugins/logs/patterns/PatternCard.vue
index 5cfd5ecc69..332755e6c8 100644
--- a/web/src/plugins/logs/patterns/PatternCard.vue
+++ b/web/src/plugins/logs/patterns/PatternCard.vue
@@ -16,14 +16,14 @@ along with this program. If not, see .
-
+
-
+
{{ pattern.frequency.toLocaleString() }}
@@ -49,16 +49,16 @@ along with this program. If not, see .
-
+
{{ pattern.percentage.toFixed(2) }}%
-
+
.
-
+
{{ t("search.patternColumnHeader") }}
-
+
{{ t("search.occurrenceColumnHeader") }}
-
+
{{ t("search.percentageColumnHeader") }}
-
diff --git a/web/src/plugins/logs/patterns/PatternStatistics.spec.ts b/web/src/plugins/logs/patterns/PatternStatistics.spec.ts
index 1548f08b9a..84702fffc9 100644
--- a/web/src/plugins/logs/patterns/PatternStatistics.spec.ts
+++ b/web/src/plugins/logs/patterns/PatternStatistics.spec.ts
@@ -51,77 +51,65 @@ describe("PatternStatistics", () => {
).toBe(true);
});
- describe("Statistics Cards", () => {
- it("should display logs scanned card with correct value", () => {
- const logsScannedCard = wrapper.find(
- '[data-test="pattern-stats-logs-scanned-card"]',
- );
- expect(logsScannedCard.exists()).toBe(true);
+ describe("Summary Text", () => {
+ it("should display summary with correct statistics", () => {
+ const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
+ const summaryText = summaryElement.text();
- const logsScannedValue = wrapper.find(
- '[data-test="pattern-stats-logs-scanned-value"]',
- );
- expect(logsScannedValue.text()).toBe("1,000");
+ // Check that summary contains the key statistics
+ expect(summaryText).toContain("1,000"); // logs analyzed
+ expect(summaryText).toContain("42"); // patterns found
+ expect(summaryText).toContain("events");
+ expect(summaryText).toContain("patterns found");
+ expect(summaryText).toContain("ms");
});
- it("should display patterns found card with correct value", () => {
- const patternsFoundCard = wrapper.find(
- '[data-test="pattern-stats-patterns-found-card"]',
- );
- expect(patternsFoundCard.exists()).toBe(true);
+ it("should display correct format", () => {
+ const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
+ const summaryText = summaryElement.text();
- const patternsFoundValue = wrapper.find(
- '[data-test="pattern-stats-patterns-found-value"]',
- );
- expect(patternsFoundValue.text()).toBe("42");
+ // Check the full format
+ expect(summaryText).toMatch(/Showing 1 to 50 out of \d+/);
+ expect(summaryText).toMatch(/\d+ patterns found in \d+/);
});
- it("should display coverage card with correct value", () => {
- const coverageCard = wrapper.find(
- '[data-test="pattern-stats-coverage-card"]',
- );
- expect(coverageCard.exists()).toBe(true);
+ it("should handle totalEvents prop", async () => {
+ await wrapper.setProps({
+ statistics: mockStatistics,
+ totalEvents: 5000,
+ });
- const coverageValue = wrapper.find(
- '[data-test="pattern-stats-coverage-value"]',
- );
- expect(coverageValue.text()).toBe("87.5%");
+ const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
+ const summaryText = summaryElement.text();
+
+ expect(summaryText).toContain("5,000"); // totalEvents
});
- it("should display processing time card with correct value", () => {
- const processingTimeCard = wrapper.find(
- '[data-test="pattern-stats-processing-time-card"]',
- );
- expect(processingTimeCard.exists()).toBe(true);
+ it("should combine histogram time with extraction time", async () => {
+ await wrapper.setProps({
+ statistics: mockStatistics,
+ histogramTime: 50,
+ });
- const processingTimeValue = wrapper.find(
- '[data-test="pattern-stats-processing-time-value"]',
- );
- expect(processingTimeValue.text()).toBe("150ms");
+ const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
+ const summaryText = summaryElement.text();
+
+ // Should show 50 (histogram) + 150 (extraction) = 200ms
+ expect(summaryText).toContain("200 ms");
});
});
-
describe("Empty Statistics", () => {
it("should display zero values when statistics are null", async () => {
await wrapper.setProps({
statistics: null,
});
- const patternsFoundValue = wrapper.find(
- '[data-test="pattern-stats-patterns-found-value"]',
- );
- expect(patternsFoundValue.text()).toBe("0");
+ const summaryElement = wrapper.find('[data-test="pattern-statistics"]');
+ const summaryText = summaryElement.text();
- const coverageValue = wrapper.find(
- '[data-test="pattern-stats-coverage-value"]',
- );
- expect(coverageValue.text()).toBe("0.0%");
-
- const processingTimeValue = wrapper.find(
- '[data-test="pattern-stats-processing-time-value"]',
- );
- expect(processingTimeValue.text()).toBe("0ms");
+ expect(summaryText).toContain("0 patterns found");
+ expect(summaryText).toContain("0 events");
});
});
});
diff --git a/web/src/plugins/logs/patterns/PatternStatistics.vue b/web/src/plugins/logs/patterns/PatternStatistics.vue
index 6cdc662203..c54f26baad 100644
--- a/web/src/plugins/logs/patterns/PatternStatistics.vue
+++ b/web/src/plugins/logs/patterns/PatternStatistics.vue
@@ -15,111 +15,39 @@ along with this program. If not, see
.
-->
-
-
-
-
-
-
-
- Total Logs Scanned
-
-
- {{ (statistics?.total_logs_analyzed || 0).toLocaleString() }}
-
-
-
-
-
-
-
-
-
-
- Patterns Found
-
-
- {{ statistics?.total_patterns_found || 0 }}
-
-
-
-
-
-
-
-
-
-
- Coverage
-
-
- {{ (statistics?.coverage_percentage || 0).toFixed(1) }}%
-
-
-
-
-
-
-
-
-
-
- Processing Time
-
-
- {{ statistics?.extraction_time_ms || 0 }}ms
-
-
-
-
-
+
+ {{ summaryText }}