logpare 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VVVVUJFY.js → chunk-SAYWGRX4.js} +88 -8
- package/dist/chunk-SAYWGRX4.js.map +1 -0
- package/dist/cli.cjs +88 -21
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +6 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +91 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -2
- package/dist/index.d.ts +40 -2
- package/dist/index.js +9 -1
- package/package.json +6 -1
- package/dist/chunk-VVVVUJFY.js.map +0 -1
|
@@ -105,15 +105,18 @@ function extractDurations(line) {
|
|
|
105
105
|
return durations.slice(0, 5);
|
|
106
106
|
}
|
|
107
107
|
var DEFAULT_PATTERNS = {
|
|
108
|
-
// Timestamps (
|
|
108
|
+
// Timestamps (must run before port to avoid fragmentation)
|
|
109
109
|
isoTimestamp: /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g,
|
|
110
|
+
// UUID must run before unixTimestamp to prevent partial matching of UUID segments
|
|
111
|
+
uuid: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
|
|
110
112
|
unixTimestamp: /\b\d{10,13}\b/g,
|
|
111
113
|
// Network addresses
|
|
112
114
|
ipv4: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
|
|
113
|
-
|
|
115
|
+
// IPv6: matches full, compressed (::1, ::), and partial forms
|
|
116
|
+
// Order matters: longer matches must come before shorter ones in alternation
|
|
117
|
+
ipv6: /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::)/g,
|
|
114
118
|
port: /:\d{2,5}\b/g,
|
|
115
119
|
// Identifiers
|
|
116
|
-
uuid: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
|
|
117
120
|
hexId: /\b0x[0-9a-fA-F]+\b/g,
|
|
118
121
|
blockId: /\bblk_-?\d+\b/g,
|
|
119
122
|
// Paths and URLs
|
|
@@ -412,6 +415,20 @@ var LogCluster = class {
|
|
|
412
415
|
};
|
|
413
416
|
|
|
414
417
|
// src/output/formatter.ts
|
|
418
|
+
function sortObjectKeys(value) {
|
|
419
|
+
if (value === null || typeof value !== "object") {
|
|
420
|
+
return value;
|
|
421
|
+
}
|
|
422
|
+
if (Array.isArray(value)) {
|
|
423
|
+
return value.map(sortObjectKeys);
|
|
424
|
+
}
|
|
425
|
+
const sorted = {};
|
|
426
|
+
const keys = Object.keys(value).sort();
|
|
427
|
+
for (const key of keys) {
|
|
428
|
+
sorted[key] = sortObjectKeys(value[key]);
|
|
429
|
+
}
|
|
430
|
+
return sorted;
|
|
431
|
+
}
|
|
415
432
|
function formatSummary(templates, stats) {
|
|
416
433
|
const lines = [];
|
|
417
434
|
lines.push("=== Log Compression Summary ===");
|
|
@@ -518,6 +535,33 @@ function formatJson(templates, stats) {
|
|
|
518
535
|
};
|
|
519
536
|
return JSON.stringify(output, null, 2);
|
|
520
537
|
}
|
|
538
|
+
function formatJsonStable(templates, stats) {
|
|
539
|
+
const output = {
|
|
540
|
+
stats: {
|
|
541
|
+
compressionRatio: Math.round(stats.compressionRatio * 1e3) / 1e3,
|
|
542
|
+
estimatedTokenReduction: Math.round(stats.estimatedTokenReduction * 1e3) / 1e3,
|
|
543
|
+
inputLines: stats.inputLines,
|
|
544
|
+
uniqueTemplates: stats.uniqueTemplates
|
|
545
|
+
},
|
|
546
|
+
templates: templates.map((t) => ({
|
|
547
|
+
correlationIdSamples: t.correlationIdSamples,
|
|
548
|
+
durationSamples: t.durationSamples,
|
|
549
|
+
firstSeen: t.firstSeen,
|
|
550
|
+
fullUrlSamples: t.fullUrlSamples,
|
|
551
|
+
id: t.id,
|
|
552
|
+
isStackFrame: t.isStackFrame,
|
|
553
|
+
lastSeen: t.lastSeen,
|
|
554
|
+
occurrences: t.occurrences,
|
|
555
|
+
pattern: t.pattern,
|
|
556
|
+
samples: t.sampleVariables,
|
|
557
|
+
severity: t.severity,
|
|
558
|
+
statusCodeSamples: t.statusCodeSamples,
|
|
559
|
+
urlSamples: t.urlSamples
|
|
560
|
+
})),
|
|
561
|
+
version: "1.1"
|
|
562
|
+
};
|
|
563
|
+
return JSON.stringify(sortObjectKeys(output));
|
|
564
|
+
}
|
|
521
565
|
|
|
522
566
|
// src/drain/drain.ts
|
|
523
567
|
var DEFAULTS = {
|
|
@@ -582,6 +626,14 @@ var Drain = class {
|
|
|
582
626
|
addLogLines(lines) {
|
|
583
627
|
const total = lines.length;
|
|
584
628
|
const reportInterval = Math.max(1, Math.floor(total / 100));
|
|
629
|
+
if (this.onProgress && total > 0) {
|
|
630
|
+
this.onProgress({
|
|
631
|
+
processedLines: 0,
|
|
632
|
+
totalLines: total,
|
|
633
|
+
currentPhase: "parsing",
|
|
634
|
+
percentComplete: 0
|
|
635
|
+
});
|
|
636
|
+
}
|
|
585
637
|
for (let i = 0; i < total; i++) {
|
|
586
638
|
this.addLogLine(lines[i]);
|
|
587
639
|
if (this.onProgress && i % reportInterval === 0) {
|
|
@@ -604,6 +656,10 @@ var Drain = class {
|
|
|
604
656
|
}
|
|
605
657
|
/**
|
|
606
658
|
* Search the parse tree for a matching cluster.
|
|
659
|
+
*
|
|
660
|
+
* Uses XDrain-inspired token-position fallback: if the first token
|
|
661
|
+
* looks like a variable (timestamp, PID, etc.), tries using the
|
|
662
|
+
* second token as the navigation key for better matching.
|
|
607
663
|
*/
|
|
608
664
|
treeSearch(tokens) {
|
|
609
665
|
const tokenCount = tokens.length;
|
|
@@ -612,19 +668,36 @@ var Drain = class {
|
|
|
612
668
|
if (lengthNode === void 0) {
|
|
613
669
|
return null;
|
|
614
670
|
}
|
|
671
|
+
const primaryResult = this.treeSearchFromToken(lengthNode, tokens, 0);
|
|
672
|
+
if (primaryResult !== null) {
|
|
673
|
+
return primaryResult;
|
|
674
|
+
}
|
|
615
675
|
const firstToken = tokens[0];
|
|
616
|
-
if (firstToken
|
|
676
|
+
if (tokens.length > 1 && firstToken !== void 0 && this.looksLikeVariable(firstToken)) {
|
|
677
|
+
return this.treeSearchFromToken(lengthNode, tokens, 1);
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Search from a specific token position in the tree.
|
|
683
|
+
* @param lengthNode - The node at level 1 (token count)
|
|
684
|
+
* @param tokens - All tokens in the log line
|
|
685
|
+
* @param startIndex - Which token to use as "first token" for navigation
|
|
686
|
+
*/
|
|
687
|
+
treeSearchFromToken(lengthNode, tokens, startIndex) {
|
|
688
|
+
const navToken = tokens[startIndex];
|
|
689
|
+
if (navToken === void 0) {
|
|
617
690
|
return null;
|
|
618
691
|
}
|
|
619
|
-
let currentNode = lengthNode.getChild(
|
|
620
|
-
if (currentNode === void 0) {
|
|
692
|
+
let currentNode = lengthNode.getChild(navToken);
|
|
693
|
+
if (currentNode === void 0 && startIndex === 0) {
|
|
621
694
|
currentNode = lengthNode.getChild(WILDCARD_KEY);
|
|
622
695
|
}
|
|
623
696
|
if (currentNode === void 0) {
|
|
624
697
|
return null;
|
|
625
698
|
}
|
|
626
699
|
let searchNode = currentNode;
|
|
627
|
-
for (let i = 1; i < Math.min(tokens.length, this.depth); i++) {
|
|
700
|
+
for (let i = startIndex + 1; i < Math.min(tokens.length, this.depth); i++) {
|
|
628
701
|
const token = tokens[i];
|
|
629
702
|
if (token === void 0) {
|
|
630
703
|
break;
|
|
@@ -752,6 +825,9 @@ var Drain = class {
|
|
|
752
825
|
case "json":
|
|
753
826
|
formatted = formatJson(limitedTemplates, stats);
|
|
754
827
|
break;
|
|
828
|
+
case "json-stable":
|
|
829
|
+
formatted = formatJsonStable(limitedTemplates, stats);
|
|
830
|
+
break;
|
|
755
831
|
case "summary":
|
|
756
832
|
default:
|
|
757
833
|
formatted = formatSummary(limitedTemplates, stats);
|
|
@@ -829,6 +905,10 @@ export {
|
|
|
829
905
|
detectSeverity,
|
|
830
906
|
isStackFrame,
|
|
831
907
|
extractUrls,
|
|
908
|
+
extractFullUrls,
|
|
909
|
+
extractStatusCodes,
|
|
910
|
+
extractCorrelationIds,
|
|
911
|
+
extractDurations,
|
|
832
912
|
DEFAULT_PATTERNS,
|
|
833
913
|
WILDCARD,
|
|
834
914
|
defineStrategy,
|
|
@@ -837,4 +917,4 @@ export {
|
|
|
837
917
|
compress,
|
|
838
918
|
compressText
|
|
839
919
|
};
|
|
840
|
-
//# sourceMappingURL=chunk-
|
|
920
|
+
//# sourceMappingURL=chunk-SAYWGRX4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/preprocessing/patterns.ts","../src/preprocessing/default.ts","../src/drain/node.ts","../src/drain/cluster.ts","../src/output/formatter.ts","../src/drain/drain.ts","../src/api.ts"],"sourcesContent":["import type { Severity } from '../types.js';\n\n/**\n * Patterns for detecting log severity levels.\n */\nexport const SEVERITY_PATTERNS = {\n error: /\\b(Error|ERROR|error|Uncaught|UNCAUGHT|Failed|FAILED|Exception|EXCEPTION|FATAL|fatal|TypeError|ReferenceError|SyntaxError|RangeError)\\b/,\n warning: /\\b(Warning|WARNING|warn|WARN|\\[Violation\\]|Violation|DEPRECATED|deprecated|Deprecation)\\b/,\n} as const;\n\n/**\n * Patterns for detecting stack trace frames.\n */\nexport const STACK_FRAME_PATTERNS = [\n /^\\s*at\\s+/, // \" at Function.x\" (V8/Node)\n /^\\s*@\\s*\\S+:\\d+/, // \"@ file.js:123\" (Firefox)\n /^\\s*\\w+@\\S+:\\d+/, // \"fn@file.js:123\" (Firefox named)\n /^\\s*\\(anonymous\\)\\s*@/, // \"(anonymous) @ file.js:123\" (Chrome DevTools)\n /^\\s*[A-Za-z_$][\\w$]*\\s+@\\s+\\S+:\\d+/, // \"functionName @ file.js:123\"\n] as const;\n\n/**\n * Detect the severity level of a log line.\n * Returns 'error', 'warning', or 'info'.\n */\nexport function detectSeverity(line: string): Severity {\n if (SEVERITY_PATTERNS.error.test(line)) {\n return 'error';\n }\n if (SEVERITY_PATTERNS.warning.test(line)) {\n return 'warning';\n }\n return 'info';\n}\n\n/**\n * Detect if a line is a stack trace frame.\n */\nexport function isStackFrame(line: string): boolean {\n return STACK_FRAME_PATTERNS.some(pattern => pattern.test(line));\n}\n\n/**\n * Extract URLs from a line (before masking).\n * Returns hostnames only for brevity in urlSamples.\n */\nexport function extractUrls(line: string): string[] {\n const urlPattern = /https?:\\/\\/[^\\s\"'<>]+/g;\n const matches = line.match(urlPattern);\n if (!matches) return [];\n\n // Extract just the host from each URL for brevity\n return matches.map(url => {\n try {\n const parsed = new URL(url);\n return parsed.hostname;\n } catch {\n return url;\n }\n }).filter((v, i, a) => a.indexOf(v) === i); // dedupe\n}\n\n/**\n * Extract full URLs from a line (before masking).\n * Returns complete URLs with paths for better diagnostics.\n */\nexport function extractFullUrls(line: string): string[] {\n const urlPattern = /https?:\\/\\/[^\\s\"'<>]+/g;\n const matches = line.match(urlPattern);\n if (!matches) return [];\n\n // Return full URLs, deduplicated\n return [...new Set(matches)];\n}\n\n/**\n * Extract HTTP status codes from a line.\n * Matches common patterns like \"status 404\", \"HTTP 500\", \"status: 403\".\n */\nexport function extractStatusCodes(line: string): number[] {\n // Match status codes in context to avoid false positives\n const patterns = [\n /\\bstatus[:\\s]+(\\d{3})\\b/gi, // \"status 404\", \"status: 500\"\n /\\bHTTP[\\/\\s]\\d\\.\\d\\s+(\\d{3})\\b/gi, // \"HTTP/1.1 404\", \"HTTP 1.1 500\"\n /\\bcode[:\\s]+(\\d{3})\\b/gi, // \"code: 403\", \"code 500\"\n /\\b(\\d{3})\\s+(?:OK|Not Found|Bad Request|Unauthorized|Forbidden|Internal Server Error|Service Unavailable)\\b/gi,\n ];\n\n const codes: number[] = [];\n\n for (const pattern of patterns) {\n const regex = new RegExp(pattern.source, pattern.flags);\n let match;\n while ((match = regex.exec(line)) !== null) {\n const codeStr = match[1];\n if (codeStr) {\n const code = parseInt(codeStr, 10);\n // Only include valid HTTP status codes (100-599)\n if (code >= 100 && code <= 599 && !codes.includes(code)) {\n codes.push(code);\n }\n }\n }\n }\n\n return codes;\n}\n\n/**\n * Extract correlation/trace IDs from a line.\n * Matches common patterns like trace-id, request-id, correlation-id, and UUIDs.\n */\nexport function extractCorrelationIds(line: string): string[] {\n const patterns = [\n // Named correlation IDs: trace-id=xxx, request_id: xxx, x-request-id=xxx\n /\\b(?:trace[-_]?id|request[-_]?id|correlation[-_]?id|x-request-id)[=:\\s]+[\"']?([a-zA-Z0-9-_]+)[\"']?/gi,\n // Standalone UUIDs (common correlation ID format)\n /\\b([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\\b/gi,\n ];\n\n const ids: string[] = [];\n\n for (const pattern of patterns) {\n const regex = new RegExp(pattern.source, pattern.flags);\n let match;\n while ((match = regex.exec(line)) !== null) {\n const id = match[1] || match[0];\n if (id && !ids.includes(id)) {\n ids.push(id);\n }\n }\n }\n\n // Limit to 3 IDs per line\n return ids.slice(0, 3);\n}\n\n/**\n * Extract duration/timing values from a line.\n * Matches common duration patterns like \"80ms\", \"1.5s\", \"250µs\", \"2sec\", etc.\n * Called before preprocessing masks these values.\n */\nexport function extractDurations(line: string): string[] {\n // Pattern matches:\n // - Integer or decimal numbers\n // - Followed by duration units (case-insensitive)\n // - Common units: ms, s, sec, second(s), millisecond(s), µs, us, μs, ns, min, hour(s), hr\n const durationPattern = /\\b(\\d+(?:\\.\\d+)?)\\s*(ms|milliseconds?|s|sec(?:onds?)?|µs|μs|us|microseconds?|ns|nanoseconds?|min(?:utes?)?|h(?:ours?)?|hr)\\b/gi;\n\n const durations: string[] = [];\n let match;\n\n while ((match = durationPattern.exec(line)) !== null) {\n // Reconstruct the full duration string (number + unit, no whitespace)\n const duration = `${match[1]}${match[2]}`;\n if (!durations.includes(duration)) {\n durations.push(duration);\n }\n }\n\n // Limit to 5 durations per line to prevent memory issues\n return durations.slice(0, 5);\n}\n\n/**\n * Built-in regex patterns for common variable types.\n * These are applied in order during preprocessing to mask variables.\n * Order matters: more specific patterns (like timestamps) must run before\n * patterns that could match substrings (like port numbers).\n */\nexport const DEFAULT_PATTERNS: Record<string, RegExp> = {\n // Timestamps (must run before port to avoid fragmentation)\n isoTimestamp: /\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(?:[.,]\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?/g,\n\n // UUID must run before unixTimestamp to prevent partial matching of UUID segments\n uuid: /\\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\b/g,\n\n unixTimestamp: /\\b\\d{10,13}\\b/g,\n\n // Network addresses\n ipv4: /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/g,\n // IPv6: matches full, compressed (::1, ::), and partial forms\n // Order matters: longer matches must come before shorter ones in alternation\n ipv6: /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::)/g,\n port: /:\\d{2,5}\\b/g,\n\n // Identifiers\n hexId: /\\b0x[0-9a-fA-F]+\\b/g,\n blockId: /\\bblk_-?\\d+\\b/g,\n\n // Paths and URLs\n // Note: Don't match version-like paths (e.g., /2.7, /v1.0)\n // Require at least 2 path segments or a segment with letters\n filePath: /(?:\\/[a-zA-Z][\\w.-]*)+/g,\n url: /https?:\\/\\/[^\\s]+/g,\n\n // Long numeric IDs only (6+ digits) - preserves status codes, line numbers\n // Examples masked: request IDs (12345678), order numbers (1234567890)\n // Examples preserved: HTTP 404, line:123, /v2.7\n numericId: /\\b\\d{6,}\\b/g,\n\n // Numbers with optional duration/size suffixes\n // Matches: 1500, 250ms, 1.5s, 100KB, etc.\n numbers: /\\b\\d+(?:\\.\\d+)?(?:ms|s|µs|us|ns|min|h|hr|sec|[KkMmGgTt][Bb]?)?\\b/g,\n};\n\n/**\n * Placeholder used when masking variables.\n */\nexport const WILDCARD = '<*>';\n\n/**\n * Apply a set of patterns to mask variables in a line.\n * Patterns are applied in the order provided.\n */\nexport function applyPatterns(\n line: string,\n patterns: Record<string, RegExp>,\n wildcard: string = WILDCARD\n): string {\n let result = line;\n\n for (const pattern of Object.values(patterns)) {\n // Create a new RegExp instance to reset lastIndex for global patterns\n const regex = new RegExp(pattern.source, pattern.flags);\n result = result.replace(regex, wildcard);\n }\n\n return result;\n}\n","import type { ParsingStrategy } from '../types.js';\nimport { DEFAULT_PATTERNS, applyPatterns, WILDCARD } from './patterns.js';\n\n/**\n * Default parsing strategy for log preprocessing.\n */\nexport const defaultStrategy: ParsingStrategy = {\n /**\n * Preprocess a log line by masking common variable patterns.\n */\n preprocess(line: string): string {\n return applyPatterns(line, DEFAULT_PATTERNS, WILDCARD);\n },\n\n /**\n * Tokenize a line by splitting on whitespace.\n */\n tokenize(line: string): string[] {\n return line.split(/\\s+/).filter((token) => token.length > 0);\n },\n\n /**\n * Get similarity threshold for a given tree depth.\n * Uses a constant threshold of 0.4.\n */\n getSimThreshold(_depth: number): number {\n return 0.4;\n },\n};\n\n/**\n * Create a custom parsing strategy by extending the default.\n */\nexport function defineStrategy(\n overrides: Partial<ParsingStrategy> & {\n patterns?: Record<string, RegExp>;\n }\n): ParsingStrategy {\n const { patterns, ...strategyOverrides } = overrides;\n\n return {\n preprocess: strategyOverrides.preprocess ?? ((line: string) => {\n const mergedPatterns = patterns\n ? { ...DEFAULT_PATTERNS, ...patterns }\n : DEFAULT_PATTERNS;\n return applyPatterns(line, mergedPatterns, WILDCARD);\n }),\n\n tokenize: strategyOverrides.tokenize ?? defaultStrategy.tokenize,\n\n getSimThreshold: strategyOverrides.getSimThreshold ?? defaultStrategy.getSimThreshold,\n };\n}\n","import type { LogCluster } from './cluster.js';\n\n/**\n * A node in the Drain parse tree.\n *\n * V8 Optimization: Uses Map<string, DrainNode> instead of plain objects\n * for dynamic children. This prevents V8 \"dictionary mode\" which would\n * cause 10-100x slower property access.\n *\n * V8 Optimization: All properties are initialized in the constructor\n * to ensure monomorphic object shapes.\n */\nexport class DrainNode {\n /** Depth of this node in the tree (0 = root) */\n readonly depth: number;\n\n /**\n * Child nodes keyed by token value.\n * Using Map instead of Object for V8 optimization.\n */\n readonly children: Map<string, DrainNode>;\n\n /**\n * Clusters (templates) stored at this node.\n * Only leaf nodes contain clusters.\n */\n readonly clusters: LogCluster[];\n\n constructor(depth: number) {\n this.depth = depth;\n this.children = new Map();\n this.clusters = [];\n }\n\n /**\n * Get or create a child node for the given key.\n */\n getOrCreateChild(key: string): DrainNode {\n let child = this.children.get(key);\n if (child === undefined) {\n child = new DrainNode(this.depth + 1);\n this.children.set(key, child);\n }\n return child;\n }\n\n /**\n * Check if this node has a child for the given key.\n */\n hasChild(key: string): boolean {\n return this.children.has(key);\n }\n\n /**\n * Get a child node by key, or undefined if not found.\n */\n getChild(key: string): DrainNode | undefined {\n return this.children.get(key);\n }\n\n /**\n * Add a cluster to this node.\n */\n addCluster(cluster: LogCluster): void {\n this.clusters.push(cluster);\n }\n\n /**\n * Get the number of children.\n */\n get childCount(): number {\n return this.children.size;\n }\n\n /**\n * Get the number of clusters.\n */\n get clusterCount(): number {\n return this.clusters.length;\n }\n}\n","import {\n WILDCARD,\n detectSeverity,\n isStackFrame,\n extractUrls,\n extractFullUrls,\n extractStatusCodes,\n extractCorrelationIds,\n extractDurations,\n} from '../preprocessing/patterns.js';\nimport type { Severity } from '../types.js';\n\n/**\n * Represents a log cluster (template) discovered by the Drain algorithm.\n *\n * V8 Optimization: All properties are initialized in the constructor\n * to ensure monomorphic object shapes for optimal property access.\n */\nexport class LogCluster {\n /** Unique identifier for this cluster */\n readonly id: string;\n\n /** Template tokens (with wildcards for variable positions) */\n readonly tokens: string[];\n\n /** Number of log lines matching this template */\n count: number;\n\n /** Sample variable values from first N matches */\n readonly sampleVariables: string[][];\n\n /** Line index of first occurrence */\n firstSeen: number;\n\n /** Line index of most recent occurrence */\n lastSeen: number;\n\n /** Detected severity level */\n readonly severity: Severity;\n\n /** Sample URLs extracted from matching lines (hostnames only) */\n readonly urlSamples: string[];\n\n /** Full URLs extracted from matching lines (complete paths) */\n readonly fullUrlSamples: string[];\n\n /** HTTP status codes extracted from matching lines */\n readonly statusCodeSamples: number[];\n\n /** Correlation/trace IDs extracted from matching lines */\n readonly correlationIdSamples: string[];\n\n /** Duration/timing values extracted from matching lines */\n readonly durationSamples: string[];\n\n /** Whether this template represents a stack trace frame */\n readonly isStackFrame: boolean;\n\n /** Maximum number of sample variables to store */\n private readonly maxSamples: number;\n\n /** Maximum number of URL samples to store */\n private readonly maxUrlSamples: number = 5;\n\n /** Maximum number of status code samples to store */\n private readonly maxStatusCodeSamples: number = 5;\n\n /** Maximum number of correlation ID samples to store */\n private readonly maxCorrelationIdSamples: number = 3;\n\n /** Maximum number of duration samples to store */\n private readonly maxDurationSamples: number = 5;\n\n constructor(\n id: string,\n tokens: string[],\n lineIndex: number,\n maxSamples: number = 3,\n originalLine: string = ''\n ) {\n this.id = id;\n this.tokens = tokens.slice(); // Defensive copy\n this.count = 1;\n this.sampleVariables = [];\n this.firstSeen = lineIndex;\n this.lastSeen = lineIndex;\n this.maxSamples = maxSamples;\n\n // Detect severity, stack frame from original line\n this.severity = detectSeverity(originalLine);\n this.isStackFrame = isStackFrame(originalLine);\n\n // Initialize sample arrays\n this.urlSamples = [];\n this.fullUrlSamples = [];\n this.statusCodeSamples = [];\n this.correlationIdSamples = [];\n this.durationSamples = [];\n\n // Extract and store samples from original line\n const urls = extractUrls(originalLine);\n if (urls.length > 0) {\n this.urlSamples.push(...urls.slice(0, this.maxUrlSamples));\n }\n\n const fullUrls = extractFullUrls(originalLine);\n if (fullUrls.length > 0) {\n this.fullUrlSamples.push(...fullUrls.slice(0, this.maxUrlSamples));\n }\n\n const statusCodes = extractStatusCodes(originalLine);\n if (statusCodes.length > 0) {\n this.statusCodeSamples.push(...statusCodes.slice(0, this.maxStatusCodeSamples));\n }\n\n const correlationIds = extractCorrelationIds(originalLine);\n if (correlationIds.length > 0) {\n this.correlationIdSamples.push(...correlationIds.slice(0, this.maxCorrelationIdSamples));\n }\n\n const durations = extractDurations(originalLine);\n if (durations.length > 0) {\n this.durationSamples.push(...durations.slice(0, this.maxDurationSamples));\n }\n }\n\n /**\n * Update the cluster with a new matching log line.\n * Returns the variables extracted from this match.\n */\n update(tokens: string[], lineIndex: number, originalLine: string = ''): string[] {\n this.count++;\n this.lastSeen = lineIndex;\n\n // Extract variables (positions where template has wildcard)\n const variables: string[] = [];\n for (let i = 0; i < this.tokens.length && i < tokens.length; i++) {\n if (this.tokens[i] === WILDCARD) {\n variables.push(tokens[i] ?? '');\n }\n }\n\n // Store sample if we haven't reached the limit\n if (this.sampleVariables.length < this.maxSamples) {\n this.sampleVariables.push(variables);\n }\n\n // Extract and store samples from original line if present\n if (originalLine) {\n // URLs (hostnames)\n if (this.urlSamples.length < this.maxUrlSamples) {\n const urls = extractUrls(originalLine);\n for (const url of urls) {\n if (!this.urlSamples.includes(url) && this.urlSamples.length < this.maxUrlSamples) {\n this.urlSamples.push(url);\n }\n }\n }\n\n // Full URLs\n if (this.fullUrlSamples.length < this.maxUrlSamples) {\n const fullUrls = extractFullUrls(originalLine);\n for (const url of fullUrls) {\n if (!this.fullUrlSamples.includes(url) && this.fullUrlSamples.length < this.maxUrlSamples) {\n this.fullUrlSamples.push(url);\n }\n }\n }\n\n // Status codes\n if (this.statusCodeSamples.length < this.maxStatusCodeSamples) {\n const statusCodes = extractStatusCodes(originalLine);\n for (const code of statusCodes) {\n if (!this.statusCodeSamples.includes(code) && this.statusCodeSamples.length < this.maxStatusCodeSamples) {\n this.statusCodeSamples.push(code);\n }\n }\n }\n\n // Correlation IDs\n if (this.correlationIdSamples.length < this.maxCorrelationIdSamples) {\n const correlationIds = extractCorrelationIds(originalLine);\n for (const id of correlationIds) {\n if (!this.correlationIdSamples.includes(id) && this.correlationIdSamples.length < this.maxCorrelationIdSamples) {\n this.correlationIdSamples.push(id);\n }\n }\n }\n\n // Durations\n if (this.durationSamples.length < this.maxDurationSamples) {\n const durations = extractDurations(originalLine);\n for (const duration of durations) {\n if (!this.durationSamples.includes(duration) && this.durationSamples.length < this.maxDurationSamples) {\n this.durationSamples.push(duration);\n }\n }\n }\n }\n\n return variables;\n }\n\n /**\n * Get the template pattern as a string.\n */\n getPattern(): string {\n return this.tokens.join(' ');\n }\n\n /**\n * Compute similarity between this cluster's template and a set of tokens.\n * Returns a value between 0.0 and 1.0.\n */\n computeSimilarity(tokens: string[]): number {\n // Guard against division by zero\n if (this.tokens.length === 0) {\n return 0;\n }\n\n if (tokens.length !== this.tokens.length) {\n return 0;\n }\n\n let matchCount = 0;\n for (let i = 0; i < this.tokens.length; i++) {\n const templateToken = this.tokens[i];\n const inputToken = tokens[i];\n\n // Wildcards always match\n if (templateToken === WILDCARD) {\n matchCount++;\n } else if (templateToken === inputToken) {\n matchCount++;\n }\n }\n\n return matchCount / this.tokens.length;\n }\n\n /**\n * Merge tokens into the template, converting differing positions to wildcards.\n * Mutates the template tokens in place.\n */\n mergeTokens(tokens: string[]): void {\n for (let i = 0; i < this.tokens.length && i < tokens.length; i++) {\n if (this.tokens[i] !== WILDCARD && this.tokens[i] !== tokens[i]) {\n (this.tokens as string[])[i] = WILDCARD;\n }\n }\n }\n}\n","import type { Template, CompressionResult } from '../types.js';\n\n/**\n * Recursively sort object keys for deterministic JSON output.\n * This enables LLM KV-cache hits when the same data is serialized multiple times.\n */\nfunction sortObjectKeys(value: unknown): unknown {\n if (value === null || typeof value !== 'object') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(sortObjectKeys);\n }\n\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(value as Record<string, unknown>).sort();\n for (const key of keys) {\n sorted[key] = sortObjectKeys((value as Record<string, unknown>)[key]);\n }\n return sorted;\n}\n\n/**\n * Format templates as a compact summary.\n */\nexport function formatSummary(\n templates: Template[],\n stats: CompressionResult['stats']\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push('=== Log Compression Summary ===');\n lines.push(\n `Input: ${stats.inputLines.toLocaleString()} lines → ${stats.uniqueTemplates} templates ` +\n `(${(stats.compressionRatio * 100).toFixed(1)}% reduction)`\n );\n lines.push('');\n\n if (templates.length === 0) {\n lines.push('No templates discovered.');\n return lines.join('\\n');\n }\n\n // Top templates by frequency\n lines.push('Top templates by frequency:');\n\n const topTemplates = templates.slice(0, 20);\n topTemplates.forEach((template, index) => {\n const count = template.occurrences.toLocaleString();\n lines.push(`${index + 1}. [${count}x] ${template.pattern}`);\n });\n\n if (templates.length > 20) {\n lines.push(`... and ${templates.length - 20} more templates`);\n }\n\n // Rare events section\n const rareTemplates = templates.filter((t) => t.occurrences <= 5);\n if (rareTemplates.length > 0) {\n lines.push('');\n lines.push(`Rare events (≤5 occurrences): ${rareTemplates.length} templates`);\n\n const shownRare = rareTemplates.slice(0, 5);\n for (const template of shownRare) {\n lines.push(`- [${template.occurrences}x] ${template.pattern}`);\n }\n\n if (rareTemplates.length > 5) {\n lines.push(`... and ${rareTemplates.length - 5} more rare templates`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format templates with full details including sample variables.\n */\nexport function formatDetailed(\n templates: Template[],\n stats: CompressionResult['stats']\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push('=== Log Compression Details ===');\n lines.push(\n `Input: ${stats.inputLines.toLocaleString()} lines → ${stats.uniqueTemplates} templates ` +\n `(${(stats.compressionRatio * 100).toFixed(1)}% reduction)`\n );\n lines.push(`Estimated token reduction: ${(stats.estimatedTokenReduction * 100).toFixed(1)}%`);\n lines.push('');\n\n if (templates.length === 0) {\n lines.push('No templates discovered.');\n return lines.join('\\n');\n }\n\n // Each template with samples\n for (const template of templates) {\n lines.push(`=== Template ${template.id} (${template.occurrences.toLocaleString()} occurrences) ===`);\n lines.push(`Pattern: ${template.pattern}`);\n lines.push(`Severity: ${template.severity}${template.isStackFrame ? ' (stack frame)' : ''}`);\n lines.push(`First seen: line ${template.firstSeen + 1}`);\n lines.push(`Last seen: line ${template.lastSeen + 1}`);\n\n if (template.fullUrlSamples.length > 0) {\n lines.push('URLs:');\n for (const url of template.fullUrlSamples) {\n lines.push(` - ${url}`);\n }\n }\n\n if (template.statusCodeSamples.length > 0) {\n lines.push(`Status codes: ${template.statusCodeSamples.join(', ')}`);\n }\n\n if (template.correlationIdSamples.length > 0) {\n lines.push(`Correlation IDs: ${template.correlationIdSamples.join(', ')}`);\n }\n\n if (template.durationSamples.length > 0) {\n lines.push(`Durations: ${template.durationSamples.join(', ')}`);\n }\n\n if (template.sampleVariables.length > 0) {\n lines.push('Sample variables:');\n for (const vars of template.sampleVariables) {\n if (vars.length > 0) {\n lines.push(` - ${vars.join(', ')}`);\n }\n }\n }\n\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format templates as JSON.\n */\nexport function formatJson(\n templates: Template[],\n stats: CompressionResult['stats']\n): string {\n const output = {\n version: '1.1',\n stats: {\n inputLines: stats.inputLines,\n uniqueTemplates: stats.uniqueTemplates,\n compressionRatio: Math.round(stats.compressionRatio * 1000) / 1000,\n estimatedTokenReduction: Math.round(stats.estimatedTokenReduction * 1000) / 1000,\n },\n templates: templates.map((t) => ({\n id: t.id,\n pattern: t.pattern,\n occurrences: t.occurrences,\n severity: t.severity,\n isStackFrame: t.isStackFrame,\n samples: t.sampleVariables,\n urlSamples: t.urlSamples,\n fullUrlSamples: t.fullUrlSamples,\n statusCodeSamples: t.statusCodeSamples,\n correlationIdSamples: t.correlationIdSamples,\n durationSamples: t.durationSamples,\n firstSeen: t.firstSeen,\n lastSeen: t.lastSeen,\n })),\n };\n\n return JSON.stringify(output, null, 2);\n}\n\n/**\n * Format templates as cache-optimized JSON.\n *\n * Key differences from standard JSON:\n * - Sorted keys at all levels for deterministic output\n * - Compact format (no whitespace) to minimize tokens\n * - Enables LLM prompt caching (KV-cache hits) when sending\n * the same compressed logs multiple times\n */\nexport function formatJsonStable(\n templates: Template[],\n stats: CompressionResult['stats']\n): string {\n const output = {\n stats: {\n compressionRatio: Math.round(stats.compressionRatio * 1000) / 1000,\n estimatedTokenReduction: Math.round(stats.estimatedTokenReduction * 1000) / 1000,\n inputLines: stats.inputLines,\n uniqueTemplates: stats.uniqueTemplates,\n },\n templates: templates.map((t) => ({\n correlationIdSamples: t.correlationIdSamples,\n durationSamples: t.durationSamples,\n firstSeen: t.firstSeen,\n fullUrlSamples: t.fullUrlSamples,\n id: t.id,\n isStackFrame: t.isStackFrame,\n lastSeen: t.lastSeen,\n occurrences: t.occurrences,\n pattern: t.pattern,\n samples: t.sampleVariables,\n severity: t.severity,\n statusCodeSamples: t.statusCodeSamples,\n urlSamples: t.urlSamples,\n })),\n version: '1.1',\n };\n\n // Compact output with sorted keys for cache optimization\n return JSON.stringify(sortObjectKeys(output));\n}\n","import type { DrainOptions, ParsingStrategy, Template, OutputFormat, CompressionResult, ProgressCallback } from '../types.js';\nimport { defaultStrategy } from '../preprocessing/default.js';\nimport { WILDCARD } from '../preprocessing/patterns.js';\nimport { DrainNode } from './node.js';\nimport { LogCluster } from './cluster.js';\nimport { formatSummary, formatDetailed, formatJson, formatJsonStable } from '../output/formatter.js';\n\n/**\n * Default configuration values for Drain algorithm.\n */\nconst DEFAULTS = {\n depth: 4,\n simThreshold: 0.4,\n maxChildren: 100,\n maxClusters: 1000,\n maxSamples: 3,\n} as const;\n\n/**\n * Special key used for the wildcard child in the parse tree.\n */\nconst WILDCARD_KEY = '<WILDCARD>';\n\n/**\n * Drain algorithm implementation for log template mining.\n *\n * The algorithm constructs a fixed-depth parse tree to efficiently\n * cluster log messages by their template structure.\n *\n * Tree Structure:\n * - Level 0 (root): Entry point\n * - Level 1: Token count (length of log message)\n * - Level 2: First token of the message\n * - Levels 3+: Subsequent tokens up to configured depth\n * - Leaf: LogCluster containing the template\n */\nexport class Drain {\n private readonly root: DrainNode;\n private readonly clusters: LogCluster[];\n private readonly strategy: ParsingStrategy;\n private readonly depth: number;\n private readonly maxChildren: number;\n private readonly maxClusters: number;\n private readonly maxSamples: number;\n private readonly onProgress: ProgressCallback | undefined;\n private lineCount: number;\n private nextClusterId: number;\n\n constructor(options: DrainOptions = {}) {\n this.root = new DrainNode(0);\n this.clusters = [];\n this.strategy = options.preprocessing ?? defaultStrategy;\n this.depth = options.depth ?? DEFAULTS.depth;\n this.maxChildren = options.maxChildren ?? DEFAULTS.maxChildren;\n this.maxClusters = options.maxClusters ?? DEFAULTS.maxClusters;\n this.maxSamples = options.maxSamples ?? DEFAULTS.maxSamples;\n this.onProgress = options.onProgress;\n this.lineCount = 0;\n this.nextClusterId = 1;\n }\n\n /**\n * Process a single log line.\n */\n addLogLine(line: string): LogCluster | null {\n const lineIndex = this.lineCount++;\n\n // Skip empty lines\n const trimmed = line.trim();\n if (trimmed.length === 0) {\n return null;\n }\n\n // Preprocess and tokenize\n const preprocessed = this.strategy.preprocess(trimmed);\n const tokens = this.strategy.tokenize(preprocessed);\n\n if (tokens.length === 0) {\n return null;\n }\n\n // Search for matching cluster\n const matchedCluster = this.treeSearch(tokens);\n\n if (matchedCluster !== null) {\n // Update existing cluster, passing original line for URL extraction\n matchedCluster.update(tokens, lineIndex, trimmed);\n matchedCluster.mergeTokens(tokens);\n return matchedCluster;\n }\n\n // Create new cluster if under limit\n if (this.clusters.length >= this.maxClusters) {\n return null;\n }\n\n return this.createCluster(tokens, lineIndex, trimmed);\n }\n\n /**\n * Process multiple log lines with optional progress reporting.\n */\n addLogLines(lines: string[]): void {\n const total = lines.length;\n\n // Calculate report interval (emit at most 100 progress events)\n const reportInterval = Math.max(1, Math.floor(total / 100));\n\n // Emit initial parsing phase\n if (this.onProgress && total > 0) {\n this.onProgress({\n processedLines: 0,\n totalLines: total,\n currentPhase: 'parsing',\n percentComplete: 0,\n });\n }\n\n for (let i = 0; i < total; i++) {\n this.addLogLine(lines[i] as string);\n\n // Emit progress at intervals\n if (this.onProgress && i % reportInterval === 0) {\n this.onProgress({\n processedLines: i + 1,\n totalLines: total,\n currentPhase: 'clustering',\n percentComplete: Math.round(((i + 1) / total) * 100),\n });\n }\n }\n\n // Emit final progress event\n if (this.onProgress && total > 0) {\n this.onProgress({\n processedLines: total,\n totalLines: total,\n currentPhase: 'finalizing',\n percentComplete: 100,\n });\n }\n }\n\n /**\n * Search the parse tree for a matching cluster.\n *\n * Uses XDrain-inspired token-position fallback: if the first token\n * looks like a variable (timestamp, PID, etc.), tries using the\n * second token as the navigation key for better matching.\n */\n private treeSearch(tokens: string[]): LogCluster | null {\n const tokenCount = tokens.length;\n const tokenCountKey = String(tokenCount);\n\n // Level 1: Navigate by token count\n const lengthNode = this.root.getChild(tokenCountKey);\n if (lengthNode === undefined) {\n return null;\n }\n\n // Try primary search with first token\n const primaryResult = this.treeSearchFromToken(lengthNode, tokens, 0);\n if (primaryResult !== null) {\n return primaryResult;\n }\n\n // XDrain-inspired fallback: if first token looks like a variable,\n // try searching with second token as the navigation key\n const firstToken = tokens[0];\n if (\n tokens.length > 1 &&\n firstToken !== undefined &&\n this.looksLikeVariable(firstToken)\n ) {\n return this.treeSearchFromToken(lengthNode, tokens, 1);\n }\n\n return null;\n }\n\n /**\n * Search from a specific token position in the tree.\n * @param lengthNode - The node at level 1 (token count)\n * @param tokens - All tokens in the log line\n * @param startIndex - Which token to use as \"first token\" for navigation\n */\n private treeSearchFromToken(\n lengthNode: DrainNode,\n tokens: string[],\n startIndex: number\n ): LogCluster | null {\n // Level 2: Navigate by token at startIndex\n const navToken = tokens[startIndex];\n if (navToken === undefined) {\n return null;\n }\n\n let currentNode = lengthNode.getChild(navToken);\n\n // Only use wildcard fallback for primary search (startIndex=0).\n // For alternate token searches, wildcard path is already covered by\n // primary search and would have misaligned traversal indices.\n if (currentNode === undefined && startIndex === 0) {\n currentNode = lengthNode.getChild(WILDCARD_KEY);\n }\n\n if (currentNode === undefined) {\n return null;\n }\n\n // Levels 3+: Navigate by subsequent tokens\n // Use this.depth as upper bound (not this.depth + startIndex) to match\n // the tree structure created during insertion\n let searchNode: DrainNode = currentNode;\n for (let i = startIndex + 1; i < Math.min(tokens.length, this.depth); i++) {\n const token = tokens[i];\n if (token === undefined) {\n break;\n }\n\n let nextNode = searchNode.getChild(token);\n\n // Try wildcard child if exact match not found\n if (nextNode === undefined) {\n nextNode = searchNode.getChild(WILDCARD_KEY);\n }\n\n if (nextNode === undefined) {\n break;\n }\n\n searchNode = nextNode;\n }\n\n // Search clusters at this node for best match\n return this.findBestMatch(searchNode, tokens);\n }\n\n /**\n * Find the best matching cluster at a node.\n */\n private findBestMatch(node: DrainNode, tokens: string[]): LogCluster | null {\n let bestCluster: LogCluster | null = null;\n let bestSimilarity = 0;\n\n const threshold = this.strategy.getSimThreshold(node.depth);\n\n for (const cluster of node.clusters) {\n const similarity = cluster.computeSimilarity(tokens);\n\n if (similarity >= threshold && similarity > bestSimilarity) {\n bestSimilarity = similarity;\n bestCluster = cluster;\n }\n }\n\n return bestCluster;\n }\n\n /**\n * Create a new cluster and add it to the tree.\n */\n private createCluster(tokens: string[], lineIndex: number, originalLine: string = ''): LogCluster {\n const clusterId = `t${String(this.nextClusterId++).padStart(3, '0')}`;\n const cluster = new LogCluster(clusterId, tokens, lineIndex, this.maxSamples, originalLine);\n\n // Navigate/create path in tree\n const tokenCount = tokens.length;\n const tokenCountKey = String(tokenCount);\n\n // Level 1: Token count\n const lengthNode = this.root.getOrCreateChild(tokenCountKey);\n\n // Level 2: First token\n const firstToken = tokens[0];\n if (firstToken === undefined) {\n this.clusters.push(cluster);\n return cluster;\n }\n\n // Decide whether to use actual token or wildcard key\n const firstKey = this.shouldUseWildcard(lengthNode, firstToken)\n ? WILDCARD_KEY\n : firstToken;\n\n let currentNode = lengthNode.getOrCreateChild(firstKey);\n\n // Levels 3+: Subsequent tokens\n for (let i = 1; i < Math.min(tokens.length, this.depth); i++) {\n const token = tokens[i];\n if (token === undefined) {\n break;\n }\n\n const key = this.shouldUseWildcard(currentNode, token)\n ? WILDCARD_KEY\n : token;\n\n currentNode = currentNode.getOrCreateChild(key);\n }\n\n // Add cluster to leaf node\n currentNode.addCluster(cluster);\n this.clusters.push(cluster);\n\n return cluster;\n }\n\n /**\n * Determine if we should use a wildcard key for a token.\n * Uses maxChildren limit to prevent tree explosion.\n */\n private shouldUseWildcard(node: DrainNode, token: string): boolean {\n // If token already exists as child, use it\n if (node.hasChild(token)) {\n return false;\n }\n\n // If we have wildcard child and are at capacity, use wildcard\n if (node.hasChild(WILDCARD_KEY) && node.childCount >= this.maxChildren) {\n return true;\n }\n\n // If token looks like a variable (starts with digit, etc.), use wildcard\n if (this.looksLikeVariable(token)) {\n return true;\n }\n\n // Otherwise use the actual token\n return false;\n }\n\n /**\n * Heuristic to detect if a token looks like a variable value.\n */\n private looksLikeVariable(token: string): boolean {\n // Already a wildcard\n if (token === WILDCARD) {\n return true;\n }\n\n // Starts with a digit\n const firstChar = token.charAt(0);\n if (firstChar >= '0' && firstChar <= '9') {\n return true;\n }\n\n // Contains only hex characters (likely an ID)\n if (/^[0-9a-fA-F]+$/.test(token) && token.length > 8) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Get all discovered templates.\n */\n getTemplates(): Template[] {\n return this.clusters.map((cluster) => ({\n id: cluster.id,\n pattern: cluster.getPattern(),\n occurrences: cluster.count,\n sampleVariables: cluster.sampleVariables,\n firstSeen: cluster.firstSeen,\n lastSeen: cluster.lastSeen,\n severity: cluster.severity,\n urlSamples: cluster.urlSamples,\n fullUrlSamples: cluster.fullUrlSamples,\n statusCodeSamples: cluster.statusCodeSamples,\n correlationIdSamples: cluster.correlationIdSamples,\n durationSamples: cluster.durationSamples,\n isStackFrame: cluster.isStackFrame,\n }));\n }\n\n /**\n * Get compression result with formatted output.\n */\n getResult(format: OutputFormat = 'summary', maxTemplates: number = 50): CompressionResult {\n const templates = this.getTemplates();\n\n // Sort by occurrences (descending)\n templates.sort((a, b) => b.occurrences - a.occurrences);\n\n // Limit templates in output\n const limitedTemplates = templates.slice(0, maxTemplates);\n\n // Calculate stats\n const stats = this.calculateStats(templates);\n\n // Format output\n let formatted: string;\n switch (format) {\n case 'detailed':\n formatted = formatDetailed(limitedTemplates, stats);\n break;\n case 'json':\n formatted = formatJson(limitedTemplates, stats);\n break;\n case 'json-stable':\n formatted = formatJsonStable(limitedTemplates, stats);\n break;\n case 'summary':\n default:\n formatted = formatSummary(limitedTemplates, stats);\n break;\n }\n\n return {\n templates: limitedTemplates,\n stats,\n formatted,\n };\n }\n\n /**\n * Calculate compression statistics.\n */\n private calculateStats(templates: Template[]): CompressionResult['stats'] {\n const inputLines = this.lineCount;\n const uniqueTemplates = templates.length;\n\n // Compression ratio: 1 - (templates / lines)\n const compressionRatio = inputLines > 0\n ? 1 - (uniqueTemplates / inputLines)\n : 0;\n\n // Estimate token reduction using character count proxy\n // Each template is shown once instead of repeated N times\n let originalChars = 0;\n let compressedChars = 0;\n\n for (const template of templates) {\n const patternLength = template.pattern.length;\n // Original: pattern repeated for each occurrence\n originalChars += patternLength * template.occurrences;\n // Compressed: pattern shown once + count indicator\n compressedChars += patternLength + 20; // ~20 chars for \"[Nx] \" prefix\n }\n\n const estimatedTokenReduction = originalChars > 0\n ? 1 - (compressedChars / originalChars)\n : 0;\n\n return {\n inputLines,\n uniqueTemplates,\n compressionRatio: Math.max(0, Math.min(1, compressionRatio)),\n estimatedTokenReduction: Math.max(0, Math.min(1, estimatedTokenReduction)),\n };\n }\n\n /**\n * Get the number of lines processed.\n */\n get totalLines(): number {\n return this.lineCount;\n }\n\n /**\n * Get the number of clusters (templates) discovered.\n */\n get totalClusters(): number {\n return this.clusters.length;\n }\n}\n\n/**\n * Create a new Drain instance with the given options.\n */\nexport function createDrain(options?: DrainOptions): Drain {\n return new Drain(options);\n}\n","import type { CompressOptions, CompressionResult } from './types.js';\nimport { createDrain } from './drain/drain.js';\n\n/**\n * Compress log lines by extracting templates.\n *\n * This is the main entry point for simple use cases.\n * For more control, use `createDrain()` directly.\n *\n * @param lines - Array of log lines to compress\n * @param options - Compression options\n * @returns Compression result with templates and statistics\n *\n * @example\n * ```typescript\n * import { compress } from 'logpare';\n *\n * const logs = [\n * 'Connection from 192.168.1.1 established',\n * 'Connection from 192.168.1.2 established',\n * 'Connection from 10.0.0.1 established',\n * ];\n *\n * const result = compress(logs);\n * console.log(result.formatted);\n * // Output: [3x] Connection from <*> established\n * ```\n */\nexport function compress(\n lines: string[],\n options: CompressOptions = {}\n): CompressionResult {\n const { format = 'summary', maxTemplates = 50, drain: drainOptions } = options;\n\n const startTime = performance.now();\n\n const drain = createDrain(drainOptions);\n drain.addLogLines(lines);\n\n const result = drain.getResult(format, maxTemplates);\n const processingTimeMs = Math.round(performance.now() - startTime);\n\n // Add processing time to stats\n return {\n ...result,\n stats: {\n ...result.stats,\n processingTimeMs,\n },\n };\n}\n\n/**\n * Compress a single string containing multiple log lines.\n *\n * @param text - Raw log text (lines separated by newlines)\n * @param options - Compression options\n * @returns Compression result with templates and statistics\n */\nexport function compressText(\n text: string,\n options: CompressOptions = {}\n): CompressionResult {\n const lines = text.split(/\\r?\\n/);\n return compress(lines, options);\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.cjs
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
var import_node_util = require("util");
|
|
6
6
|
var import_node_fs = require("fs");
|
|
7
|
-
var import_node_url = require("url");
|
|
8
|
-
var import_node_path = require("path");
|
|
9
7
|
|
|
10
8
|
// src/preprocessing/patterns.ts
|
|
11
9
|
var SEVERITY_PATTERNS = {
|
|
@@ -114,15 +112,18 @@ function extractDurations(line) {
|
|
|
114
112
|
return durations.slice(0, 5);
|
|
115
113
|
}
|
|
116
114
|
var DEFAULT_PATTERNS = {
|
|
117
|
-
// Timestamps (
|
|
115
|
+
// Timestamps (must run before port to avoid fragmentation)
|
|
118
116
|
isoTimestamp: /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g,
|
|
117
|
+
// UUID must run before unixTimestamp to prevent partial matching of UUID segments
|
|
118
|
+
uuid: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
|
|
119
119
|
unixTimestamp: /\b\d{10,13}\b/g,
|
|
120
120
|
// Network addresses
|
|
121
121
|
ipv4: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
|
|
122
|
-
|
|
122
|
+
// IPv6: matches full, compressed (::1, ::), and partial forms
|
|
123
|
+
// Order matters: longer matches must come before shorter ones in alternation
|
|
124
|
+
ipv6: /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::)/g,
|
|
123
125
|
port: /:\d{2,5}\b/g,
|
|
124
126
|
// Identifiers
|
|
125
|
-
uuid: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
|
|
126
127
|
hexId: /\b0x[0-9a-fA-F]+\b/g,
|
|
127
128
|
blockId: /\bblk_-?\d+\b/g,
|
|
128
129
|
// Paths and URLs
|
|
@@ -410,6 +411,20 @@ var LogCluster = class {
|
|
|
410
411
|
};
|
|
411
412
|
|
|
412
413
|
// src/output/formatter.ts
|
|
414
|
+
function sortObjectKeys(value) {
|
|
415
|
+
if (value === null || typeof value !== "object") {
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
if (Array.isArray(value)) {
|
|
419
|
+
return value.map(sortObjectKeys);
|
|
420
|
+
}
|
|
421
|
+
const sorted = {};
|
|
422
|
+
const keys = Object.keys(value).sort();
|
|
423
|
+
for (const key of keys) {
|
|
424
|
+
sorted[key] = sortObjectKeys(value[key]);
|
|
425
|
+
}
|
|
426
|
+
return sorted;
|
|
427
|
+
}
|
|
413
428
|
function formatSummary(templates, stats) {
|
|
414
429
|
const lines = [];
|
|
415
430
|
lines.push("=== Log Compression Summary ===");
|
|
@@ -516,6 +531,33 @@ function formatJson(templates, stats) {
|
|
|
516
531
|
};
|
|
517
532
|
return JSON.stringify(output, null, 2);
|
|
518
533
|
}
|
|
534
|
+
function formatJsonStable(templates, stats) {
|
|
535
|
+
const output = {
|
|
536
|
+
stats: {
|
|
537
|
+
compressionRatio: Math.round(stats.compressionRatio * 1e3) / 1e3,
|
|
538
|
+
estimatedTokenReduction: Math.round(stats.estimatedTokenReduction * 1e3) / 1e3,
|
|
539
|
+
inputLines: stats.inputLines,
|
|
540
|
+
uniqueTemplates: stats.uniqueTemplates
|
|
541
|
+
},
|
|
542
|
+
templates: templates.map((t) => ({
|
|
543
|
+
correlationIdSamples: t.correlationIdSamples,
|
|
544
|
+
durationSamples: t.durationSamples,
|
|
545
|
+
firstSeen: t.firstSeen,
|
|
546
|
+
fullUrlSamples: t.fullUrlSamples,
|
|
547
|
+
id: t.id,
|
|
548
|
+
isStackFrame: t.isStackFrame,
|
|
549
|
+
lastSeen: t.lastSeen,
|
|
550
|
+
occurrences: t.occurrences,
|
|
551
|
+
pattern: t.pattern,
|
|
552
|
+
samples: t.sampleVariables,
|
|
553
|
+
severity: t.severity,
|
|
554
|
+
statusCodeSamples: t.statusCodeSamples,
|
|
555
|
+
urlSamples: t.urlSamples
|
|
556
|
+
})),
|
|
557
|
+
version: "1.1"
|
|
558
|
+
};
|
|
559
|
+
return JSON.stringify(sortObjectKeys(output));
|
|
560
|
+
}
|
|
519
561
|
|
|
520
562
|
// src/drain/drain.ts
|
|
521
563
|
var DEFAULTS = {
|
|
@@ -580,6 +622,14 @@ var Drain = class {
|
|
|
580
622
|
addLogLines(lines) {
|
|
581
623
|
const total = lines.length;
|
|
582
624
|
const reportInterval = Math.max(1, Math.floor(total / 100));
|
|
625
|
+
if (this.onProgress && total > 0) {
|
|
626
|
+
this.onProgress({
|
|
627
|
+
processedLines: 0,
|
|
628
|
+
totalLines: total,
|
|
629
|
+
currentPhase: "parsing",
|
|
630
|
+
percentComplete: 0
|
|
631
|
+
});
|
|
632
|
+
}
|
|
583
633
|
for (let i = 0; i < total; i++) {
|
|
584
634
|
this.addLogLine(lines[i]);
|
|
585
635
|
if (this.onProgress && i % reportInterval === 0) {
|
|
@@ -602,6 +652,10 @@ var Drain = class {
|
|
|
602
652
|
}
|
|
603
653
|
/**
|
|
604
654
|
* Search the parse tree for a matching cluster.
|
|
655
|
+
*
|
|
656
|
+
* Uses XDrain-inspired token-position fallback: if the first token
|
|
657
|
+
* looks like a variable (timestamp, PID, etc.), tries using the
|
|
658
|
+
* second token as the navigation key for better matching.
|
|
605
659
|
*/
|
|
606
660
|
treeSearch(tokens) {
|
|
607
661
|
const tokenCount = tokens.length;
|
|
@@ -610,19 +664,36 @@ var Drain = class {
|
|
|
610
664
|
if (lengthNode === void 0) {
|
|
611
665
|
return null;
|
|
612
666
|
}
|
|
667
|
+
const primaryResult = this.treeSearchFromToken(lengthNode, tokens, 0);
|
|
668
|
+
if (primaryResult !== null) {
|
|
669
|
+
return primaryResult;
|
|
670
|
+
}
|
|
613
671
|
const firstToken = tokens[0];
|
|
614
|
-
if (firstToken
|
|
672
|
+
if (tokens.length > 1 && firstToken !== void 0 && this.looksLikeVariable(firstToken)) {
|
|
673
|
+
return this.treeSearchFromToken(lengthNode, tokens, 1);
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Search from a specific token position in the tree.
|
|
679
|
+
* @param lengthNode - The node at level 1 (token count)
|
|
680
|
+
* @param tokens - All tokens in the log line
|
|
681
|
+
* @param startIndex - Which token to use as "first token" for navigation
|
|
682
|
+
*/
|
|
683
|
+
treeSearchFromToken(lengthNode, tokens, startIndex) {
|
|
684
|
+
const navToken = tokens[startIndex];
|
|
685
|
+
if (navToken === void 0) {
|
|
615
686
|
return null;
|
|
616
687
|
}
|
|
617
|
-
let currentNode = lengthNode.getChild(
|
|
618
|
-
if (currentNode === void 0) {
|
|
688
|
+
let currentNode = lengthNode.getChild(navToken);
|
|
689
|
+
if (currentNode === void 0 && startIndex === 0) {
|
|
619
690
|
currentNode = lengthNode.getChild(WILDCARD_KEY);
|
|
620
691
|
}
|
|
621
692
|
if (currentNode === void 0) {
|
|
622
693
|
return null;
|
|
623
694
|
}
|
|
624
695
|
let searchNode = currentNode;
|
|
625
|
-
for (let i = 1; i < Math.min(tokens.length, this.depth); i++) {
|
|
696
|
+
for (let i = startIndex + 1; i < Math.min(tokens.length, this.depth); i++) {
|
|
626
697
|
const token = tokens[i];
|
|
627
698
|
if (token === void 0) {
|
|
628
699
|
break;
|
|
@@ -750,6 +821,9 @@ var Drain = class {
|
|
|
750
821
|
case "json":
|
|
751
822
|
formatted = formatJson(limitedTemplates, stats);
|
|
752
823
|
break;
|
|
824
|
+
case "json-stable":
|
|
825
|
+
formatted = formatJsonStable(limitedTemplates, stats);
|
|
826
|
+
break;
|
|
753
827
|
case "summary":
|
|
754
828
|
default:
|
|
755
829
|
formatted = formatSummary(limitedTemplates, stats);
|
|
@@ -822,10 +896,7 @@ function compressText(text, options = {}) {
|
|
|
822
896
|
}
|
|
823
897
|
|
|
824
898
|
// src/cli.ts
|
|
825
|
-
var
|
|
826
|
-
var __dirname = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
827
|
-
var pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
828
|
-
var VERSION = pkg.version;
|
|
899
|
+
var VERSION = true ? "0.1.0" : "0.0.0";
|
|
829
900
|
var HELP = `
|
|
830
901
|
logpare - Semantic log compression for LLM context windows
|
|
831
902
|
|
|
@@ -834,7 +905,7 @@ USAGE:
|
|
|
834
905
|
cat logs.txt | logpare [options]
|
|
835
906
|
|
|
836
907
|
OPTIONS:
|
|
837
|
-
-f, --format <fmt> Output format: summary, detailed, json (default: summary)
|
|
908
|
+
-f, --format <fmt> Output format: summary, detailed, json, json-stable (default: summary)
|
|
838
909
|
-o, --output <file> Write output to file instead of stdout
|
|
839
910
|
-d, --depth <n> Parse tree depth (default: 4)
|
|
840
911
|
-t, --threshold <n> Similarity threshold 0.0-1.0 (default: 0.4)
|
|
@@ -873,9 +944,9 @@ function parseCliArgs() {
|
|
|
873
944
|
allowPositionals: true
|
|
874
945
|
});
|
|
875
946
|
const format = values.format;
|
|
876
|
-
if (!["summary", "detailed", "json"].includes(format)) {
|
|
947
|
+
if (!["summary", "detailed", "json", "json-stable"].includes(format)) {
|
|
877
948
|
console.error(
|
|
878
|
-
`Error: Invalid format "${format}". Use: summary, detailed, json`
|
|
949
|
+
`Error: Invalid format "${format}". Use: summary, detailed, json, json-stable`
|
|
879
950
|
);
|
|
880
951
|
process.exit(1);
|
|
881
952
|
}
|
|
@@ -974,11 +1045,7 @@ function main() {
|
|
|
974
1045
|
}
|
|
975
1046
|
};
|
|
976
1047
|
const result = compressText(input, options);
|
|
977
|
-
const output =
|
|
978
|
-
{ templates: result.templates, stats: result.stats },
|
|
979
|
-
null,
|
|
980
|
-
2
|
|
981
|
-
) : result.formatted;
|
|
1048
|
+
const output = result.formatted;
|
|
982
1049
|
if (args.output) {
|
|
983
1050
|
(0, import_node_fs.writeFileSync)(args.output, output, "utf-8");
|
|
984
1051
|
console.error(`Output written to ${args.output}`);
|