@pilaf/backends 1.0.2 → 1.2.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/README.md +1170 -10
- package/lib/collectors/DockerLogCollector.js +334 -0
- package/lib/collectors/index.js +9 -0
- package/lib/core/CommandRouter.js +154 -0
- package/lib/core/CorrelationStrategy.js +172 -0
- package/lib/core/LogCollector.js +194 -0
- package/lib/core/LogParser.js +125 -0
- package/lib/core/index.js +26 -0
- package/lib/errors/index.js +363 -0
- package/lib/helpers/EventObserver.js +267 -0
- package/lib/helpers/QueryHelper.js +279 -0
- package/lib/helpers/index.js +13 -0
- package/lib/index.js +72 -1
- package/lib/mineflayer-backend.js +266 -1
- package/lib/monitoring/CircularBuffer.js +202 -0
- package/lib/monitoring/LogMonitor.js +303 -0
- package/lib/monitoring/correlations/TagCorrelationStrategy.js +214 -0
- package/lib/monitoring/correlations/UsernameCorrelationStrategy.js +233 -0
- package/lib/monitoring/correlations/index.js +13 -0
- package/lib/monitoring/index.js +16 -0
- package/lib/parsers/MinecraftLogParser.js +512 -0
- package/lib/parsers/PatternRegistry.js +366 -0
- package/lib/parsers/fixtures/minecraft-logs.js +188 -0
- package/lib/parsers/index.js +13 -0
- package/lib/rcon-backend.js +42 -26
- package/package.json +2 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PatternRegistry - Centralized pattern management for log parsing
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities (MECE):
|
|
5
|
+
* - ONLY: Store, compile, and match regex patterns
|
|
6
|
+
* - NOT: Parse log lines (LogParser's responsibility - uses this registry)
|
|
7
|
+
* - NOT: Emit events (LogMonitor's responsibility)
|
|
8
|
+
*
|
|
9
|
+
* Design Principles:
|
|
10
|
+
* - Lazy compilation: Regex compiled on first use (performance)
|
|
11
|
+
* - Priority ordering: Patterns tested in order of addition
|
|
12
|
+
* - Named capture groups: Support for extracting named data
|
|
13
|
+
* - Thread-safe: Safe for concurrent access
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const registry = new PatternRegistry();
|
|
17
|
+
* registry.addPattern('teleport', /Teleported (\w+) to (.+)/, (match) => ({
|
|
18
|
+
* player: match[1],
|
|
19
|
+
* destination: match[2]
|
|
20
|
+
* }));
|
|
21
|
+
* const result = registry.match(logLine);
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
class PatternRegistry {
|
|
25
|
+
/**
|
|
26
|
+
* Create a PatternRegistry
|
|
27
|
+
* @param {Object} options - Registry options
|
|
28
|
+
* @param {boolean} [options.caseInsensitive=false] - Make all patterns case-insensitive
|
|
29
|
+
*/
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
/**
|
|
32
|
+
* Map of pattern name to pattern definition
|
|
33
|
+
* @private
|
|
34
|
+
* @type {Map<string, Object>}
|
|
35
|
+
*/
|
|
36
|
+
this._patterns = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ordered array of pattern names (for priority ordering)
|
|
40
|
+
* @private
|
|
41
|
+
* @type {Array<string>}
|
|
42
|
+
*/
|
|
43
|
+
this._patternOrder = [];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compiled regex cache
|
|
47
|
+
* @private
|
|
48
|
+
* @type {Map<string, RegExp>}
|
|
49
|
+
*/
|
|
50
|
+
this._compiled = new Map();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Case-insensitive flag
|
|
54
|
+
* @private
|
|
55
|
+
* @type {boolean}
|
|
56
|
+
*/
|
|
57
|
+
this._caseInsensitive = options?.caseInsensitive || false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add a pattern to the registry
|
|
62
|
+
*
|
|
63
|
+
* @param {string} name - Unique pattern name/identifier
|
|
64
|
+
* @param {RegExp|string} pattern - Regex pattern or string (will be converted to RegExp)
|
|
65
|
+
* @param {Function} handler - Handler function: (match: RegExpMatchArray) => Object
|
|
66
|
+
* @param {number} [priority] - Optional priority (lower = higher priority, default: append to end)
|
|
67
|
+
* @returns {void}
|
|
68
|
+
* @throws {Error} If pattern name already exists
|
|
69
|
+
* @throws {Error} If pattern is invalid
|
|
70
|
+
* @throws {Error} If handler is not a function
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Add with regex
|
|
74
|
+
* registry.addPattern('teleport', /Teleported (\w+) to (.+)/, (match) => ({
|
|
75
|
+
* player: match[1],
|
|
76
|
+
* position: match[2]
|
|
77
|
+
* }));
|
|
78
|
+
*
|
|
79
|
+
* // Add with string (converted to regex)
|
|
80
|
+
* registry.addPattern('join', 'UUID of player .* is (.+)', (match) => ({
|
|
81
|
+
* username: match[1]
|
|
82
|
+
* }));
|
|
83
|
+
*/
|
|
84
|
+
addPattern(name, pattern, handler, priority) {
|
|
85
|
+
// Validate name
|
|
86
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
87
|
+
throw new Error('Pattern name must be a non-empty string');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check for duplicate
|
|
91
|
+
if (this._patterns.has(name)) {
|
|
92
|
+
throw new Error(`Pattern "${name}" already exists`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Validate pattern
|
|
96
|
+
if (!(pattern instanceof RegExp) && typeof pattern !== 'string') {
|
|
97
|
+
throw new Error('Pattern must be a RegExp or string');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate handler
|
|
101
|
+
if (typeof handler !== 'function') {
|
|
102
|
+
throw new Error('Handler must be a function');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Convert string to RegExp if needed
|
|
106
|
+
let regexPattern = pattern;
|
|
107
|
+
if (typeof pattern === 'string') {
|
|
108
|
+
const flags = this._caseInsensitive ? 'i' : '';
|
|
109
|
+
regexPattern = new RegExp(pattern, flags);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Store pattern definition
|
|
113
|
+
this._patterns.set(name, {
|
|
114
|
+
name,
|
|
115
|
+
pattern: regexPattern,
|
|
116
|
+
handler,
|
|
117
|
+
priority: priority ?? this._patternOrder.length
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Update order
|
|
121
|
+
if (priority !== undefined) {
|
|
122
|
+
this._patternOrder.push(name);
|
|
123
|
+
this._patternOrder.sort((a, b) => {
|
|
124
|
+
const pA = this._patterns.get(a).priority;
|
|
125
|
+
const pB = this._patterns.get(b).priority;
|
|
126
|
+
return pA - pB;
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
this._patternOrder.push(name);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove a pattern from the registry
|
|
135
|
+
*
|
|
136
|
+
* @param {string} name - Pattern name to remove
|
|
137
|
+
* @returns {boolean} - True if pattern was removed, false if not found
|
|
138
|
+
*/
|
|
139
|
+
removePattern(name) {
|
|
140
|
+
if (!this._patterns.has(name)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this._patterns.delete(name);
|
|
145
|
+
this._compiled.delete(name);
|
|
146
|
+
this._patternOrder = this._patternOrder.filter(n => n !== name);
|
|
147
|
+
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get a pattern by name
|
|
153
|
+
*
|
|
154
|
+
* @param {string} name - Pattern name
|
|
155
|
+
* @returns {Object|null} - Pattern definition or null if not found
|
|
156
|
+
* @property {RegExp} pattern - The regex pattern
|
|
157
|
+
* @property {Function} handler - The handler function
|
|
158
|
+
*/
|
|
159
|
+
getPattern(name) {
|
|
160
|
+
return this._patterns.get(name) || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get all pattern names
|
|
165
|
+
*
|
|
166
|
+
* @returns {Array<string>} - Array of pattern names in priority order
|
|
167
|
+
*/
|
|
168
|
+
getPatterns() {
|
|
169
|
+
return [...this._patternOrder];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the count of registered patterns
|
|
174
|
+
*
|
|
175
|
+
* @returns {number} - Number of patterns
|
|
176
|
+
*/
|
|
177
|
+
get size() {
|
|
178
|
+
return this._patterns.size;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Clear all patterns
|
|
183
|
+
*
|
|
184
|
+
* @returns {void}
|
|
185
|
+
*/
|
|
186
|
+
clear() {
|
|
187
|
+
this._patterns.clear();
|
|
188
|
+
this._compiled.clear();
|
|
189
|
+
this._patternOrder = [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Match a line against all registered patterns
|
|
194
|
+
*
|
|
195
|
+
* Tests patterns in priority order and returns the first match.
|
|
196
|
+
* Returns null if no patterns match.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} line - Log line to match
|
|
199
|
+
* @returns {Object|null} - Match result or null if no match
|
|
200
|
+
* @property {string} name - Name of the pattern that matched
|
|
201
|
+
* @property {*} data - Data returned by the pattern's handler
|
|
202
|
+
* @property {RegExpMatchArray} match - The regex match result
|
|
203
|
+
* @property {string} raw - Original input line
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* const result = registry.match('[12:34:56] Teleported TestPlayer to 100.0, 64.0, 100.0');
|
|
207
|
+
* // Returns:
|
|
208
|
+
* // {
|
|
209
|
+
* // name: 'teleport',
|
|
210
|
+
* // data: { player: 'TestPlayer', position: '100.0, 64.0, 100.0' },
|
|
211
|
+
* // match: RegExpMatchArray,
|
|
212
|
+
* // raw: '[12:34:56] Teleported TestPlayer to 100.0, 64.0, 100.0'
|
|
213
|
+
* // }
|
|
214
|
+
*/
|
|
215
|
+
match(line) {
|
|
216
|
+
if (typeof line !== 'string') {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Test each pattern in priority order
|
|
221
|
+
for (const name of this._patternOrder) {
|
|
222
|
+
const patternDef = this._patterns.get(name);
|
|
223
|
+
const regex = this._getCompiledPattern(name, patternDef.pattern);
|
|
224
|
+
|
|
225
|
+
const match = line.match(regex);
|
|
226
|
+
if (match) {
|
|
227
|
+
try {
|
|
228
|
+
const data = patternDef.handler(match);
|
|
229
|
+
return {
|
|
230
|
+
name,
|
|
231
|
+
data,
|
|
232
|
+
match,
|
|
233
|
+
raw: line
|
|
234
|
+
};
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Handler failed - continue to next pattern
|
|
237
|
+
// (Or we could emit an error event)
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Match a line against a specific pattern
|
|
248
|
+
*
|
|
249
|
+
* @param {string} line - Log line to match
|
|
250
|
+
* @param {string} patternName - Name of pattern to match against
|
|
251
|
+
* @returns {Object|null} - Match result or null if no match
|
|
252
|
+
*/
|
|
253
|
+
matchPattern(line, patternName) {
|
|
254
|
+
const patternDef = this._patterns.get(patternName);
|
|
255
|
+
if (!patternDef) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const regex = this._getCompiledPattern(patternName, patternDef.pattern);
|
|
260
|
+
const match = line.match(regex);
|
|
261
|
+
|
|
262
|
+
if (!match) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const data = patternDef.handler(match);
|
|
268
|
+
return {
|
|
269
|
+
name: patternName,
|
|
270
|
+
data,
|
|
271
|
+
match,
|
|
272
|
+
raw: line
|
|
273
|
+
};
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Test if a pattern matches (without executing handler)
|
|
281
|
+
*
|
|
282
|
+
* @param {string} line - Log line to test
|
|
283
|
+
* @param {string} patternName - Name of pattern to test
|
|
284
|
+
* @returns {boolean} - True if pattern matches
|
|
285
|
+
*/
|
|
286
|
+
test(line, patternName) {
|
|
287
|
+
const patternDef = this._patterns.get(patternName);
|
|
288
|
+
if (!patternDef) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const regex = this._getCompiledPattern(patternName, patternDef.pattern);
|
|
293
|
+
return regex.test(line);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get compiled regex for a pattern (lazy compilation)
|
|
298
|
+
*
|
|
299
|
+
* @private
|
|
300
|
+
* @param {string} name - Pattern name
|
|
301
|
+
* @param {RegExp} pattern - The regex pattern
|
|
302
|
+
* @returns {RegExp} - Compiled regex (cached or newly compiled)
|
|
303
|
+
*/
|
|
304
|
+
_getCompiledPattern(name, pattern) {
|
|
305
|
+
// Check cache first
|
|
306
|
+
if (this._compiled.has(name)) {
|
|
307
|
+
return this._compiled.get(name);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// For string patterns, we've already converted to RegExp in addPattern
|
|
311
|
+
// For RegExp patterns, use as-is
|
|
312
|
+
this._compiled.set(name, pattern);
|
|
313
|
+
return pattern;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Clone the registry (creates a new instance with same patterns)
|
|
318
|
+
*
|
|
319
|
+
* @returns {PatternRegistry} - New registry with copied patterns
|
|
320
|
+
*/
|
|
321
|
+
clone() {
|
|
322
|
+
const cloned = new PatternRegistry({
|
|
323
|
+
caseInsensitive: this._caseInsensitive
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Copy all patterns
|
|
327
|
+
for (const [name, patternDef] of this._patterns) {
|
|
328
|
+
cloned.addPattern(
|
|
329
|
+
name,
|
|
330
|
+
patternDef.pattern,
|
|
331
|
+
patternDef.handler,
|
|
332
|
+
patternDef.priority
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return cloned;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Export patterns as JSON (for serialization)
|
|
341
|
+
*
|
|
342
|
+
* Note: Handlers cannot be serialized (functions are lost)
|
|
343
|
+
*
|
|
344
|
+
* @returns {Object} - JSON-serializable representation
|
|
345
|
+
*/
|
|
346
|
+
toJSON() {
|
|
347
|
+
const patterns = {};
|
|
348
|
+
|
|
349
|
+
for (const [name, patternDef] of this._patterns) {
|
|
350
|
+
patterns[name] = {
|
|
351
|
+
name,
|
|
352
|
+
pattern: patternDef.pattern.toString(),
|
|
353
|
+
priority: patternDef.priority
|
|
354
|
+
// Note: handler function is lost
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
caseInsensitive: this._caseInsensitive,
|
|
360
|
+
patternCount: this._patterns.size,
|
|
361
|
+
patterns
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = { PatternRegistry };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minecraft Log Fixtures
|
|
3
|
+
*
|
|
4
|
+
* Real log lines from Minecraft servers 1.19, 1.20, 1.21+
|
|
5
|
+
* Used for testing MinecraftLogParser
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// STATUS EVENTS (Server Lifecycle)
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
status: {
|
|
14
|
+
starting: [
|
|
15
|
+
'[12:34:56] [Server thread/INFO]: Starting minecraft server version 1.20.1',
|
|
16
|
+
'[12:34:56] [Server thread/INFO]: Starting minecraft server on *:25565',
|
|
17
|
+
'[12:34:56] [Server thread/INFO]: Loading properties',
|
|
18
|
+
'[12:34:56] [Server thread/INFO]: Default game type: SURVIVAL',
|
|
19
|
+
'[12:34:56] [Server thread/INFO]: Generating keypair',
|
|
20
|
+
'[12:34:56] [Worker-Main-1/INFO]: Starting minecraft server version 1.19.4',
|
|
21
|
+
'[12:34:56] [Worker-Main-2/INFO]: Starting minecraft server version 1.21'
|
|
22
|
+
],
|
|
23
|
+
preparing: [
|
|
24
|
+
'[12:34:56] [Server thread/INFO]: Preparing level "world"',
|
|
25
|
+
'[12:34:56] [Server thread/INFO]: Preparing start region for dimension minecraft:overworld',
|
|
26
|
+
'[12:34:56] [Server thread/INFO]: Preparing start region for level 0'
|
|
27
|
+
],
|
|
28
|
+
done: [
|
|
29
|
+
'[12:34:56] [Server thread/INFO]: Done (3.452s)! For help, type "help"',
|
|
30
|
+
'[12:34:56] [Server thread/INFO]: Done (5.123s)! For help, type "help"',
|
|
31
|
+
'[12:34:56] [Worker-Main-3/INFO]: Time elapsed: 3422 ms'
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// ENTITY EVENTS (Player join/leave/death/spawn)
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
entity: {
|
|
40
|
+
join: [
|
|
41
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer joined the game',
|
|
42
|
+
'[12:34:56] [Server thread/INFO]: Steve joined the game',
|
|
43
|
+
'[12:34:56] [Server thread/INFO]: Alex joined the game'
|
|
44
|
+
],
|
|
45
|
+
leave: [
|
|
46
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer lost connection: Disconnected',
|
|
47
|
+
'[12:34:56] [Server thread/INFO]: Steve lost connection: Timed out',
|
|
48
|
+
'[12:34:56] [Server thread/INFO]: Alex lost connection: Internal Exception: java.io.IOException'
|
|
49
|
+
],
|
|
50
|
+
spawn: [
|
|
51
|
+
'[12:34:56] [Server thread/INFO]: UUID of player TestPlayer is abc123-def456-7890-abcd-ef1234567890',
|
|
52
|
+
'[12:34:56] [Server thread/INFO]: UUID of player Steve is 12345678-1234-5678-1234-5678123456789'
|
|
53
|
+
],
|
|
54
|
+
death: {
|
|
55
|
+
slain: [
|
|
56
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer was slain by Zombie',
|
|
57
|
+
'[12:34:56] [Server thread/INFO]: Steve was slain by Skeleton',
|
|
58
|
+
'[12:34:56] [Server thread/INFO]: Alex was slain by Spider',
|
|
59
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer was slain by Creeper',
|
|
60
|
+
'[12:34:56] [Server thread/INFO]: Player was slain by Husk',
|
|
61
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer was slain by Phantom'
|
|
62
|
+
],
|
|
63
|
+
fall: [
|
|
64
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer fell from a high place',
|
|
65
|
+
'[12:34:56] [Server thread/INFO]: Steve fell from a high place'
|
|
66
|
+
],
|
|
67
|
+
fire: [
|
|
68
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer burned to death',
|
|
69
|
+
'[12:34:56] [Server thread/INFO]: Alex was burnt to a crisp whilst fighting Skeleton'
|
|
70
|
+
],
|
|
71
|
+
lava: [
|
|
72
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer tried to swim in lava',
|
|
73
|
+
'[12:34:56] [Server thread/INFO]: Steve was killed by Magma Block',
|
|
74
|
+
'[12:34:56] [Server thread/INFO]: Alex was killed by Lava'
|
|
75
|
+
],
|
|
76
|
+
drown: [
|
|
77
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer drowned',
|
|
78
|
+
'[12:34:56] [Server thread/INFO]: Steve drowned'
|
|
79
|
+
],
|
|
80
|
+
sprint: [
|
|
81
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer splatted against a wall'
|
|
82
|
+
],
|
|
83
|
+
generic: [
|
|
84
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer died',
|
|
85
|
+
'[12:34:56] [Server thread/INFO]: Steve died'
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// MOVEMENT EVENTS (Teleport)
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
movement: {
|
|
95
|
+
teleport: [
|
|
96
|
+
'[12:34:56] [Server thread/INFO]: Teleported TestPlayer from 100.5, 64.0, 200.3 to 150.2, 70.0, -300.5',
|
|
97
|
+
'[12:34:56] [Server thread/INFO]: Teleported Steve from -50.0, 80.0, 100.0 to 50.0, 64.0, -50.0',
|
|
98
|
+
'[12:34:56] [Server thread/INFO]: Teleported Alex from 0.5, 100.0, 0.5 to 100.0, 64.0, 200.0',
|
|
99
|
+
'[12:34:56] [Server thread/INFO]: Teleported TestPlayer1 from 256.5, 64.0, -128.9 to -256.5, 70.0, 128.9',
|
|
100
|
+
'[12:34:56] [Server thread/INFO]: Teleported NPC from 10, 70, 20 to 100, 65, -100'
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// COMMAND EVENTS (Server commands)
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
command: {
|
|
109
|
+
issued: [
|
|
110
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer issued server command: /gamemode creative',
|
|
111
|
+
'[12:34:56] [Server thread/INFO]: Steve issued server command: /time set 1000',
|
|
112
|
+
'[12:34:56] [Server thread/INFO]: Alex issued server command: /weather rain',
|
|
113
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer issued server command: /tp Steve 100 64 100',
|
|
114
|
+
'[12:34:56] [Server thread/INFO]: OP issued server command: /stop',
|
|
115
|
+
'[12:34:56] [Server thread/INFO]: Server issued server command: /save-all'
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// WORLD EVENTS (Time/weather/save)
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
world: {
|
|
124
|
+
time: [
|
|
125
|
+
'[12:34:56] [Server thread/INFO]: Changing the time to 1000',
|
|
126
|
+
'[12:34:56] [Server thread/INFO]: Changing the time to 13000',
|
|
127
|
+
'[12:34:56] [Server thread/INFO]: Changing the time to 18000'
|
|
128
|
+
],
|
|
129
|
+
weather: [
|
|
130
|
+
'[12:34:56] [Server thread/INFO]: Changing the weather to rain',
|
|
131
|
+
'[12:34:56] [Server thread/INFO]: Changing the weather to thunder',
|
|
132
|
+
'[12:34:56] [Server thread/INFO]: Changing the weather to clear'
|
|
133
|
+
],
|
|
134
|
+
difficulty: [
|
|
135
|
+
'[12:34:56] [Server thread/INFO]: Changing the difficulty to hard',
|
|
136
|
+
'[12:34:56] [Server thread/INFO]: Changing the difficulty to easy',
|
|
137
|
+
'[12:34:56] [Server thread/INFO]: Changing the difficulty to normal'
|
|
138
|
+
],
|
|
139
|
+
gamemode: [
|
|
140
|
+
'[12:34:56] [Server thread/INFO]: The game mode has been updated to Creative',
|
|
141
|
+
'[12:34:56] [Server thread/INFO]: Gamemode has been updated to Survival Mode',
|
|
142
|
+
'[12:34:56] [Server thread/INFO]: The game mode has been updated to Spectator'
|
|
143
|
+
],
|
|
144
|
+
save: [
|
|
145
|
+
'[12:34:56] [Server thread/INFO]: Saving the game',
|
|
146
|
+
'[12:34:56] [Server thread/INFO]: Saving the game in level 0',
|
|
147
|
+
'[12:34:56] [Server thread/INFO]: Saved the game',
|
|
148
|
+
'[12:34:56] [Server thread/INFO]: Saving chunks for level \'world\'/minecraft:overworld'
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// MIXED EVENTS (for testing priority ordering)
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
mixed: [
|
|
157
|
+
// Teleport should match over other entity events (priority test)
|
|
158
|
+
'[12:34:56] [Server thread/INFO]: Teleported TestPlayer from 100.0, 64.0, 200.0 to 150.0, 70.0, -300.0',
|
|
159
|
+
// Death should match even if contains command-like text
|
|
160
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer was slain by Zombie using /kill'
|
|
161
|
+
],
|
|
162
|
+
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// EDGE CASES AND VARIATIONS
|
|
165
|
+
// ============================================================================
|
|
166
|
+
|
|
167
|
+
edge: [
|
|
168
|
+
// Different thread names
|
|
169
|
+
'[12:34:56] [Worker-Main-1/INFO]: TestPlayer joined the game',
|
|
170
|
+
'[12:34:56] [Worker-Main-2/INFO]: Steve issued server command: /gamemode creative',
|
|
171
|
+
'[12:34:56] [Server-Add-Worker-1/INFO]: Saving the game',
|
|
172
|
+
|
|
173
|
+
// Unicode player names
|
|
174
|
+
'[12:34:56] [Server thread/INFO]: 张三 joined the game',
|
|
175
|
+
'[12:34:56] [Server thread/INFO]: игрок joined the game',
|
|
176
|
+
|
|
177
|
+
// Long coordinates (negative values)
|
|
178
|
+
'[12:34:56] [Server thread/INFO]: Teleported TestPlayer from -1024.5, 128.0, 2048.9 to 512.0, -60.0, -512.0',
|
|
179
|
+
|
|
180
|
+
// Multi-word death messages
|
|
181
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer was shot by a Skeleton',
|
|
182
|
+
'[12:34:56] [Server thread/INFO]: TestPlayer was blown up by a Creeper',
|
|
183
|
+
|
|
184
|
+
// Unknown/unmatched patterns
|
|
185
|
+
'[12:34:56] [Server thread/INFO]: Some unknown log message that does not match any pattern',
|
|
186
|
+
'[12:34:56] [Server thread/WARN]: Can\'t keep up! Did the system time change, or is the server overloaded?'
|
|
187
|
+
]
|
|
188
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Parsers
|
|
3
|
+
*
|
|
4
|
+
* Concrete implementations of LogParser for various log formats.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { PatternRegistry } = require('./PatternRegistry.js');
|
|
8
|
+
const { MinecraftLogParser } = require('./MinecraftLogParser.js');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
PatternRegistry,
|
|
12
|
+
MinecraftLogParser
|
|
13
|
+
};
|
package/lib/rcon-backend.js
CHANGED
|
@@ -20,6 +20,7 @@ class RconBackend extends PilafBackend {
|
|
|
20
20
|
* @param {number} config.port - RCON port
|
|
21
21
|
* @param {string} config.password - RCON password
|
|
22
22
|
* @param {number} [config.timeout=30000] - Connection timeout in milliseconds
|
|
23
|
+
* @param {number} [config.maxRetries=5] - Max connection retry attempts
|
|
23
24
|
* @returns {Promise<void>}
|
|
24
25
|
*/
|
|
25
26
|
async connect(config) {
|
|
@@ -27,38 +28,53 @@ class RconBackend extends PilafBackend {
|
|
|
27
28
|
const port = config?.port || 25575;
|
|
28
29
|
const password = config?.password || '';
|
|
29
30
|
const timeout = config?.timeout || 30000; // 30 second default timeout
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const maxRetries = config?.maxRetries || 5; // Retry up to 5 times
|
|
32
|
+
|
|
33
|
+
let lastError = null;
|
|
34
|
+
|
|
35
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
36
|
+
try {
|
|
37
|
+
// Add timeout to prevent hanging on slow/unresponsive RCON servers
|
|
38
|
+
// Store timeout ID so we can clear it on success
|
|
39
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
40
|
+
this._connectTimeout = setTimeout(() => {
|
|
41
|
+
this._connectTimeout = null;
|
|
42
|
+
reject(new Error(`RCON connection timeout after ${timeout}ms`));
|
|
43
|
+
}, timeout);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.client = await Promise.race([
|
|
47
|
+
Rcon.connect({ host, port, password }),
|
|
48
|
+
timeoutPromise
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// Clear timeout if connection succeeded
|
|
52
|
+
if (this._connectTimeout) {
|
|
53
|
+
clearTimeout(this._connectTimeout);
|
|
36
54
|
this._connectTimeout = null;
|
|
37
|
-
|
|
38
|
-
}, timeout);
|
|
39
|
-
});
|
|
55
|
+
}
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
this.connected = true;
|
|
58
|
+
return this;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// Clean up timeout on error
|
|
61
|
+
if (this._connectTimeout) {
|
|
62
|
+
clearTimeout(this._connectTimeout);
|
|
63
|
+
this._connectTimeout = null;
|
|
64
|
+
}
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
if (this._connectTimeout) {
|
|
48
|
-
clearTimeout(this._connectTimeout);
|
|
49
|
-
this._connectTimeout = null;
|
|
50
|
-
}
|
|
66
|
+
lastError = error;
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
clearTimeout(this._connectTimeout);
|
|
58
|
-
this._connectTimeout = null;
|
|
68
|
+
// If not the last attempt, wait before retrying with exponential backoff
|
|
69
|
+
if (attempt < maxRetries) {
|
|
70
|
+
const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // Max 5 seconds
|
|
71
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
72
|
+
}
|
|
59
73
|
}
|
|
60
|
-
throw new Error(`Failed to connect to RCON: ${error.message}`);
|
|
61
74
|
}
|
|
75
|
+
|
|
76
|
+
// All retries exhausted
|
|
77
|
+
throw new Error(`Failed to connect to RCON after ${maxRetries} attempts: ${lastError.message}`);
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pilaf/backends",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"dockerode": "^4.0.0",
|
|
21
22
|
"mineflayer": "^4.20.1",
|
|
22
23
|
"rcon-client": "^4.2.5"
|
|
23
24
|
}
|