Skip to content

Commit 957410c

Browse files
ozgesolidkeyclaude
andcommitted
Add Column-Aware Analyzer with actionable insights and advanced filtering
Major features: - Column-Aware Analyzer that detects structured log formats - Automatic column detection from headers - Per-column statistics with bar charts - 4 actionable insights: Noise Detection, Error Groups, Anomalies, Filter Suggestions - One-click filter application with undo support - Advanced Conditional Filter (Rule Builder) - AND/OR group operators - Rule types: contains, not contains, level, regex - Visual UI for building complex filter expressions - Time Gap Detection - Configurable gap threshold - Pattern/line range filtering - Next/prev navigation with position indicator - European timestamp format support (DD.MM.YYYY) - Highlight Navigation - Next/prev buttons for each highlight color - On-demand search with caching - Position indicator UI improvements: - Distinct scrollbar colors for sidebar sections - Click-to-navigate for insights (errors, anomalies) - Active filter indicator with clear button Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3176b0a commit 957410c

10 files changed

Lines changed: 3033 additions & 76 deletions

File tree

src/main/analyzers/columnAwareAnalyzer.ts

Lines changed: 599 additions & 0 deletions
Large diffs are not rendered by default.

src/main/analyzers/drainAnalyzer.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ interface LogCluster {
5050
template: string[]; // The log template (tokens with <*> for variables)
5151
count: number; // Number of logs matching this pattern
5252
sampleLines: number[]; // Sample line numbers
53+
sampleText?: string; // First sample log line for display
5354
level?: string; // Detected log level
5455
}
5556

