Refactor log parser

This commit is contained in:
Thomas Smyth 2020-12-10 20:46:29 +00:00
parent f434e94199
commit baf07a79b6
23 changed files with 142 additions and 125 deletions

98
core/log-parser/index.js Normal file
View File

@ -0,0 +1,98 @@
import EventEmitter from 'events';
import async from 'async';
import moment from 'moment';
import Logger from '../logger.js';
import TailLogReader from './log-readers/tail.js';
import FTPLogReader from './log-readers/ftp.js';
export default class LogParser extends EventEmitter {
constructor(filename = 'filename.log', options = {}) {
super();
options.filename = filename;
this.eventStrore = {};
this.linesPerMinute = 0;
this.matchingLinesPerMinute = 0;
this.matchingLatency = 0;
this.parsingStatsInterval = null;
this.processLine = this.processLine.bind(this);
this.logStats = this.logStats.bind(this);
this.queue = async.queue(this.processLine);
switch (options.mode || 'tail') {
case 'tail':
this.logReader = new TailLogReader(this.queue.push, options);
break;
case 'ftp':
this.logReader = new FTPLogReader(this.queue.push, options);
break;
default:
throw new Error('Invalid mode.');
}
}
async processLine(line) {
Logger.verbose('LogParser', 4, `Matching on line: ${line}`);
for (const rule of this.getRules()) {
const match = line.match(rule.regex);
if (!match) continue;
Logger.verbose('LogParser', 3, `Matched on line: ${match[0]}`);
match[1] = moment.utc(match[1], 'YYYY.MM.DD-hh.mm.ss:SSS').toDate();
match[2] = parseInt(match[2]);
rule.onMatch(match, this);
this.matchingLinesPerMinute++;
this.matchingLatency += Date.now() - match[1];
break;
}
this.linesPerMinute++;
}
getRules() {
return [];
}
async watch() {
Logger.verbose('LogParser', 1, 'Attempting to watch log file...');
await this.logReader.watch();
Logger.verbose('LogParser', 1, 'Watching log file...');
this.parsingStatsInterval = setInterval(this.logStats, 60 * 1000);
}
logStats() {
Logger.verbose(
'LogParser',
1,
`Lines parsed per minute: ${
this.linesPerMinute
} lines per minute | Matching lines per minute: ${
this.matchingLinesPerMinute
} matching lines per minute | Average matching latency: ${
this.matchingLatency / this.matchingLinesPerMinute
}ms`
);
this.linesPerMinute = 0;
this.matchingLinesPerMinute = 0;
this.matchingLatency = 0;
}
async unwatch() {
await this.logReader.unwatch();
clearInterval(this.parsingStatsInterval);
}
}

View File

