Skip to content

Commit 16c1574

Browse files
authored
test: Add a DB log filter by test name cli tool (#2677)
Add a CLI tool "Crawfish" can take structured mongodb logs and the new xunit test report and merge the information to obtain filtered mongodb logs by test name. Deduplicate suite names to ensure grepping / filtering works as expected Ensure all tests print to stderr to prevent logs from interfering with custom reporters. Update eslint config to correctly handle mjs files as well as ignore compiled js in the lib folder Skunkworks
1 parent a41d503 commit 16c1574

29 files changed

+918
-73
lines changed

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
docs
2+
lib

.eslintrc.json

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,15 @@
3535
"@typescript-eslint/no-explicit-any": "off"
3636
},
3737
"overrides": [{
38-
"files": ["*.d.ts"],
39-
"rules": {
40-
"prettier/prettier": "off",
41-
"@typescript-eslint/no-empty-interface": "off"
42-
}
43-
}]
38+
"files": ["*.d.ts"],
39+
"rules": {
40+
"prettier/prettier": "off",
41+
"@typescript-eslint/no-empty-interface": "off"
42+
}
43+
},
44+
{
45+
"files": ["*.mjs"],
46+
"parserOptions": {"sourceType": "module"}
47+
48+
}]
4449
}

.mocharc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
"ui": "test/tools/runner/metadata_ui.js",
66
"recursive": true,
77
"timeout": 60000,
8-
"reporter": "spec-xunit-file",
8+
"reporter": "test/tools/reporter/mongodb_reporter.js",
99
"color": true
1010
}

etc/crawfish.mjs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#! /usr/bin/env node --experimental-modules
2+
3+
import { createReadStream, existsSync, promises } from 'fs';
4+
const { readFile } = promises;
5+
import { createInterface } from 'readline';
6+
import xml2js from 'xml2js';
7+
const { parseStringPromise } = xml2js;
8+
import yargs from 'yargs';
9+
import chalk from 'chalk';
10+
11+
let warnings = false;
12+
13+
/**
14+
* @param args - program arguments
15+
*/
16+
async function main(args) {
17+
args = yargs(args)
18+
.option('l', {
19+
alias: 'log',
20+
demandOption: true,
21+
default: './data/mongod.log', // cluster_setup.sh default
22+
describe: 'The log you wish to filter',
23+
type: 'string'
24+
})
25+
.option('f', {
26+
alias: 'filter',
27+
demandOption: true,
28+
default: '', // No filter is still useful if you want to look at all tests
29+
describe: 'The test name filter, if none provided all test logs will be shown',
30+
type: 'string'
31+
})
32+
.option('v', {
33+
alias: 'verbose',
34+
demandOption: false,
35+
describe: 'Enable warnings about processing',
36+
type: 'boolean'
37+
})
38+
.help('h')
39+
.alias('h', 'help').epilog(`
40+
- Some log processing is done:
41+
- better date time format
42+
- string interpolation
43+
- 'testName' property added
44+
- Depends on an xunit file, should be left over from every test run
45+
46+
Examples:
47+
${chalk.green('crawfish.mjs | jq -SC | less -R')}
48+
- jq -SC will sort the keys and force color output
49+
- less lets you page through and search logs
50+
51+
${chalk.green('crawfish.mjs | jq -Sc | code -')}
52+
- jq -Sc will sort the keys and keep the logs one line (compact)
53+
- Opens the output in vscode, good for searching!
54+
`).argv;
55+
56+
warnings = !!args.verbose;
57+
const logFile = args.log;
58+
const testNameRegex = args.filter;
59+
60+
if (!existsSync('xunit.xml')) {
61+
console.error('xunit.xml file not found, required for db log test filtering.');
62+
process.exit(1);
63+
}
64+
65+
const content = await readFile('xunit.xml', { encoding: 'utf8' });
66+
const xunit = await parseStringPromise(content);
67+
68+
const tests = collectTests(xunit, testNameRegex);
69+
if (warnings) console.error(`filtering log file ${logFile}`);
70+
71+
const logStream =
72+
logFile === '-' ? process.stdin : createReadStream(logFile, { encoding: 'utf8' });
73+
const lineStream = createInterface({
74+
input: logStream,
75+
crlfDelay: Infinity
76+
});
77+
78+
const testToLogs = new Map(tests.map(({ name }) => [name, []]));
79+
for await (const line of lineStream) {
80+
const structuredLog = JSON.parse(line);
81+
for (const test of tests) {
82+
const logTime = Date.parse(structuredLog.t.$date);
83+
if (logTime <= test.end && logTime >= test.start) {
84+
testToLogs.get(test.name).push(structuredLog);
85+
}
86+
}
87+
}
88+
89+
for (const [name, logs] of testToLogs.entries()) {
90+
for (const log of logs) {
91+
log.testName = name;
92+
interpolateMsg(log);
93+
friendlyDate(log);
94+
console.log(JSON.stringify(log));
95+
}
96+
}
97+
}
98+
99+
function interpolateMsg(log) {
100+
if (!log.msg) return;
101+
102+
if (!log.attr) return;
103+
104+
for (const key in log.attr) {
105+
if (Reflect.has(log.attr, key)) {
106+
log.msg = log.msg.split(`{${key}}`).join(`${JSON.stringify(log.attr[key])}`);
107+
delete log.attr[key];
108+
}
109+
}
110+
111+
if (Object.keys(log.attr).length === 0) delete log.attr;
112+
log.msg = log.msg.split(`"`).join(`'`);
113+
}
114+
115+
function friendlyDate(log) {
116+
const dateString = typeof log.t === 'string' ? log.t : log.t.$date;
117+
try {
118+
log.t = new Date(Date.parse(dateString)).toISOString();
119+
} catch (e) {
120+
if (warnings) console.error(`Cannot translate date time of ${JSON.stringify(log)}`);
121+
}
122+
}
123+
124+
function collectTests(xuint, testFilter) {
125+
const suites = xuint.testsuites.testsuite;
126+
127+
const tests = [];
128+
129+
for (const suite of suites) {
130+
if (suite.testcase) {
131+
for (const test of suite.testcase) {
132+
const fullName = `${suite.$.name} ${test.$.name}`;
133+
if (fullName.toLowerCase().includes(testFilter.toLowerCase())) {
134+
if (test.$.start === '0') {
135+
if (warnings) console.error(`Warning: ${fullName} was skipped, theres no logs`);
136+
continue;
137+
}
138+
tests.push({
139+
name: fullName,
140+
start: Date.parse(test.$.start),
141+
end: Date.parse(test.$.end)
142+
});
143+
}
144+
}
145+
}
146+
}
147+
148+
return tests;
149+
}
150+
151+
main(process.argv).catch(e => {
152+
console.error(e);
153+
process.exit(1);
154+
});

0 commit comments

Comments
 (0)