@@ -220,7 +221,7 @@ export class DrainAnalyzer implements LogAnalyzer {
220221
if (this.clusterCount < MAX_CLUSTERS) {
221222
const tokens = tokenize(trimmed);
222223
if (tokens.length > 0 && tokens.length < 100) {
223-
this.addLogMessage(tokens, lineNumber, level);
224+
this.addLogMessage(tokens, lineNumber, level, trimmed);
224225
}
225226
}
226227

@@ -253,13 +254,22 @@ export class DrainAnalyzer implements LogAnalyzer {
253254
// Build pattern groups
254255
const patterns: PatternGroup[] = allClusters
255256
.filter(c => c.count > 1)
256-
.map(cluster => ({
257-
pattern: cluster.template.join(' '),
258-
template: cluster.template.join(' '),
259-
count: cluster.count,
260-
sampleLines: cluster.sampleLines.slice(0, 5),
261-
category: categorizePattern(cluster.template, cluster.level)
262-
}))
257+
.map(cluster => {
258+
// Create human-readable template: extract key fixed tokens
259+
const keyTokens = cluster.template.filter(t => t !== PARAM_TOKEN && t.length > 2);
260+
const readableTemplate = keyTokens.length > 0
261+
? keyTokens.slice(0, 8).join(' ') // Show up to 8 key tokens
262+
: cluster.template.slice(0, 5).join(' ');
263+
264+
return {
265+
pattern: cluster.template.join(' '),
266+
template: readableTemplate,
267+
count: cluster.count,
268+
sampleLines: cluster.sampleLines.slice(0, 5),
269+
sampleText: cluster.sampleText,
270+
category: categorizePattern(cluster.template, cluster.level)
271+
};
272+
})
263273
.sort((a, b) => b.count - a.count)
264274
.slice(0, maxPatterns);
265275

@@ -303,7 +313,7 @@ export class DrainAnalyzer implements LogAnalyzer {
303313
};
304314
}
305315

306-
private addLogMessage(tokens: string[], lineNumber: number, level?: string): void {
316+
private addLogMessage(tokens: string[], lineNumber: number, level?: string, originalLine?: string): void {
307317
const len = tokens.length;
308318

309319
// Get or create length bucket
@@ -357,10 +367,14 @@ export class DrainAnalyzer implements LogAnalyzer {
357367
}
358368
} else {
359369
// Create new cluster
370+
const sampleText = originalLine
371+
? (originalLine.length > 150 ? originalLine.substring(0, 150) + '...' : originalLine)
372+
: undefined;
360373
const newCluster: LogCluster = {
361374
template: tokens.map(t => isVariable(t) ? PARAM_TOKEN : t),
362375
count: 1,
363376
sampleLines: [lineNumber],
377+
sampleText,
364378
level
365379
};
366380
node.clusters.push(newCluster);

src/main/analyzers/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ export * from './types';
33
export { analyzerRegistry } from './registry';
44
export { RuleBasedAnalyzer } from './ruleBasedAnalyzer';
55
export { DrainAnalyzer } from './drainAnalyzer';
6+
export { ColumnAwareAnalyzer } from './columnAwareAnalyzer';
67

78
// Register built-in analyzers
89
import { analyzerRegistry } from './registry';
910
import { DrainAnalyzer } from './drainAnalyzer';
1011
import { RuleBasedAnalyzer } from './ruleBasedAnalyzer';
12+
import { ColumnAwareAnalyzer } from './columnAwareAnalyzer';
1113

12-
// Drain is the default (registered first) - better pattern discovery
14+
// Column-aware is the default (registered first) - best for structured logs
15+
analyzerRegistry.register(new ColumnAwareAnalyzer());
1316
analyzerRegistry.register(new DrainAnalyzer());
1417
analyzerRegistry.register(new RuleBasedAnalyzer());

src/main/analyzers/types.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface PatternGroup {
1818
template: string; // Human-readable template with {placeholders}
1919
count: number;
2020
sampleLines: number[];
21+
sampleText?: string; // Actual example log line
2122
category: 'noise' | 'error' | 'warning' | 'info' | 'debug' | 'unknown';
2223
}
2324

@@ -28,6 +29,71 @@ export interface DuplicateGroup {
2829
lineNumbers: number[];
2930
}
3031

32+
export interface ColumnStatValue {
33+
value: string;
34+
count: number;
35+
percentage: number;
36+
}
37+
38+
export interface ColumnStats {
39+
name: string;
40+
type: string;
41+
topValues: ColumnStatValue[];
42+
uniqueCount: number;
43+
}
44+
45+
// Noise candidate - high frequency message that could be filtered
46+
export interface NoiseCandidate {
47+
pattern: string;
48+
sampleText: string;
49+
count: number;
50+
percentage: number;
51+
channel?: string;
52+
suggestedFilter: string;
53+
}
54+
55+
// Grouped error/warning messages
56+
export interface ErrorGroup {
57+
pattern: string;
58+
sampleText: string;
59+
count: number;
60+
level: 'error' | 'warning';
61+
channel?: string;
62+
firstLine: number;
63+
lastLine: number;
64+
}
65+
66+
// Rare/anomalous message
67+
export interface Anomaly {
68+
text: string;
69+
lineNumber: number;
70+
level?: string;
71+
channel?: string;
72+
reason: string; // Why it's considered anomalous
73+
}
74+
75+
// Filter suggestion
76+
export interface FilterSuggestion {
77+
id: string;
78+
title: string;
79+
description: string;
80+
type: 'exclude' | 'include' | 'level';
81+
filter: {
82+
excludePatterns?: string[];
83+
includePatterns?: string[];
84+
levels?: string[];
85+
channel?: string;
86+
};
87+
}
88+
89+
// Analysis insights - the useful stuff
90+
export interface AnalysisInsights {
91+
noiseCandidates: NoiseCandidate[];
92+
errorGroups: ErrorGroup[];
93+
anomalies: Anomaly[];
94+
filterSuggestions: FilterSuggestion[];
95+
}
96+
3197
export interface AnalysisResult {
3298
stats: {
3399
totalLines: number;
@@ -41,6 +107,8 @@ export interface AnalysisResult {
41107
timeRange?: { start: string; end: string };
42108
analyzerName: string;
43109
analyzedAt: number;
110+
columnStats?: ColumnStats[];
111+
insights?: AnalysisInsights;
44112
}
45113

46114
// Base interface - all analyzers must implement this

0 commit comments

Comments
 (0)