@ -16,7 +16,7 @@ export default class TailLogReader {
encoding: 'utf8',
verbose: options.verbose,
path: path.join(options.logDir, 'SquadGame.log'),
path: path.join(options.logDir, options.filename),
fetchInterval: options.fetchInterval || 0,
maxTempFileSize: options.maxTempFileSize || 5 * 1000 * 1000, // 5 MB

View File

@ -1,16 +1,18 @@
import path from 'path';
import TailModule from 'tail';
export default class TailLogReader {
constructor(queueLine, options = {}) {
if (!('logDir' in options)) throw new Error(`logDir must be specified.`);
this.reader = new TailModule.Tail(path.join(options.logDir, 'SquadGame.log'), {
this.reader = new TailModule.Tail(path.join(options.logDir, options.filename), {
useWatchFile: true
});
if (typeof queueLine !== 'function')
throw new Error('queueLine argument must be specified and be a function.');
this.reader.on('line', queueLine);
}

View File

@ -3,11 +3,16 @@
"version": "1.0.0",
"type": "module",
"exports": {
"./log-parser": "./log-parser/index.js",
"./constants": "./constants.js",
"./logger": "./logger.js",
"./rcon": "./rcon.js"
},
"dependencies": {
"chalk": "^4.1.0"
"async": "^3.2.0",
"chalk": "^4.1.0",
"ftp-tail": "^1.1.1",
"moment": "^2.29.1",
"tail": "^2.0.4"
}
}

View File

@ -1,87 +0,0 @@
import EventEmitter from 'events';
import async from 'async';
import moment from 'moment';
import Logger from 'core/logger';
import TailLogReader from './log-readers/tail.js';
import FTPLogReader from './log-readers/ftp.js';
import rules from './rules/index.js';
export default class LogParser extends EventEmitter {
constructor(options = {}) {
super();
this.eventStore = {};
this.linesPerMinute = 0;
this.matchingLinesPerMinute = 0;
this.matchingLatency = 0;
this.parsingStatsInterval = null;
this.queue = async.queue(async (line) => {
Logger.verbose('LogParser', 4, `Matching on line: ${line}`);
for (const rule of rules) {
const match = line.match(rule.regex);
if (!match) continue;
Logger.verbose('LogParser', 3, `Matched on line: ${match[0]}`);
match[1] = moment.utc(match[1], 'YYYY.MM.DD-hh.mm.ss:SSS').toDate();
match[2] = parseInt(match[2]);
rule.onMatch(match, this);
this.matchingLinesPerMinute++;
this.matchingLatency += Date.now() - match[1];
break;
}
this.linesPerMinute++;
});
switch (options.mode || 'tail') {
case 'tail':
this.logReader = new TailLogReader(this.queue.push, options);
break;
case 'ftp':
this.logReader = new FTPLogReader(this.queue.push, options);
break;
default:
throw new Error('Invalid mode.');
}
}
async watch() {
Logger.verbose('LogParser', 1, 'Attempting to watch log file...');
await this.logReader.watch();
Logger.verbose('LogParser', 1, 'Watching log file...');
this.parsingStatsInterval = setInterval(() => {
Logger.verbose(
'LogParser',
1,
`Lines parsed per minute: ${
this.linesPerMinute
} lines per minute | Matching lines per minute: ${
this.matchingLinesPerMinute
} matching lines per minute | Average matching latency: ${
this.matchingLatency / this.matchingLinesPerMinute
}ms`
);
this.linesPerMinute = 0;
this.matchingLinesPerMinute = 0;
this.matchingLatency = 0;
}, 60 * 1000);
}
async unwatch() {
await this.logReader.unwatch();
clearInterval(this.parsingStatsInterval);
}
}

View File

@ -1,14 +0,0 @@
{
"name": "log-parser",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./index.js"
},
"dependencies": {
"async": "^3.2.0",
"ftp-tail": "^1.1.1",
"moment": "^2.29.0",
"tail": "^2.0.4"
}
}

View File

@ -8,7 +8,6 @@
"workspaces": [
"assets",
"core",
"log-parser",
"squad-server"
],
"scripts": {

View File

@ -116,14 +116,19 @@ export default class SquadServerFactory {
if (type === 'sequelize') {
let connector;
if(typeof connectorConfig === 'string') {
connector = new Sequelize(connectorConfig, { logging: msg => Logger.verbose('Sequelize', 3, msg) })
if (typeof connectorConfig === 'string') {
connector = new Sequelize(connectorConfig, {
logging: (msg) => Logger.verbose('Sequelize', 3, msg)
});
} else if (typeof connectorConfig === 'object') {
connector = new Sequelize({ ...connectorConfig, logging: msg => Logger.verbose('Sequelize', 3, msg) });
connector = new Sequelize({
...connectorConfig,
logging: (msg) => Logger.verbose('Sequelize', 3, msg)
});
} else {
throw new Error('Unknown sequelize connector config type.');
}
await connector.authenticate();
return connector;
}

View File

@ -6,7 +6,7 @@ import Gamedig from 'gamedig';
import Logger from 'core/logger';
import { SQUADJS_API_DOMAIN } from 'core/constants';
import LogParser from 'log-parser';
import LogParser from './log-parser/index.js';
import Rcon from './rcon.js';
import { SQUADJS_VERSION } from './utils/constants.js';

View File

@ -1,3 +1,5 @@
import LogParser from 'core/log-parser';
import AdminBroadcast from './admin-broadcast.js';
import NewGame from './new-game.js';
import PlayerConnected from './player-connected.js';
@ -11,17 +13,25 @@ import RoundWinner from './round-winner.js';
import ServerTickRate from './server-tick-rate.js';
import SteamIDConnected from './steamid-connected.js';
export default [
AdminBroadcast,
NewGame,
PlayerConnected,
PlayerDamaged,
PlayerDied,
PlayerPossess,
PlayerRevived,
PlayerUnPossess,
PlayerWounded,
RoundWinner,
ServerTickRate,
SteamIDConnected
];
export default class SquadLogParser extends LogParser {
constructor(options) {
super('SquadGame.log', options);
}
getRules() {
return [
AdminBroadcast,
NewGame,
PlayerConnected,
PlayerDamaged,
PlayerDied,
PlayerPossess,
PlayerRevived,
PlayerUnPossess,
PlayerWounded,
RoundWinner,
ServerTickRate,
SteamIDConnected
];
}
}

View File

@ -8,7 +8,6 @@
"didyoumean": "^1.2.1",
"discord.js": "^12.3.1",
"gamedig": "^2.0.20",
"log-parser": "1.0.0",
"mariadb": "^2.5.1",
"mysql2": "^2.2.5",
"pg": "^8.5.1",