[ftr] support redirecting server logs to a file (#140334) (#140438)

(cherry picked from commit 1530f8e1d3)

Co-authored-by: Spencer <spencer@elastic.co>
This commit is contained in:
Kibana Machine 2022-09-09 12:54:33 -06:00 committed by GitHub
parent e792e17dbd
commit bb3222a9e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 204 additions and 589 deletions

View file

@ -6,8 +6,10 @@
* Side Public License, v 1.
*/
import { statSync } from 'fs';
import Fs from 'fs';
import Path from 'path';
import { promisify } from 'util';
import stripAnsi from 'strip-ansi';
import execa from 'execa';
import * as Rx from 'rxjs';
@ -29,6 +31,7 @@ export interface ProcOptions {
cwd: string;
env?: Record<string, string | undefined>;
stdin?: string;
writeLogsToPath?: string;
}
async function withTimeout(
@ -44,13 +47,21 @@ export type Proc = ReturnType<typeof startProc>;
export function startProc(name: string, options: ProcOptions, log: ToolingLog) {
const { cmd, args, cwd, env, stdin } = options;
log.info('[%s] > %s', name, cmd === process.execPath ? 'node' : cmd, args.join(' '));
let stdioTarget: undefined | NodeJS.WritableStream;
if (!options.writeLogsToPath) {
log.info('starting [%s] > %s', name, cmd === process.execPath ? 'node' : cmd, args.join(' '));
} else {
stdioTarget = Fs.createWriteStream(options.writeLogsToPath, 'utf8');
const exec = cmd === process.execPath ? 'node' : cmd;
const relOut = Path.relative(process.cwd(), options.writeLogsToPath);
log.info(`starting [${name}] and writing output to ${relOut} > ${exec} ${args.join(' ')}`);
}
// spawn fails with ENOENT when either the
// cmd or cwd don't exist, so we check for the cwd
// ahead of time so that the error is less ambiguous
try {
if (!statSync(cwd).isDirectory()) {
if (!Fs.statSync(cwd).isDirectory()) {
throw new Error(`cwd "${cwd}" exists but is not a directory`);
}
} catch (err) {
@ -104,7 +115,20 @@ export function startProc(name: string, options: ProcOptions, log: ToolingLog) {
observeLines(childProcess.stdout!), // TypeScript note: As long as the proc stdio[1] is 'pipe', then stdout will not be null
observeLines(childProcess.stderr!) // TypeScript note: As long as the proc stdio[1] is 'pipe', then stderr will not be null
).pipe(
tap((line) => log.write(` ${chalk.gray('proc')} [${chalk.gray(name)}] ${line}`)),
tap({
next(line) {
if (stdioTarget) {
stdioTarget.write(stripAnsi(line) + '\n');
} else {
log.write(` ${chalk.gray('proc')} [${chalk.gray(name)}] ${line}`);
}
},
complete() {
if (stdioTarget) {
stdioTarget.end();
}
},
}),
share()
);

View file

@ -36,12 +36,12 @@ export class ProcRunner {
private procs: Proc[] = [];
private signalUnsubscribe: () => void;
constructor(private log: ToolingLog) {
constructor(private readonly log: ToolingLog) {
this.log = log.withType('ProcRunner');
this.signalUnsubscribe = exitHook(() => {
this.teardown().catch((error) => {
log.error(`ProcRunner teardown error: ${error.stack}`);
this.log.error(`ProcRunner teardown error: ${error.stack}`);
});
});
}
@ -58,6 +58,7 @@ export class ProcRunner {
waitTimeout = 15 * MINUTE,
env = process.env,
onEarlyExit,
writeLogsToPath,
} = options;
const cmd = options.cmd === 'node' ? process.execPath : options.cmd;
@ -79,6 +80,7 @@ export class ProcRunner {
cwd,
env,
stdin,
writeLogsToPath,
});
if (onEarlyExit) {

View file

@ -6,10 +6,12 @@
* Side Public License, v 1.
*/
const fs = require('fs');
const fsp = require('fs/promises');
const execa = require('execa');
const chalk = require('chalk');
const path = require('path');
const Rx = require('rxjs');
const { Client } = require('@elastic/elasticsearch');
const { downloadSnapshot, installSnapshot, installSource, installArchive } = require('./install');
const { ES_BIN, ES_PLUGIN_BIN, ES_KEYSTORE_BIN } = require('./paths');
@ -315,6 +317,7 @@ exports.Cluster = class Cluster {
startTime,
skipReadyCheck,
readyTimeout,
writeLogsToPath,
...options
} = opts;
@ -322,7 +325,19 @@ exports.Cluster = class Cluster {
throw new Error('ES has already been started');
}
this._log.info(chalk.bold('Starting'));
/** @type {NodeJS.WritableStream | undefined} */
let stdioTarget;
if (writeLogsToPath) {
stdioTarget = fs.createWriteStream(writeLogsToPath, 'utf8');
this._log.info(
chalk.bold('Starting'),
`and writing logs to ${path.relative(process.cwd(), writeLogsToPath)}`
);
} else {
this._log.info(chalk.bold('Starting'));
}
this._log.indent(4);
const esArgs = new Map([
@ -428,7 +443,8 @@ exports.Cluster = class Cluster {
let reportSent = false;
// parse and forward es stdout to the log
this._process.stdout.on('data', (data) => {
const lines = parseEsLog(data.toString());
const chunk = data.toString();
const lines = parseEsLog(chunk);
lines.forEach((line) => {
if (!reportSent && line.message.includes('publish_address')) {
reportSent = true;
@ -436,12 +452,36 @@ exports.Cluster = class Cluster {
success: true,
});
}
this._log.info(line.formattedMessage);
if (stdioTarget) {
stdioTarget.write(chunk);
} else {
this._log.info(line.formattedMessage);
}
});
});
// forward es stderr to the log
this._process.stderr.on('data', (data) => this._log.error(chalk.red(data.toString())));
this._process.stderr.on('data', (data) => {
const chunk = data.toString();
if (stdioTarget) {
stdioTarget.write(chunk);
} else {
this._log.error(chalk.red());
}
});
// close the stdio target if we have one defined
if (stdioTarget) {
Rx.combineLatest([
Rx.fromEvent(this._process.stderr, 'end'),
Rx.fromEvent(this._process.stdout, 'end'),
])
.pipe(Rx.first())
.subscribe(() => {
stdioTarget.end();
});
}
// observe the exit code of the process and reflect in _outcome promies
const exitCode = new Promise((resolve) => this._process.once('exit', resolve));

View file

@ -95,6 +95,7 @@ export interface CreateTestEsClusterOptions {
*/
license?: 'basic' | 'gold' | 'trial'; // | 'oss'
log: ToolingLog;
writeLogsToPath?: string;
/**
* Node-specific configuration if you wish to run a multi-node
* cluster. One node will be added for each item in the array.
@ -168,6 +169,7 @@ export function createTestEsCluster<
password = 'changeme',
license = 'basic',
log,
writeLogsToPath,
basePath = Path.resolve(REPO_ROOT, '.es'),
esFrom = esTestConfig.getBuildFrom(),
dataArchive,
@ -272,6 +274,7 @@ export function createTestEsCluster<
skipNativeRealmSetup: this.nodes.length > 1 && i < this.nodes.length - 1,
skipReadyCheck: this.nodes.length > 1 && i < this.nodes.length - 1,
onEarlyExit,
writeLogsToPath,
});
});
}

View file

@ -23,6 +23,7 @@ Options:
--include-tag <tag> Tags that suites must include to be run, can be included multiple times.
--exclude-tag <tag> Tags that suites must NOT include to be run, can be included multiple times.
--assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags.
--logToFile Write the log output from Kibana/Elasticsearch to files instead of to stdout
--verbose Log everything.
--debug Run in debug mode.
--quiet Only log errors.
@ -40,6 +41,7 @@ Object {
"esFrom": "snapshot",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -62,6 +64,7 @@ Object {
"esFrom": "snapshot",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -85,6 +88,7 @@ Object {
"esFrom": "snapshot",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -107,6 +111,7 @@ Object {
"esFrom": "snapshot",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -133,6 +138,7 @@ Object {
"extraKbnOpts": Object {
"server.foo": "bar",
},
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -154,6 +160,7 @@ Object {
"esFrom": "snapshot",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"quiet": true,
"suiteFiles": Object {
"exclude": Array [],
@ -176,6 +183,7 @@ Object {
"esFrom": "snapshot",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"silent": true,
"suiteFiles": Object {
"exclude": Array [],
@ -198,6 +206,7 @@ Object {
"esFrom": "source",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -219,6 +228,7 @@ Object {
"esFrom": "source",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -241,6 +251,7 @@ Object {
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"installDir": "foo",
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -263,6 +274,7 @@ Object {
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"grep": "management",
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -284,6 +296,7 @@ Object {
"esFrom": "snapshot",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],
@ -306,6 +319,7 @@ Object {
"esFrom": "snapshot",
"esVersion": "999.999.999",
"extraKbnOpts": undefined,
"logsDir": undefined,
"suiteFiles": Object {
"exclude": Array [],
"include": Array [],

View file

@ -1,74 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`run tests CLI options accepts help option even if invalid options passed 1`] = `
"Run Functional Tests
Usage:
node scripts/functional_tests --help
node scripts/functional_tests [--config <file1> [--config <file2> ...]]
node scripts/functional_tests [options] [-- --<other args>]
Options:
--help Display this menu and exit.
--config <file> Pass in a config. Can pass in multiple configs.
--esFrom <snapshot|source> Build Elasticsearch from source or run from snapshot. Default: $TEST_ES_FROM or snapshot
--kibana-install-dir <dir> Run Kibana from existing install directory instead of from source.
--bail Stop the test run at the first failure.
--grep <pattern> Pattern to select which tests to run.
--updateBaselines Replace baseline screenshots with whatever is generated from the test.
--updateSnapshots Replace inline and file snapshots with whatever is generated from the test.
--u Replace both baseline screenshots and snapshots
--include <file> Files that must included to be run, can be included multiple times.
--exclude <file> Files that must NOT be included to be run, can be included multiple times.
--include-tag <tag> Tags that suites must include to be run, can be included multiple times.
--exclude-tag <tag> Tags that suites must NOT include to be run, can be included multiple times.
--assert-none-excluded Exit with 1/0 based on if any test is excluded with the current set of tags.
--verbose Log everything.
--debug Run in debug mode.
--quiet Only log errors.
--silent Log nothing.
--dry-run Report tests without executing them.
"
`;
exports[`run tests CLI options rejects boolean config value 1`] = `
"
functional_tests: invalid argument [true] to option [config]
...stack trace...
"
`;
exports[`run tests CLI options rejects boolean value for kibana-install-dir 1`] = `
"
functional_tests: invalid argument [true] to option [kibana-install-dir]
...stack trace...
"
`;
exports[`run tests CLI options rejects empty config value if no default passed 1`] = `
"
functional_tests: config is required
...stack trace...
"
`;
exports[`run tests CLI options rejects invalid options even if valid options exist 1`] = `
"
functional_tests: invalid option [aintnothang]
...stack trace...
"
`;
exports[`run tests CLI options rejects non-boolean value for bail 1`] = `
"
functional_tests: invalid argument [peanut] to option [bail]
...stack trace...
"
`;
exports[`run tests CLI options rejects non-enum value for esFrom 1`] = `
"
functional_tests: invalid argument [butter] to option [esFrom]
...stack trace...
"
`;

View file

@ -6,9 +6,11 @@
* Side Public License, v 1.
*/
import { resolve } from 'path';
import Path from 'path';
import { v4 as uuid } from 'uuid';
import dedent from 'dedent';
import { REPO_ROOT } from '@kbn/utils';
import { ToolingLog, pickLevelFromFlags } from '@kbn/tooling-log';
import { EsVersion } from '../../../functional_test_runner';
@ -61,6 +63,9 @@ const options = {
'assert-none-excluded': {
desc: 'Exit with 1/0 based on if any test is excluded with the current set of tags.',
},
logToFile: {
desc: 'Write the log output from Kibana/Elasticsearch to files instead of to stdout',
},
verbose: { desc: 'Log everything.' },
debug: { desc: 'Run in debug mode.' },
quiet: { desc: 'Only log errors.' },
@ -142,19 +147,24 @@ export function processOptions(userOptions, defaultConfigPaths) {
delete userOptions['dry-run'];
}
const log = new ToolingLog({
level: pickLevelFromFlags(userOptions),
writeTo: process.stdout,
});
function createLogger() {
return new ToolingLog({
level: pickLevelFromFlags(userOptions),
writeTo: process.stdout,
});
return log;
}
const logToFile = !!userOptions.logToFile;
const logsDir = logToFile ? Path.resolve(REPO_ROOT, 'data/ftr_servers_logs', uuid()) : undefined;
return {
...userOptions,
configs: configs.map((c) => resolve(c)),
configs: configs.map((c) => Path.resolve(c)),
createLogger,
extraKbnOpts: userOptions._,
esVersion: EsVersion.getDefault(),
logsDir,
};
}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { runTests } from '../../tasks';
import { runTests, initLogsDir } from '../../tasks';
import { runCli } from '../../lib';
import { processOptions, displayHelp } from './args';
@ -21,6 +21,7 @@ import { processOptions, displayHelp } from './args';
export async function runTestsCli(defaultConfigPaths) {
await runCli(displayHelp, async (userOptions) => {
const options = processOptions(userOptions, defaultConfigPaths);
initLogsDir(options);
await runTests(options);
});
}

View file

@ -1,232 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Writable } from 'stream';
import { runTestsCli } from './cli';
import { checkMockConsoleLogSnapshot } from '../../test_helpers';
// Note: Stub the runTests function to keep testing only around the cli
// method and arguments.
jest.mock('../../tasks', () => ({
runTests: jest.fn(),
}));
describe('run tests CLI', () => {
describe('options', () => {
const originalObjects = { process, console };
const exitMock = jest.fn();
const logMock = jest.fn(); // mock logging so we don't send output to the test results
const argvMock = ['foo', 'foo'];
const processMock = {
exit: exitMock,
argv: argvMock,
stdout: new Writable(),
cwd: jest.fn(),
env: {
...originalObjects.process.env,
TEST_ES_FROM: 'snapshot',
},
};
beforeAll(() => {
global.process = processMock;
global.console = { log: logMock };
});
afterAll(() => {
global.process = originalObjects.process;
global.console = originalObjects.console;
});
beforeEach(() => {
global.process.argv = [...argvMock];
global.process.env = {
...originalObjects.process.env,
TEST_ES_FROM: 'snapshot',
};
jest.resetAllMocks();
});
it('rejects boolean config value', async () => {
global.process.argv.push('--config');
await runTestsCli();
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('rejects empty config value if no default passed', async () => {
global.process.argv.push('--config', '');
await runTestsCli();
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('accepts empty config value if default passed', async () => {
global.process.argv.push('--config', '');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalled();
});
it('rejects non-boolean value for bail', async () => {
global.process.argv.push('--bail', 'peanut');
await runTestsCli(['foo']);
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('accepts string value for kibana-install-dir', async () => {
global.process.argv.push('--kibana-install-dir', 'foo');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalled();
});
it('rejects boolean value for kibana-install-dir', async () => {
global.process.argv.push('--kibana-install-dir');
await runTestsCli(['foo']);
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('accepts boolean value for updateBaselines', async () => {
global.process.argv.push('--updateBaselines');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalledWith();
});
it('accepts boolean value for updateSnapshots', async () => {
global.process.argv.push('--updateSnapshots');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalledWith();
});
it('accepts boolean value for -u', async () => {
global.process.argv.push('-u');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalledWith();
});
it('accepts source value for esFrom', async () => {
global.process.argv.push('--esFrom', 'source');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalled();
});
it('rejects non-enum value for esFrom', async () => {
global.process.argv.push('--esFrom', 'butter');
await runTestsCli(['foo']);
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('accepts value for grep', async () => {
global.process.argv.push('--grep', 'management');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts debug option', async () => {
global.process.argv.push('--debug');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts silent option', async () => {
global.process.argv.push('--silent');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts quiet option', async () => {
global.process.argv.push('--quiet');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts verbose option', async () => {
global.process.argv.push('--verbose');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts network throttle option', async () => {
global.process.argv.push('--throttle');
await runTestsCli(['foo']);
expect(exitMock).toHaveBeenCalledWith(1);
});
it('accepts headless option', async () => {
global.process.argv.push('--headless');
await runTestsCli(['foo']);
expect(exitMock).toHaveBeenCalledWith(1);
});
it('accepts extra server options', async () => {
global.process.argv.push('--', '--server.foo=bar');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts help option even if invalid options passed', async () => {
global.process.argv.push('--debug', '--aintnothang', '--help');
await runTestsCli(['foo']);
expect(exitMock).not.toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('rejects invalid options even if valid options exist', async () => {
global.process.argv.push('--debug', '--aintnothang', '--bail');
await runTestsCli(['foo']);
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
});
});

View file

@ -13,6 +13,7 @@ Options:
--config <file> Pass in a config
--esFrom <snapshot|source|path> Build Elasticsearch from source, snapshot or path to existing install dir. Default: $TEST_ES_FROM or snapshot
--kibana-install-dir <dir> Run Kibana from existing install directory instead of from source.
--logToFile Write the log output from Kibana/Elasticsearch to files instead of to stdout
--verbose Log everything.
--debug Run in debug mode.
--quiet Only log errors.
@ -26,6 +27,7 @@ Object {
"debug": true,
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"logsDir": undefined,
"useDefaultConfig": true,
}
`;
@ -36,6 +38,7 @@ Object {
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"logsDir": undefined,
"useDefaultConfig": true,
}
`;
@ -51,6 +54,7 @@ Object {
"extraKbnOpts": Object {
"server.foo": "bar",
},
"logsDir": undefined,
"useDefaultConfig": true,
}
`;
@ -61,6 +65,7 @@ Object {
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"logsDir": undefined,
"quiet": true,
"useDefaultConfig": true,
}
@ -72,6 +77,7 @@ Object {
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"logsDir": undefined,
"silent": true,
"useDefaultConfig": true,
}
@ -83,6 +89,7 @@ Object {
"createLogger": [Function],
"esFrom": "source",
"extraKbnOpts": undefined,
"logsDir": undefined,
"useDefaultConfig": true,
}
`;
@ -93,6 +100,7 @@ Object {
"createLogger": [Function],
"esFrom": "source",
"extraKbnOpts": undefined,
"logsDir": undefined,
"useDefaultConfig": true,
}
`;
@ -104,6 +112,7 @@ Object {
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"installDir": "foo",
"logsDir": undefined,
"useDefaultConfig": true,
}
`;
@ -114,6 +123,7 @@ Object {
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"logsDir": undefined,
"useDefaultConfig": true,
"verbose": true,
}
@ -125,6 +135,7 @@ Object {
"createLogger": [Function],
"esFrom": "snapshot",
"extraKbnOpts": undefined,
"logsDir": undefined,
"useDefaultConfig": true,
}
`;

View file

@ -1,50 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`start servers CLI options accepts boolean value for updateBaselines 1`] = `
"
functional_tests_server: invalid option [updateBaselines]
...stack trace...
"
`;
exports[`start servers CLI options accepts boolean value for updateSnapshots 1`] = `
"
functional_tests_server: invalid option [updateSnapshots]
...stack trace...
"
`;
exports[`start servers CLI options rejects bail 1`] = `
"
functional_tests_server: invalid option [bail]
...stack trace...
"
`;
exports[`start servers CLI options rejects boolean config value 1`] = `
"
functional_tests_server: invalid argument [true] to option [config]
...stack trace...
"
`;
exports[`start servers CLI options rejects boolean value for kibana-install-dir 1`] = `
"
functional_tests_server: invalid argument [true] to option [kibana-install-dir]
...stack trace...
"
`;
exports[`start servers CLI options rejects empty config value if no default passed 1`] = `
"
functional_tests_server: config is required
...stack trace...
"
`;
exports[`start servers CLI options rejects invalid options even if valid options exist 1`] = `
"
functional_tests_server: invalid option [grep]
...stack trace...
"
`;

View file

@ -6,9 +6,11 @@
* Side Public License, v 1.
*/
import { resolve } from 'path';
import Path from 'path';
import { v4 as uuid } from 'uuid';
import dedent from 'dedent';
import { REPO_ROOT } from '@kbn/utils';
import { ToolingLog, pickLevelFromFlags } from '@kbn/tooling-log';
const options = {
@ -26,6 +28,9 @@ const options = {
arg: '<dir>',
desc: 'Run Kibana from existing install directory instead of from source.',
},
logToFile: {
desc: 'Write the log output from Kibana/Elasticsearch to files instead of to stdout',
},
verbose: { desc: 'Log everything.' },
debug: { desc: 'Run in debug mode.' },
quiet: { desc: 'Only log errors.' },
@ -80,16 +85,22 @@ export function processOptions(userOptions, defaultConfigPath) {
delete userOptions['kibana-install-dir'];
}
const log = new ToolingLog({
level: pickLevelFromFlags(userOptions),
writeTo: process.stdout,
});
function createLogger() {
return new ToolingLog({
level: pickLevelFromFlags(userOptions),
writeTo: process.stdout,
});
return log;
}
const logToFile = !!userOptions.logToFile;
const logsDir = logToFile ? Path.resolve(REPO_ROOT, 'data/ftr_servers_logs', uuid()) : undefined;
return {
...userOptions,
config: resolve(config),
logsDir,
config: Path.resolve(config),
useDefaultConfig,
createLogger,
extraKbnOpts: userOptions._,

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { startServers } from '../../tasks';
import { startServers, initLogsDir } from '../../tasks';
import { runCli } from '../../lib';
import { processOptions, displayHelp } from './args';
@ -18,6 +18,7 @@ import { processOptions, displayHelp } from './args';
export async function startServersCli(defaultConfigPath) {
await runCli(displayHelp, async (userOptions) => {
const options = processOptions(userOptions, defaultConfigPath);
initLogsDir(options);
await startServers({
...options,
});

View file

@ -1,192 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Writable } from 'stream';
import { startServersCli } from './cli';
import { checkMockConsoleLogSnapshot } from '../../test_helpers';
// Note: Stub the startServers function to keep testing only around the cli
// method and arguments.
jest.mock('../../tasks', () => ({
startServers: jest.fn(),
}));
describe('start servers CLI', () => {
describe('options', () => {
const originalObjects = { process, console };
const exitMock = jest.fn();
const logMock = jest.fn(); // mock logging so we don't send output to the test results
const argvMock = ['foo', 'foo'];
const processMock = {
exit: exitMock,
argv: argvMock,
stdout: new Writable(),
cwd: jest.fn(),
env: {
...originalObjects.process.env,
TEST_ES_FROM: 'snapshot',
},
};
beforeAll(() => {
global.process = processMock;
global.console = { log: logMock };
});
afterAll(() => {
global.process = originalObjects.process;
global.console = originalObjects.console;
});
beforeEach(() => {
global.process.argv = [...argvMock];
global.process.env = {
...originalObjects.process.env,
TEST_ES_FROM: 'snapshot',
};
jest.resetAllMocks();
});
it('rejects boolean config value', async () => {
global.process.argv.push('--config');
await startServersCli();
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('rejects empty config value if no default passed', async () => {
global.process.argv.push('--config', '');
await startServersCli();
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('accepts empty config value if default passed', async () => {
global.process.argv.push('--config', '');
await startServersCli('foo');
expect(exitMock).not.toHaveBeenCalled();
});
it('rejects bail', async () => {
global.process.argv.push('--bail', true);
await startServersCli('foo');
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('accepts string value for kibana-install-dir', async () => {
global.process.argv.push('--kibana-install-dir', 'foo');
await startServersCli('foo');
expect(exitMock).not.toHaveBeenCalled();
});
it('rejects boolean value for kibana-install-dir', async () => {
global.process.argv.push('--kibana-install-dir');
await startServersCli('foo');
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('accepts boolean value for updateBaselines', async () => {
global.process.argv.push('--updateBaselines');
await startServersCli('foo');
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('accepts boolean value for updateSnapshots', async () => {
global.process.argv.push('--updateSnapshots');
await startServersCli('foo');
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
it('accepts source value for esFrom', async () => {
global.process.argv.push('--esFrom', 'source');
await startServersCli('foo');
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts debug option', async () => {
global.process.argv.push('--debug');
await startServersCli('foo');
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts silent option', async () => {
global.process.argv.push('--silent');
await startServersCli('foo');
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts quiet option', async () => {
global.process.argv.push('--quiet');
await startServersCli('foo');
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts verbose option', async () => {
global.process.argv.push('--verbose');
await startServersCli('foo');
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts extra server options', async () => {
global.process.argv.push('--', '--server.foo=bar');
await startServersCli('foo');
expect(exitMock).not.toHaveBeenCalled();
});
it('accepts help option even if invalid options passed', async () => {
global.process.argv.push('--debug', '--grep', '--help');
await startServersCli('foo');
expect(exitMock).not.toHaveBeenCalledWith(1);
});
it('rejects invalid options even if valid options exist', async () => {
global.process.argv.push('--debug', '--grep', '--bail');
await startServersCli('foo');
expect(exitMock).toHaveBeenCalledWith(1);
checkMockConsoleLogSnapshot(logMock);
});
});
});

View file

@ -18,6 +18,7 @@ interface RunElasticsearchOptions {
esFrom?: string;
config: Config;
onEarlyExit?: (msg: string) => void;
logsDir?: string;
}
interface CcsConfig {
@ -62,26 +63,41 @@ function getEsConfig({
export async function runElasticsearch(
options: RunElasticsearchOptions
): Promise<() => Promise<void>> {
const { log } = options;
const { log, logsDir } = options;
const config = getEsConfig(options);
if (!config.ccsConfig) {
const node = await startEsNode(log, 'ftr', config);
const node = await startEsNode({
log,
name: 'ftr',
logsDir,
config,
});
return async () => {
await node.cleanup();
};
}
const remotePort = await getPort();
const remoteNode = await startEsNode(log, 'ftr-remote', {
...config,
port: parseInt(new URL(config.ccsConfig.remoteClusterUrl).port, 10),
transportPort: remotePort,
const remoteNode = await startEsNode({
log,
name: 'ftr-remote',
logsDir,
config: {
...config,
port: parseInt(new URL(config.ccsConfig.remoteClusterUrl).port, 10),
transportPort: remotePort,
},
});
const localNode = await startEsNode(log, 'ftr-local', {
...config,
esArgs: [...config.esArgs, `cluster.remote.ftr-remote.seeds=localhost:${remotePort}`],
const localNode = await startEsNode({
log,
name: 'ftr-local',
logsDir,
config: {
...config,
esArgs: [...config.esArgs, `cluster.remote.ftr-remote.seeds=localhost:${remotePort}`],
},
});
return async () => {
@ -90,12 +106,19 @@ export async function runElasticsearch(
};
}
async function startEsNode(
log: ToolingLog,
name: string,
config: EsConfig & { transportPort?: number },
onEarlyExit?: (msg: string) => void
) {
async function startEsNode({
log,
name,
config,
onEarlyExit,
logsDir,
}: {
log: ToolingLog;
name: string;
config: EsConfig & { transportPort?: number };
onEarlyExit?: (msg: string) => void;
logsDir?: string;
}) {
const cluster = createTestEsCluster({
clusterName: `cluster-${name}`,
esArgs: config.esArgs,
@ -106,6 +129,7 @@ async function startEsNode(
port: config.port,
ssl: config.ssl,
log,
writeLogsToPath: logsDir ? resolve(logsDir, `es-cluster-${name}.log`) : undefined,
basePath: resolve(REPO_ROOT, '.es'),
nodes: [
{

View file

@ -42,7 +42,11 @@ export async function runKibanaServer({
}: {
procs: ProcRunner;
config: Config;
options: { installDir?: string; extraKbnOpts?: string[] };
options: {
installDir?: string;
extraKbnOpts?: string[];
logsDir?: string;
};
onEarlyExit?: (msg: string) => void;
}) {
const runOptions = config.get('kbnTestServer.runOptions');
@ -84,10 +88,14 @@ export async function runKibanaServer({
...(options.extraKbnOpts ?? []),
]);
const mainName = useTaskRunner ? 'kbn-ui' : 'kibana';
const promises = [
// main process
procs.run(useTaskRunner ? 'kbn-ui' : 'kibana', {
procs.run(mainName, {
...procRunnerOpts,
writeLogsToPath: options.logsDir
? Path.resolve(options.logsDir, `${mainName}.log`)
: undefined,
args: [
...prefixArgs,
...parseRawFlags([
@ -110,6 +118,9 @@ export async function runKibanaServer({
promises.push(
procs.run('kbn-tasks', {
...procRunnerOpts,
writeLogsToPath: options.logsDir
? Path.resolve(options.logsDir, 'kbn-tasks.log')
: undefined,
args: [
...prefixArgs,
...parseRawFlags([

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import Fs from 'fs';
import Path from 'path';
import { setTimeout } from 'timers/promises';
@ -51,6 +52,16 @@ const makeSuccessMessage = (options: StartServerOptions) => {
);
};
export async function initLogsDir(options: { logsDir?: string; createLogger(): ToolingLog }) {
if (options.logsDir) {
options
.createLogger()
.info(`Kibana/ES logs will be written to ${Path.relative(process.cwd(), options.logsDir)}/`);
Fs.mkdirSync(options.logsDir, { recursive: true });
}
}
/**
* Run servers and tests for each config
*/