-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsrc-services-log-parser-service.ts
More file actions
216 lines (192 loc) · 5.33 KB
/
src-services-log-parser-service.ts
File metadata and controls
216 lines (192 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import { Line, Command } from "./line.js";
/**
* Pointer to a line in the parsed log, either a direct index or [groupIndex, childIndex] tuple
*/
export type LinePointer = number | [number, number];
/**
* Main parser class for processing GitHub Actions log streams
*/
export class LogParser {
counter: number;
seenIDs: Set<string>;
search: string;
matches: number;
lines: Line[];
_visibleLines?: LinePointer[];
onVisibleLinesChange?: (lines?: LinePointer[]) => void;
constructor() {
this.counter = 1;
this.seenIDs = new Set();
this.search = "";
this.matches = 0;
this.lines = [];
}
/**
* Resets the parser to its initial state
*/
reset() {
this.counter = 1;
this.seenIDs = new Set();
this.matches = 0;
this.lines = [];
this.resetVisibleLines();
}
/**
* Get an array of pointers to visible lines, taking into account group state
* @returns Array of line pointers indicating which lines are currently visible
*/
getVisibleLines(): LinePointer[] {
if (this._visibleLines) {
return this._visibleLines;
}
// build a mapping of visible lines to their locations in the array for fast lookups
this._visibleLines = [];
for (let i = 0; i < this.lines.length; i++) {
this._visibleLines.push(i);
const line = this.lines[i];
if (line.group?.open) {
// if we're in an open group, add all of its children
for (let j = 0; j < line.group.children.length; j++) {
this._visibleLines.push([i, j]);
}
continue;
}
}
this.onVisibleLinesChange?.(this._visibleLines);
return this._visibleLines;
}
/**
* Gets a visible line by its index in the visible lines array
* @param idx - Index in the visible lines array
* @returns The line at the specified visible index, or undefined if not found
*/
getVisibleLine(idx: number): Line | undefined {
const mapping = this.getVisibleLines()[idx];
if (typeof mapping === "undefined") {
return;
}
if (typeof mapping === "number") {
return this.lines[mapping];
}
return this.lines[mapping[0]].group?.children[mapping[1]];
}
/**
* Resets the visible lines cache and notifies listeners
*/
resetVisibleLines() {
this._visibleLines = undefined;
this.onVisibleLinesChange?.(this.getVisibleLines());
}
/**
* Adds a new log line to the parser
* @param raw - The raw log line string
* @param id - Optional unique identifier for the line
*/
add(raw: string, id?: string) {
if (id) {
if (this.seenIDs.has(id)) {
return;
}
this.seenIDs.add(id);
}
const line = new Line(this.counter, raw, id);
if (this.search) {
line.highlight(this.search);
}
switch (line.cmd) {
case Command.EndGroup: {
if (this.inGroup()) {
this.endGroup();
// don't add endgroup lines when they properly close a group
// also don't increment the index
return;
}
// if not in group, treat endgroup as a regular line
this.lines.push(line);
break;
}
case Command.Group: {
// add a callback to reset visible lines when the group changes
line.group!.onGroupChange(() => this.resetVisibleLines());
// we'll want to close any open group
this.endGroup();
this.lines.push(line);
break;
}
default: {
if (this.inGroup()) {
// in a group, add it to said group
line.parent = this.last();
this.last()?.group!.children.push(line);
} else {
// otherwise, add it as a regular line
this.lines.push(line);
}
}
}
this.counter++;
this.matches += line.highlights.size;
this.resetVisibleLines();
}
/**
* Adds multiple log lines from a raw string (split by newlines)
* @param raw - Raw string containing multiple log lines
*/
addRaw(raw: string) {
raw.split("\n").forEach((line) => this.add(line));
}
/**
* Gets the last added line
* @returns The most recently added line
*/
last(): Line {
return this.lines[this.lines.length - 1];
}
/**
* Checks if the parser is currently in an open group
* @returns True if currently in an open group, false otherwise
*/
inGroup() {
let group = this.last()?.group;
return group && !group.ended;
}
/**
* Ends the current group if one is open
*/
endGroup() {
if (this.inGroup()) {
this.last().group!.end();
}
}
/**
* Sets the search term and highlights matching text in all lines
* @param search - The search string to highlight
* @returns The total number of matches found
*/
setSearch(search: string): number {
this.matches = 0;
this.search = search;
for (let line of this.lines) {
this.matches += line.highlight(search);
}
return this.matches;
}
/**
* Gets the current number of search matches
* @returns The total number of search matches across all lines
*/
getMatches(): number {
return this.matches;
}
}
}
/**
* Parse multiple log entries from text
*/
parseLogFile(content: string): LogEntry[] {
const lines = content.split('\n').filter(line => line.trim());
return lines
.map(line => this.parseLogEntry(line))
.filter((entry): entry is LogEntry => entry !== null);
}
}