From ae7a6cffeb6048a1333aed307d4f39297acfd1bb Mon Sep 17 00:00:00 2001
From: Cristina Amico
Date: Tue, 5 Oct 2021 11:50:52 +0200
Subject: [PATCH 01/78] [Fleet] Set code editor height to solve an overlap in
default policy settings (#113763)
---
.../package_policy_input_var_field.tsx | 58 +++++++++++--------
1 file changed, 33 insertions(+), 25 deletions(-)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx
index 398421278b723..954addd4202b1 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx
@@ -17,12 +17,17 @@ import {
EuiFieldPassword,
EuiCodeBlock,
} from '@elastic/eui';
+import styled from 'styled-components';
import type { RegistryVarsEntry } from '../../../../types';
import { CodeEditor } from '../../../../../../../../../../src/plugins/kibana_react/public';
import { MultiTextInput } from './multi_text_input';
+const FixedHeightDiv = styled.div`
+ height: 300px;
+`;
+
export const PackagePolicyInputVarField: React.FunctionComponent<{
varDef: RegistryVarsEntry;
value: any;
@@ -55,31 +60,34 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{
{value}
) : (
-
+
+
+
);
case 'bool':
return (
From b62566da335f1f761709e5dea389f87d667ba278 Mon Sep 17 00:00:00 2001
From: Liza Katz
Date: Tue, 5 Oct 2021 12:57:40 +0300
Subject: [PATCH 02/78] [ci-stats] Collects additional timings for cli tools
(#113030)
* [ci-stats] Collects additional metrics about bootstrap
Signed-off-by: Tyler Smalley
* test reporting
* [ci-stats] Collects additional metrics about bootstrap
Signed-off-by: Tyler Smalley
* Move ts timing to build_ts_refs_cli script
Signed-off-by: Tyler Smalley
* Add timings to run
Signed-off-by: Tyler Smalley
* info debug level
Signed-off-by: Tyler Smalley
* fix build
* Move report function to dev-utils
align name of tests
report from functional test runner
* report snapshot install \ ready times
fix event names
* Report memory usage and branch hash
* fix eslint
* fix integration test
* build
* mergy merge
* mergy merge 2
* eslint
* eslint
* ready events
* Update packages/kbn-es/src/cli_commands/snapshot.js
Co-authored-by: Tyler Smalley
* Update packages/kbn-es/src/cluster.js
Co-authored-by: Tyler Smalley
* Update packages/kbn-es/src/cli_commands/snapshot.js
Co-authored-by: Tyler Smalley
* Update packages/kbn-test/src/functional_tests/cli/start_servers/cli.js
Co-authored-by: Tyler Smalley
* Update src/dev/run_check_published_api_changes.ts
Co-authored-by: Tyler Smalley
* Update packages/kbn-test/src/jest/run.ts
Co-authored-by: Tyler Smalley
* Update src/dev/run_i18n_check.ts
Co-authored-by: Tyler Smalley
* Update packages/kbn-test/src/functional_test_runner/cli.ts
Co-authored-by: Tyler Smalley
* code review
* pm
* merge
* dist
* jest fix
Co-authored-by: Tyler Smalley
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Tyler Smalley
---
.../ci_stats_reporter/ci_stats_reporter.ts | 21 +++++++++++++---
.../src/ci_stats_reporter/index.ts | 1 +
.../src/ci_stats_reporter/report_time.ts | 25 +++++++++++++++++++
packages/kbn-es/src/cli_commands/snapshot.js | 20 ++++++++++++++-
packages/kbn-es/src/cluster.js | 18 ++++++++++++-
packages/kbn-pm/dist/index.js | 23 ++++++++++++++---
.../src/functional_test_runner/cli.ts | 18 ++++++++++++-
.../functional_tests/cli/start_servers/cli.js | 4 ++-
.../kbn-test/src/functional_tests/tasks.ts | 16 ++++++++++--
packages/kbn-test/src/jest/run.ts | 21 +++++++++++++---
src/dev/run_check_published_api_changes.ts | 19 +++++++++++++-
src/dev/run_i18n_check.ts | 23 +++++++++++++++--
12 files changed, 190 insertions(+), 19 deletions(-)
create mode 100644 packages/kbn-dev-utils/src/ci_stats_reporter/report_time.ts
diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts
index 4d6ea646b2ab1..45d31c1eefad9 100644
--- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts
+++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts
@@ -82,6 +82,7 @@ export class CiStatsReporter {
const upstreamBranch = options.upstreamBranch ?? this.getUpstreamBranch();
const kibanaUuid = options.kibanaUuid === undefined ? this.getKibanaUuid() : options.kibanaUuid;
let email;
+ let branch;
try {
const { stdout } = await execa('git', ['config', 'user.email']);
@@ -90,19 +91,33 @@ export class CiStatsReporter {
this.log.debug(e.message);
}
+ try {
+ const { stdout } = await execa('git', ['branch', '--show-current']);
+ branch = stdout;
+ } catch (e) {
+ this.log.debug(e.message);
+ }
+
+ const memUsage = process.memoryUsage();
const isElasticCommitter = email && email.endsWith('@elastic.co') ? true : false;
const defaultMetadata = {
+ kibanaUuid,
+ isElasticCommitter,
committerHash: email
? crypto.createHash('sha256').update(email).digest('hex').substring(0, 20)
: undefined,
+ email: isElasticCommitter ? email : undefined,
+ branch: isElasticCommitter ? branch : undefined,
cpuCount: Os.cpus()?.length,
cpuModel: Os.cpus()[0]?.model,
cpuSpeed: Os.cpus()[0]?.speed,
- email: isElasticCommitter ? email : undefined,
freeMem: Os.freemem(),
- isElasticCommitter,
- kibanaUuid,
+ memoryUsageRss: memUsage.rss,
+ memoryUsageHeapTotal: memUsage.heapTotal,
+ memoryUsageHeapUsed: memUsage.heapUsed,
+ memoryUsageExternal: memUsage.external,
+ memoryUsageArrayBuffers: memUsage.arrayBuffers,
nestedTiming: process.env.CI_STATS_NESTED_TIMING ? true : false,
osArch: Os.arch(),
osPlatform: Os.platform(),
diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts
index d99217c38b410..9cb05608526eb 100644
--- a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts
+++ b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts
@@ -8,3 +8,4 @@
export * from './ci_stats_reporter';
export * from './ship_ci_stats_cli';
+export { getTimeReporter } from './report_time';
diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/report_time.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/report_time.ts
new file mode 100644
index 0000000000000..d10250a03f091
--- /dev/null
+++ b/packages/kbn-dev-utils/src/ci_stats_reporter/report_time.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { CiStatsReporter, ToolingLog } from '..';
+
+export const getTimeReporter = (log: ToolingLog, group: string) => {
+ const reporter = CiStatsReporter.fromEnv(log);
+ return async (startTime: number, id: string, meta: Record) => {
+ await reporter.timings({
+ timings: [
+ {
+ group,
+ id,
+ ms: Date.now() - startTime,
+ meta,
+ },
+ ],
+ });
+ };
+};
diff --git a/packages/kbn-es/src/cli_commands/snapshot.js b/packages/kbn-es/src/cli_commands/snapshot.js
index 7f5653db72b49..e64dcb7c77318 100644
--- a/packages/kbn-es/src/cli_commands/snapshot.js
+++ b/packages/kbn-es/src/cli_commands/snapshot.js
@@ -8,6 +8,7 @@
const dedent = require('dedent');
const getopts = require('getopts');
+import { ToolingLog, getTimeReporter } from '@kbn/dev-utils';
const { Cluster } = require('../cluster');
exports.description = 'Downloads and run from a nightly snapshot';
@@ -36,6 +37,13 @@ exports.help = (defaults = {}) => {
};
exports.run = async (defaults = {}) => {
+ const runStartTime = Date.now();
+ const log = new ToolingLog({
+ level: 'info',
+ writeTo: process.stdout,
+ });
+ const reportTime = getTimeReporter(log, 'scripts/es snapshot');
+
const argv = process.argv.slice(2);
const options = getopts(argv, {
alias: {
@@ -56,12 +64,22 @@ exports.run = async (defaults = {}) => {
if (options['download-only']) {
await cluster.downloadSnapshot(options);
} else {
+ const installStartTime = Date.now();
const { installPath } = await cluster.installSnapshot(options);
if (options.dataArchive) {
await cluster.extractDataDirectory(installPath, options.dataArchive);
}
- await cluster.run(installPath, options);
+ reportTime(installStartTime, 'installed', {
+ success: true,
+ ...options,
+ });
+
+ await cluster.run(installPath, {
+ reportTime,
+ startTime: runStartTime,
+ ...options,
+ });
}
};
diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js
index ac4380da88be0..0866b14f4ade8 100644
--- a/packages/kbn-es/src/cluster.js
+++ b/packages/kbn-es/src/cluster.js
@@ -240,7 +240,7 @@ exports.Cluster = class Cluster {
* @return {undefined}
*/
_exec(installPath, opts = {}) {
- const { skipNativeRealmSetup = false, ...options } = opts;
+ const { skipNativeRealmSetup = false, reportTime = () => {}, startTime, ...options } = opts;
if (this._process || this._outcome) {
throw new Error('ES has already been started');
@@ -321,10 +321,17 @@ exports.Cluster = class Cluster {
await nativeRealm.setPasswords(options);
});
+ let reportSent = false;
// parse and forward es stdout to the log
this._process.stdout.on('data', (data) => {
const lines = parseEsLog(data.toString());
lines.forEach((line) => {
+ if (!reportSent && line.message.includes('publish_address')) {
+ reportSent = true;
+ reportTime(startTime, 'ready', {
+ success: true,
+ });
+ }
this._log.info(line.formattedMessage);
});
});
@@ -341,7 +348,16 @@ exports.Cluster = class Cluster {
// JVM exits with 143 on SIGTERM and 130 on SIGINT, dont' treat them as errors
if (code > 0 && !(code === 143 || code === 130)) {
+ reportTime(startTime, 'abort', {
+ success: true,
+ error: code,
+ });
throw createCliError(`ES exited with code ${code}`);
+ } else {
+ reportTime(startTime, 'error', {
+ success: false,
+ error: `exited with ${code}`,
+ });
}
});
}
diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js
index c96a1eb28cfce..cab1f6d916f02 100644
--- a/packages/kbn-pm/dist/index.js
+++ b/packages/kbn-pm/dist/index.js
@@ -9014,6 +9014,7 @@ class CiStatsReporter {
const upstreamBranch = (_options$upstreamBran = options.upstreamBranch) !== null && _options$upstreamBran !== void 0 ? _options$upstreamBran : this.getUpstreamBranch();
const kibanaUuid = options.kibanaUuid === undefined ? this.getKibanaUuid() : options.kibanaUuid;
let email;
+ let branch;
try {
const {
@@ -9024,16 +9025,32 @@ class CiStatsReporter {
this.log.debug(e.message);
}
+ try {
+ const {
+ stdout
+ } = await (0, _execa.default)('git', ['branch', '--show-current']);
+ branch = stdout;
+ } catch (e) {
+ this.log.debug(e.message);
+ }
+
+ const memUsage = process.memoryUsage();
const isElasticCommitter = email && email.endsWith('@elastic.co') ? true : false;
const defaultMetadata = {
+ kibanaUuid,
+ isElasticCommitter,
committerHash: email ? _crypto.default.createHash('sha256').update(email).digest('hex').substring(0, 20) : undefined,
+ email: isElasticCommitter ? email : undefined,
+ branch: isElasticCommitter ? branch : undefined,
cpuCount: (_Os$cpus = _os.default.cpus()) === null || _Os$cpus === void 0 ? void 0 : _Os$cpus.length,
cpuModel: (_Os$cpus$ = _os.default.cpus()[0]) === null || _Os$cpus$ === void 0 ? void 0 : _Os$cpus$.model,
cpuSpeed: (_Os$cpus$2 = _os.default.cpus()[0]) === null || _Os$cpus$2 === void 0 ? void 0 : _Os$cpus$2.speed,
- email: isElasticCommitter ? email : undefined,
freeMem: _os.default.freemem(),
- isElasticCommitter,
- kibanaUuid,
+ memoryUsageRss: memUsage.rss,
+ memoryUsageHeapTotal: memUsage.heapTotal,
+ memoryUsageHeapUsed: memUsage.heapUsed,
+ memoryUsageExternal: memUsage.external,
+ memoryUsageArrayBuffers: memUsage.arrayBuffers,
nestedTiming: process.env.CI_STATS_NESTED_TIMING ? true : false,
osArch: _os.default.arch(),
osPlatform: _os.default.platform(),
diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts
index ccd578aa038f8..3ad365a028b65 100644
--- a/packages/kbn-test/src/functional_test_runner/cli.ts
+++ b/packages/kbn-test/src/functional_test_runner/cli.ts
@@ -9,7 +9,7 @@
import { resolve } from 'path';
import { inspect } from 'util';
-import { run, createFlagError, Flags } from '@kbn/dev-utils';
+import { run, createFlagError, Flags, ToolingLog, getTimeReporter } from '@kbn/dev-utils';
import exitHook from 'exit-hook';
import { FunctionalTestRunner } from './functional_test_runner';
@@ -27,6 +27,12 @@ const parseInstallDir = (flags: Flags) => {
};
export function runFtrCli() {
+ const runStartTime = Date.now();
+ const toolingLog = new ToolingLog({
+ level: 'info',
+ writeTo: process.stdout,
+ });
+ const reportTime = getTimeReporter(toolingLog, 'scripts/functional_test_runner');
run(
async ({ flags, log }) => {
const functionalTestRunner = new FunctionalTestRunner(
@@ -68,9 +74,19 @@ export function runFtrCli() {
teardownRun = true;
if (err) {
+ await reportTime(runStartTime, 'total', {
+ success: false,
+ err: err.message,
+ ...flags,
+ });
log.indent(-log.indent());
log.error(err);
process.exitCode = 1;
+ } else {
+ await reportTime(runStartTime, 'total', {
+ success: true,
+ ...flags,
+ });
}
try {
diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js
index 824cf3e6ceec1..df7f8750b2ae3 100644
--- a/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js
+++ b/packages/kbn-test/src/functional_tests/cli/start_servers/cli.js
@@ -18,6 +18,8 @@ import { processOptions, displayHelp } from './args';
export async function startServersCli(defaultConfigPath) {
await runCli(displayHelp, async (userOptions) => {
const options = processOptions(userOptions, defaultConfigPath);
- await startServers(options);
+ await startServers({
+ ...options,
+ });
});
}
diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts
index d45f8656ed728..3bc697c143f40 100644
--- a/packages/kbn-test/src/functional_tests/tasks.ts
+++ b/packages/kbn-test/src/functional_tests/tasks.ts
@@ -9,7 +9,7 @@
import { relative } from 'path';
import * as Rx from 'rxjs';
import { startWith, switchMap, take } from 'rxjs/operators';
-import { withProcRunner, ToolingLog, REPO_ROOT } from '@kbn/dev-utils';
+import { withProcRunner, ToolingLog, REPO_ROOT, getTimeReporter } from '@kbn/dev-utils';
import dedent from 'dedent';
import {
@@ -147,7 +147,14 @@ interface StartServerOptions {
useDefaultConfig?: boolean;
}
-export async function startServers(options: StartServerOptions) {
+export async function startServers({ ...options }: StartServerOptions) {
+ const runStartTime = Date.now();
+ const toolingLog = new ToolingLog({
+ level: 'info',
+ writeTo: process.stdout,
+ });
+ const reportTime = getTimeReporter(toolingLog, 'scripts/functional_tests_server');
+
const log = options.createLogger();
const opts = {
...options,
@@ -170,6 +177,11 @@ export async function startServers(options: StartServerOptions) {
},
});
+ reportTime(runStartTime, 'ready', {
+ success: true,
+ ...options,
+ });
+
// wait for 5 seconds of silence before logging the
// success message so that it doesn't get buried
await silence(log, 5000);
diff --git a/packages/kbn-test/src/jest/run.ts b/packages/kbn-test/src/jest/run.ts
index 441104befde91..07610a3eb84c6 100644
--- a/packages/kbn-test/src/jest/run.ts
+++ b/packages/kbn-test/src/jest/run.ts
@@ -21,7 +21,8 @@ import { resolve, relative, sep as osSep } from 'path';
import { existsSync } from 'fs';
import { run } from 'jest';
import { buildArgv } from 'jest-cli/build/cli';
-import { ToolingLog } from '@kbn/dev-utils';
+import { ToolingLog, getTimeReporter } from '@kbn/dev-utils';
+import { map } from 'lodash';
// yarn test:jest src/core/server/saved_objects
// yarn test:jest src/core/public/core_system.test.ts
@@ -35,9 +36,14 @@ export function runJest(configName = 'jest.config.js') {
writeTo: process.stdout,
});
+ const runStartTime = Date.now();
+ const reportTime = getTimeReporter(log, 'scripts/jest');
+ let cwd: string;
+ let testFiles: string[];
+
if (!argv.config) {
- const cwd = process.env.INIT_CWD || process.cwd();
- const testFiles = argv._.splice(2).map((p) => resolve(cwd, p));
+ cwd = process.env.INIT_CWD || process.cwd();
+ testFiles = argv._.splice(2).map((p) => resolve(cwd, p));
const commonTestFiles = commonBasePath(testFiles);
const testFilesProvided = testFiles.length > 0;
@@ -73,7 +79,14 @@ export function runJest(configName = 'jest.config.js') {
process.env.NODE_ENV = 'test';
}
- run();
+ run().then(() => {
+ // Success means that tests finished, doesn't mean they passed.
+ reportTime(runStartTime, 'total', {
+ success: true,
+ isXpack: cwd.includes('x-pack'),
+ testFiles: map(testFiles, (testFile) => relative(cwd, testFile)),
+ });
+ });
}
/**
diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts
index 7c8105bc40c51..452922ac56bcd 100644
--- a/src/dev/run_check_published_api_changes.ts
+++ b/src/dev/run_check_published_api_changes.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { ToolingLog } from '@kbn/dev-utils';
+import { ToolingLog, getTimeReporter } from '@kbn/dev-utils';
import {
Extractor,
IConfigFile,
@@ -27,6 +27,9 @@ const log = new ToolingLog({
writeTo: process.stdout,
});
+const runStartTime = Date.now();
+const reportTime = getTimeReporter(log, 'scripts/check_published_api_changes');
+
/*
* Step 1: execute build:types
* This users tsconfig.types.json to generate types in `target/types`
@@ -184,6 +187,7 @@ async function run(folder: string, { opts }: { opts: Options }): Promise {
+ reportTime(runStartTime, 'error', {
+ success: false,
+ error: e.message,
+ });
log.error(e);
process.exitCode = 1;
});
diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts
index 48ce2e013fc29..8aa93d33f60fd 100644
--- a/src/dev/run_i18n_check.ts
+++ b/src/dev/run_i18n_check.ts
@@ -9,7 +9,7 @@
import chalk from 'chalk';
import Listr from 'listr';
-import { createFailError, run } from '@kbn/dev-utils';
+import { createFailError, run, ToolingLog, getTimeReporter } from '@kbn/dev-utils';
import { ErrorReporter, I18nConfig } from './i18n';
import {
extractDefaultMessages,
@@ -19,6 +19,14 @@ import {
mergeConfigs,
} from './i18n/tasks';
+const toolingLog = new ToolingLog({
+ level: 'info',
+ writeTo: process.stdout,
+});
+
+const runStartTime = Date.now();
+const reportTime = getTimeReporter(toolingLog, 'scripts/i18n_check');
+
const skipOnNoTranslations = ({ config }: { config: I18nConfig }) =>
!config.translations.length && 'No translations found.';
@@ -116,13 +124,24 @@ run(
const reporter = new ErrorReporter();
const messages: Map = new Map();
await list.run({ messages, reporter });
- } catch (error) {
+
+ reportTime(runStartTime, 'total', {
+ success: true,
+ });
+ } catch (error: Error | ErrorReporter) {
process.exitCode = 1;
if (error instanceof ErrorReporter) {
error.errors.forEach((e: string | Error) => log.error(e));
+ reportTime(runStartTime, 'error', {
+ success: false,
+ });
} else {
log.error('Unhandled exception!');
log.error(error);
+ reportTime(runStartTime, 'error', {
+ success: false,
+ error: error.message,
+ });
}
}
},
From a2ac439f56313b7a3fc4708f54a4deebf2615136 Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Tue, 5 Oct 2021 02:58:01 -0700
Subject: [PATCH 03/78] [Fleet] Remove enterprise license requirement for
custom registry URL (#113858)
---
.../services/epm/registry/registry_url.ts | 11 ++---------
.../fleet/server/services/fleet_server/index.ts | 17 ++++++++++-------
2 files changed, 12 insertions(+), 16 deletions(-)
diff --git a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts
index 568aafddecbad..dfca8511fd84c 100644
--- a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts
+++ b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { appContextService, licenseService } from '../../';
+import { appContextService } from '../../';
// from https://github.com/elastic/package-registry#docker (maybe from OpenAPI one day)
// the unused variables cause a TS warning about unused values
@@ -32,16 +32,9 @@ const getDefaultRegistryUrl = (): string => {
export const getRegistryUrl = (): string => {
const customUrl = appContextService.getConfig()?.registryUrl;
- const isEnterprise = licenseService.isEnterprise();
-
- if (customUrl && isEnterprise) {
- return customUrl;
- }
if (customUrl) {
- appContextService
- .getLogger()
- .warn('Enterprise license is required to use a custom registry url.');
+ return customUrl;
}
return getDefaultRegistryUrl();
diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts
index 733d962a86e9e..0d386b9ba4995 100644
--- a/x-pack/plugins/fleet/server/services/fleet_server/index.ts
+++ b/x-pack/plugins/fleet/server/services/fleet_server/index.ts
@@ -50,22 +50,25 @@ export async function startFleetServerSetup() {
_onResolve = resolve;
});
const logger = appContextService.getLogger();
+
+ // Check for security
if (!appContextService.hasSecurity()) {
// Fleet will not work if security is not enabled
logger?.warn('Fleet requires the security plugin to be enabled.');
return;
}
+ // Log information about custom registry URL
+ const customUrl = appContextService.getConfig()?.registryUrl;
+ if (customUrl) {
+ logger.info(
+ `Custom registry url is an experimental feature and is unsupported. Using custom registry at ${customUrl}`
+ );
+ }
+
try {
// We need licence to be initialized before using the SO service.
await licenseService.getLicenseInformation$()?.pipe(first())?.toPromise();
-
- const customUrl = appContextService.getConfig()?.registryUrl;
- const isEnterprise = licenseService.isEnterprise();
- if (customUrl && isEnterprise) {
- logger.info('Custom registry url is an experimental feature and is unsupported.');
- }
-
await runFleetServerMigration();
_isFleetServerSetup = true;
} catch (err) {
From 1e7d4e1adfa9aaebb8f34c81022e3816050cda56 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 5 Oct 2021 13:32:28 +0300
Subject: [PATCH 04/78] [Lens] Unskips the heatmap functional tests suite
(#113728)
* Stabilizes the lens heatmap functional tests
* Uncomment
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/test/functional/apps/lens/heatmap.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/x-pack/test/functional/apps/lens/heatmap.ts b/x-pack/test/functional/apps/lens/heatmap.ts
index ddc4130d388ce..deca06b6b351a 100644
--- a/x-pack/test/functional/apps/lens/heatmap.ts
+++ b/x-pack/test/functional/apps/lens/heatmap.ts
@@ -13,8 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const elasticChart = getService('elasticChart');
const testSubjects = getService('testSubjects');
- // FLAKY: https://github.com/elastic/kibana/issues/113043
- describe.skip('lens heatmap', () => {
+ describe('lens heatmap', () => {
before(async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
@@ -74,8 +73,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.openPalettePanel('lnsHeatmap');
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_0', '10', {
clearWithKeyboard: true,
+ typeCharByChar: true,
});
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.lens.waitForVisualization();
const debugState = await PageObjects.lens.getCurrentChartDebugState();
From 35e9f6ad6b23ade46480633fa5457731e583e065 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Tue, 5 Oct 2021 12:45:56 +0200
Subject: [PATCH 05/78] :bug: fix duplicate suggestion issue + missing over
time (#113449)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../editor_frame/suggestion_helpers.ts | 1 +
.../indexpattern_suggestions.test.tsx | 97 +++++++++++++++++++
.../indexpattern_suggestions.ts | 8 +-
x-pack/plugins/lens/public/types.ts | 1 +
4 files changed, 104 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
index 7f1e4aa58dba3..f3245759c9ef5 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
@@ -115,6 +115,7 @@ export function getSuggestions({
} else {
dataSourceSuggestions = datasource.getDatasourceSuggestionsFromCurrentState(
datasourceState,
+ (layerId) => isLayerSupportedByVisualization(layerId, [layerTypes.DATA]),
activeData
);
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
index a5d6db4be3319..bf4b10de386a1 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
@@ -1704,6 +1704,103 @@ describe('IndexPattern Data Source suggestions', () => {
);
});
+ it('adds date histogram over default time field for tables without time dimension and a threshold', async () => {
+ const initialState = testInitialState();
+ const state: IndexPatternPrivateState = {
+ ...initialState,
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['cola', 'colb'],
+ columns: {
+ cola: {
+ label: 'My Terms',
+ customLabel: true,
+ dataType: 'string',
+ isBucketed: true,
+ operationType: 'terms',
+ sourceField: 'source',
+ scale: 'ordinal',
+ params: {
+ orderBy: { type: 'alphabetical' },
+ orderDirection: 'asc',
+ size: 5,
+ },
+ },
+ colb: {
+ label: 'My Op',
+ customLabel: true,
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'average',
+ sourceField: 'bytes',
+ scale: 'ratio',
+ },
+ },
+ },
+ threshold: {
+ indexPatternId: '2',
+ columnOrder: ['thresholda'],
+ columns: {
+ thresholda: {
+ label: 'My Op',
+ customLabel: true,
+ dataType: 'number',
+ isBucketed: false,
+ operationType: 'average',
+ sourceField: 'bytes',
+ scale: 'ratio',
+ },
+ },
+ },
+ },
+ };
+
+ expect(
+ getSuggestionSubset(
+ getDatasourceSuggestionsFromCurrentState(state, (layerId) => layerId !== 'threshold')
+ )
+ ).toContainEqual(
+ expect.objectContaining({
+ table: {
+ isMultiRow: true,
+ changeType: 'extended',
+ label: 'Over time',
+ columns: [
+ {
+ columnId: 'cola',
+ operation: {
+ label: 'My Terms',
+ dataType: 'string',
+ isBucketed: true,
+ scale: 'ordinal',
+ },
+ },
+ {
+ columnId: 'id1',
+ operation: {
+ label: 'timestampLabel',
+ dataType: 'date',
+ isBucketed: true,
+ scale: 'interval',
+ },
+ },
+ {
+ columnId: 'colb',
+ operation: {
+ label: 'My Op',
+ dataType: 'number',
+ isBucketed: false,
+ scale: 'ratio',
+ },
+ },
+ ],
+ layerId: 'first',
+ },
+ })
+ );
+ });
+
it('does not create an over time suggestion if tables with numeric buckets with time dimension', async () => {
const initialState = testInitialState();
const state: IndexPatternPrivateState = {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
index 0fe0ef617dc27..604b63aa29246 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
@@ -350,9 +350,11 @@ function createNewLayerWithMetricAggregation(
}
export function getDatasourceSuggestionsFromCurrentState(
- state: IndexPatternPrivateState
+ state: IndexPatternPrivateState,
+ filterLayers: (layerId: string) => boolean = () => true
): Array> {
- const layers = Object.entries(state.layers || {});
+ const layers = Object.entries(state.layers || {}).filter(([layerId]) => filterLayers(layerId));
+
if (layers.length > 1) {
// Return suggestions that reduce the data to each layer individually
return layers
@@ -394,7 +396,7 @@ export function getDatasourceSuggestionsFromCurrentState(
}
return flatten(
- Object.entries(state.layers || {})
+ layers
.filter(([_id, layer]) => layer.columnOrder.length && layer.indexPatternId)
.map(([layerId, layer]) => {
const indexPattern = state.indexPatterns[layer.indexPatternId];
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 75ed5f4907e0b..2e7876f83fc41 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -246,6 +246,7 @@ export interface Datasource {
) => Array>;
getDatasourceSuggestionsFromCurrentState: (
state: T,
+ filterFn?: (layerId: string) => boolean,
activeData?: Record
) => Array>;
From 158b396ae1150d8ee63fa1e2b2fb8dcb33b72af5 Mon Sep 17 00:00:00 2001
From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com>
Date: Tue, 5 Oct 2021 14:08:54 +0300
Subject: [PATCH 06/78] [Discover] Fix navigation to a `new` from saved search
and saved query, fix `discover:searchOnPageLoad` (#112262)
* [Discover] fix saved search become active
* [Discover] add another fix to be consistent with data fetching code
* [Discover] simplify solution
* [Discover] add functionals
* [Discover] fix saved query bug, add functionals
* [Discover] fix functionals
* [Discover] fix functional test
* [Discover] split saved query tests
* [Discover] preselect logstash index pattern
* [Discover] remove saved query after test complete
* [Discover] change query fill order
* [Discover] try to fix unrelated functional test
* [Discover] one more fix
* [Discover] try to fix one more problem
* [Discover] fix commonly used time range test
* [Discover] revert uisettings init in before statement, do small adjustments
* [Discover] fix unit test
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../uninitialized/uninitialized.tsx | 2 +-
.../apps/main/discover_main_app.tsx | 3 +-
.../apps/main/discover_main_route.tsx | 2 -
.../apps/main/services/use_discover_state.ts | 10 +-
.../apps/main/services/use_saved_search.ts | 1 +
.../apps/main/utils/get_fetch_observable.ts | 15 ++-
.../main/utils/get_fetch_observeable.test.ts | 2 +
.../main/utils/get_state_defaults.test.ts | 2 +
.../apps/main/utils/get_state_defaults.ts | 1 +
test/functional/apps/discover/_discover.ts | 28 +----
.../apps/discover/_saved_queries.ts | 74 +++++++++---
.../apps/discover/_search_on_page_load.ts | 112 ++++++++++++++++++
test/functional/apps/discover/index.ts | 1 +
.../apps/discover/saved_searches.ts | 51 +++++++-
14 files changed, 248 insertions(+), 56 deletions(-)
create mode 100644 test/functional/apps/discover/_search_on_page_load.ts
diff --git a/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx b/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx
index c9e0c43900ba1..6c1b1bfc87d20 100644
--- a/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx
@@ -32,7 +32,7 @@ export const DiscoverUninitialized = ({ onRefresh }: Props) => {
}
actions={
-
+
{
@@ -46,7 +46,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
},
[history]
);
- const savedSearch = props.savedSearch;
/**
* State related logic
diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx
index 5141908e44ade..a95668642558c 100644
--- a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx
+++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx
@@ -75,8 +75,6 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
async function loadSavedSearch() {
try {
- // force a refresh if a given saved search without id was saved
- setSavedSearch(undefined);
const loadedSavedSearch = await services.getSavedSearchById(savedSearchId);
const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern(loadedSavedSearch);
if (loadedSavedSearch && !loadedSavedSearch?.searchSource.getField('index')) {
diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts
index e11a9937111a1..223d896b16cd1 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts
@@ -96,6 +96,7 @@ export function useDiscoverState({
useEffect(() => {
const stopSync = stateContainer.initializeAndSync(indexPattern, filterManager, data);
+
return () => stopSync();
}, [stateContainer, filterManager, data, indexPattern]);
@@ -209,16 +210,13 @@ export function useDiscoverState({
}, [config, data, savedSearch, reset, stateContainer]);
/**
- * Initial data fetching, also triggered when index pattern changes
+ * Trigger data fetching on indexPattern or savedSearch changes
*/
useEffect(() => {
- if (!indexPattern) {
- return;
- }
- if (initialFetchStatus === FetchStatus.LOADING) {
+ if (indexPattern) {
refetch$.next();
}
- }, [initialFetchStatus, refetch$, indexPattern]);
+ }, [initialFetchStatus, refetch$, indexPattern, savedSearch.id]);
return {
data$,
diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
index 26f95afba5a93..d11c76283fedd 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
@@ -156,6 +156,7 @@ export const useSavedSearch = ({
refetch$,
searchSessionManager,
searchSource,
+ initialFetchStatus,
});
const subscription = fetch$.subscribe((val) => {
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts
index aac6196e64f6f..528f0e74d3ed6 100644
--- a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts
+++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { merge } from 'rxjs';
-import { debounceTime, filter, tap } from 'rxjs/operators';
+import { debounceTime, filter, skip, tap } from 'rxjs/operators';
import { FetchStatus } from '../../../types';
import type {
@@ -26,6 +26,7 @@ export function getFetch$({
main$,
refetch$,
searchSessionManager,
+ initialFetchStatus,
}: {
setAutoRefreshDone: (val: AutoRefreshDoneFn | undefined) => void;
data: DataPublicPluginStart;
@@ -33,10 +34,11 @@ export function getFetch$({
refetch$: DataRefetch$;
searchSessionManager: DiscoverSearchSessionManager;
searchSource: SearchSource;
+ initialFetchStatus: FetchStatus;
}) {
const { timefilter } = data.query.timefilter;
const { filterManager } = data.query;
- return merge(
+ let fetch$ = merge(
refetch$,
filterManager.getFetches$(),
timefilter.getFetch$(),
@@ -58,4 +60,13 @@ export function getFetch$({
data.query.queryString.getUpdates$(),
searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId))
).pipe(debounceTime(100));
+
+ /**
+ * Skip initial fetch when discover:searchOnPageLoad is disabled.
+ */
+ if (initialFetchStatus === FetchStatus.UNINITIALIZED) {
+ fetch$ = fetch$.pipe(skip(1));
+ }
+
+ return fetch$;
}
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts
index 5f728b115b2e9..39873ff609d64 100644
--- a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts
+++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observeable.test.ts
@@ -58,6 +58,7 @@ describe('getFetchObservable', () => {
data: createDataMock(new Subject(), new Subject(), new Subject(), new Subject()),
searchSessionManager: searchSessionManagerMock.searchSessionManager,
searchSource: savedSearchMock.searchSource,
+ initialFetchStatus: FetchStatus.LOADING,
});
fetch$.subscribe(() => {
@@ -81,6 +82,7 @@ describe('getFetchObservable', () => {
data: dataMock,
searchSessionManager: searchSessionManagerMock.searchSessionManager,
searchSource: savedSearchMockWithTimeField.searchSource,
+ initialFetchStatus: FetchStatus.LOADING,
});
const fetchfnMock = jest.fn();
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts
index 554aca6ddb8f1..04ee5f414e7f4 100644
--- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts
+++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts
@@ -31,6 +31,7 @@ describe('getStateDefaults', () => {
"index": "index-pattern-with-timefield-id",
"interval": "auto",
"query": undefined,
+ "savedQuery": undefined,
"sort": Array [
Array [
"timestamp",
@@ -59,6 +60,7 @@ describe('getStateDefaults', () => {
"index": "the-index-pattern-id",
"interval": "auto",
"query": undefined,
+ "savedQuery": undefined,
"sort": Array [],
}
`);
diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts
index 4061d9a61f0a3..cd23d52022374 100644
--- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts
+++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts
@@ -47,6 +47,7 @@ export function getStateDefaults({
interval: 'auto',
filters: cloneDeep(searchSource.getOwnField('filter')),
hideChart: undefined,
+ savedQuery: undefined,
} as AppState;
if (savedSearch.grid) {
defaultState.grid = savedSearch.grid;
diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts
index 4edc4d22f0753..0a8f56ee250ea 100644
--- a/test/functional/apps/discover/_discover.ts
+++ b/test/functional/apps/discover/_discover.ts
@@ -233,11 +233,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('time zone switch', () => {
it('should show bars in the correct time zone after switching', async function () {
- await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' });
+ await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'America/Phoenix' });
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.awaitKibanaChrome();
- await queryBar.clearQuery();
await PageObjects.timePicker.setDefaultAbsoluteRange();
+ await queryBar.clearQuery();
log.debug(
'check that the newest doc timestamp is now -7 hours from the UTC time in the first test'
@@ -246,36 +246,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(rowData.startsWith('Sep 22, 2015 @ 16:50:13.253')).to.be.ok();
});
});
- describe('usage of discover:searchOnPageLoad', () => {
- it('should not fetch data from ES initially when discover:searchOnPageLoad is false', async function () {
- await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': false });
- await PageObjects.common.navigateToApp('discover');
- await PageObjects.header.awaitKibanaChrome();
-
- expect(await PageObjects.discover.getNrOfFetches()).to.be(0);
- });
-
- it('should fetch data from ES initially when discover:searchOnPageLoad is true', async function () {
- await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': true });
- await PageObjects.common.navigateToApp('discover');
- await PageObjects.header.awaitKibanaChrome();
- await retry.waitFor('number of fetches to be 1', async () => {
- const nrOfFetches = await PageObjects.discover.getNrOfFetches();
- return nrOfFetches === 1;
- });
- });
- });
describe('invalid time range in URL', function () {
it('should get the default timerange', async function () {
- const prevTime = await PageObjects.timePicker.getTimeConfig();
await PageObjects.common.navigateToUrl('discover', '#/?_g=(time:(from:now-15m,to:null))', {
useActualUrl: true,
});
await PageObjects.header.awaitKibanaChrome();
const time = await PageObjects.timePicker.getTimeConfig();
- expect(time.start).to.be(prevTime.start);
- expect(time.end).to.be(prevTime.end);
+ expect(time.start).to.be('~ 15 minutes ago');
+ expect(time.end).to.be('now');
});
});
diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts
index 20f2cab907d9b..832d895fcea3d 100644
--- a/test/functional/apps/discover/_saved_queries.ts
+++ b/test/functional/apps/discover/_saved_queries.ts
@@ -17,39 +17,83 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
const browser = getService('browser');
-
- const defaultSettings = {
- defaultIndex: 'logstash-*',
- };
const filterBar = getService('filterBar');
const queryBar = getService('queryBar');
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
const testSubjects = getService('testSubjects');
+ const defaultSettings = {
+ defaultIndex: 'logstash-*',
+ };
+
+ const setUpQueriesWithFilters = async () => {
+ // set up a query with filters and a time filter
+ log.debug('set up a query with filters to save');
+ const fromTime = 'Sep 20, 2015 @ 08:00:00.000';
+ const toTime = 'Sep 21, 2015 @ 08:00:00.000';
+ await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
+ await filterBar.addFilter('extension.raw', 'is one of', 'jpg');
+ await queryBar.setQuery('response:200');
+ };
describe('saved queries saved objects', function describeIndexTests() {
before(async function () {
log.debug('load kibana index with default index pattern');
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
+
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json');
+ await esArchiver.load('test/functional/fixtures/es_archiver/date_nested');
+ await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional');
- // and load a set of makelogs data
- await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace(defaultSettings);
log.debug('discover');
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setDefaultAbsoluteRange();
});
- describe('saved query management component functionality', function () {
- before(async function () {
- // set up a query with filters and a time filter
- log.debug('set up a query with filters to save');
- await queryBar.setQuery('response:200');
- await filterBar.addFilter('extension.raw', 'is one of', 'jpg');
- const fromTime = 'Sep 20, 2015 @ 08:00:00.000';
- const toTime = 'Sep 21, 2015 @ 08:00:00.000';
- await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
+ after(async () => {
+ await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
+ await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested');
+ await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
+ });
+
+ describe('saved query selection', () => {
+ before(async () => await setUpQueriesWithFilters());
+
+ it(`should unselect saved query when navigating to a 'new'`, async function () {
+ await savedQueryManagementComponent.saveNewQuery(
+ 'test-unselect-saved-query',
+ 'mock',
+ true,
+ true
+ );
+
+ await queryBar.submitQuery();
+
+ expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true);
+ expect(await queryBar.getQueryString()).to.eql('response:200');
+
+ await PageObjects.discover.clickNewSearchButton();
+
+ expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false);
+ expect(await queryBar.getQueryString()).to.eql('');
+
+ await PageObjects.discover.selectIndexPattern('date-nested');
+
+ expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false);
+ expect(await queryBar.getQueryString()).to.eql('');
+
+ await PageObjects.discover.selectIndexPattern('logstash-*');
+
+ expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(false);
+ expect(await queryBar.getQueryString()).to.eql('');
+
+ // reset state
+ await savedQueryManagementComponent.deleteSavedQuery('test-unselect-saved-query');
});
+ });
+
+ describe('saved query management component functionality', function () {
+ before(async () => await setUpQueriesWithFilters());
it('should show the saved query management component when there are no saved queries', async () => {
await savedQueryManagementComponent.openSavedQueryManagementComponent();
diff --git a/test/functional/apps/discover/_search_on_page_load.ts b/test/functional/apps/discover/_search_on_page_load.ts
new file mode 100644
index 0000000000000..2a66e03c3cbb8
--- /dev/null
+++ b/test/functional/apps/discover/_search_on_page_load.ts
@@ -0,0 +1,112 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const log = getService('log');
+ const retry = getService('retry');
+ const esArchiver = getService('esArchiver');
+ const queryBar = getService('queryBar');
+ const kibanaServer = getService('kibanaServer');
+ const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
+ const testSubjects = getService('testSubjects');
+
+ const defaultSettings = {
+ defaultIndex: 'logstash-*',
+ };
+
+ const initSearchOnPageLoad = async (searchOnPageLoad: boolean) => {
+ await kibanaServer.uiSettings.replace({ 'discover:searchOnPageLoad': searchOnPageLoad });
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.header.awaitKibanaChrome();
+ };
+
+ const waitForFetches = (fetchesNumber: number) => async () => {
+ const nrOfFetches = await PageObjects.discover.getNrOfFetches();
+ return nrOfFetches === fetchesNumber;
+ };
+
+ describe('usage of discover:searchOnPageLoad', () => {
+ before(async function () {
+ log.debug('load kibana index with default index pattern');
+
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json');
+
+ // and load a set of data
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
+ await esArchiver.load('test/functional/fixtures/es_archiver/date_nested');
+
+ await kibanaServer.uiSettings.replace(defaultSettings);
+ await PageObjects.common.navigateToApp('discover');
+ });
+
+ after(async () => {
+ await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
+ await esArchiver.load('test/functional/fixtures/es_archiver/date_nested');
+ await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
+ });
+
+ describe(`when it's false`, () => {
+ beforeEach(async () => await initSearchOnPageLoad(false));
+
+ it('should not fetch data from ES initially', async function () {
+ expect(await testSubjects.exists('refreshDataButton')).to.be(true);
+ await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+ });
+
+ it('should not fetch on indexPattern change', async function () {
+ expect(await testSubjects.exists('refreshDataButton')).to.be(true);
+ await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+
+ await PageObjects.discover.selectIndexPattern('date-nested');
+
+ expect(await testSubjects.exists('refreshDataButton')).to.be(true);
+ await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+ });
+
+ it('should fetch data from ES after refreshDataButton click', async function () {
+ expect(await testSubjects.exists('refreshDataButton')).to.be(true);
+ await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+
+ await testSubjects.click('refreshDataButton');
+
+ await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ expect(await testSubjects.exists('refreshDataButton')).to.be(false);
+ });
+
+ it('should fetch data from ES after submit query', async function () {
+ expect(await testSubjects.exists('refreshDataButton')).to.be(true);
+ await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+
+ await queryBar.submitQuery();
+
+ await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ expect(await testSubjects.exists('refreshDataButton')).to.be(false);
+ });
+
+ it('should fetch data from ES after choosing commonly used time range', async function () {
+ await PageObjects.discover.selectIndexPattern('logstash-*');
+ expect(await testSubjects.exists('refreshDataButton')).to.be(true);
+ await retry.waitFor('number of fetches to be 0', waitForFetches(0));
+
+ await PageObjects.timePicker.setCommonlyUsedTime('This_week');
+
+ await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ expect(await testSubjects.exists('refreshDataButton')).to.be(false);
+ });
+ });
+
+ it(`when it's false should fetch data from ES initially`, async function () {
+ await initSearchOnPageLoad(true);
+ await retry.waitFor('number of fetches to be 1', waitForFetches(1));
+ });
+ });
+}
diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts
index 3a18a55fe138b..59191b489f4c7 100644
--- a/test/functional/apps/discover/index.ts
+++ b/test/functional/apps/discover/index.ts
@@ -51,5 +51,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_runtime_fields_editor'));
loadTestFile(require.resolve('./_huge_fields'));
loadTestFile(require.resolve('./_date_nested'));
+ loadTestFile(require.resolve('./_search_on_page_load'));
});
}
diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts
index 1d8de9fe9fb6d..5d8c2aff3ed5f 100644
--- a/x-pack/test/functional/apps/discover/saved_searches.ts
+++ b/x-pack/test/functional/apps/discover/saved_searches.ts
@@ -16,16 +16,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dataGrid = getService('dataGrid');
const panelActions = getService('dashboardPanelActions');
const panelActionsTimeRange = getService('dashboardPanelTimeRange');
+ const queryBar = getService('queryBar');
+ const filterBar = getService('filterBar');
const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json';
+ const defaultSettings = {
+ defaultIndex: 'logstash-*',
+ 'doc_table:legacy': false,
+ };
+
+ const setTimeRange = async () => {
+ const fromTime = 'Apr 27, 2019 @ 23:56:51.374';
+ const toTime = 'Aug 23, 2019 @ 16:18:51.821';
+ await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
+ };
describe('Discover Saved Searches', () => {
before('initialize tests', async () => {
await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
await kibanaServer.importExport.load(ecommerceSOPath);
- await kibanaServer.uiSettings.update({ 'doc_table:legacy': false });
+ await kibanaServer.uiSettings.update(defaultSettings);
});
+
after('clean up archives', async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce');
+ await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
await kibanaServer.importExport.unload(ecommerceSOPath);
await kibanaServer.uiSettings.unset('doc_table:legacy');
});
@@ -34,9 +49,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should be possible to customize time range for saved searches on dashboards', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.clickNewDashboard();
- const fromTime = 'Apr 27, 2019 @ 23:56:51.374';
- const toTime = 'Aug 23, 2019 @ 16:18:51.821';
- await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
+ await setTimeRange();
await dashboardAddPanel.clickOpenAddPanel();
await dashboardAddPanel.addSavedSearch('Ecommerce Data');
expect(await dataGrid.getDocCount()).to.be(500);
@@ -49,5 +62,35 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await dataGrid.hasNoResults()).to.be(true);
});
});
+
+ it(`should unselect saved search when navigating to a 'new'`, async function () {
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.discover.selectIndexPattern('ecommerce');
+ await setTimeRange();
+ await filterBar.addFilter('category', 'is', `Men's Shoes`);
+ await queryBar.setQuery('customer_gender:MALE');
+
+ await PageObjects.discover.saveSearch('test-unselect-saved-search');
+
+ await queryBar.submitQuery();
+
+ expect(await filterBar.hasFilter('category', `Men's Shoes`)).to.be(true);
+ expect(await queryBar.getQueryString()).to.eql('customer_gender:MALE');
+
+ await PageObjects.discover.clickNewSearchButton();
+
+ expect(await filterBar.hasFilter('category', `Men's Shoes`)).to.be(false);
+ expect(await queryBar.getQueryString()).to.eql('');
+
+ await PageObjects.discover.selectIndexPattern('logstash-*');
+
+ expect(await filterBar.hasFilter('category', `Men's Shoes`)).to.be(false);
+ expect(await queryBar.getQueryString()).to.eql('');
+
+ await PageObjects.discover.selectIndexPattern('ecommerce');
+
+ expect(await filterBar.hasFilter('category', `Men's Shoes`)).to.be(false);
+ expect(await queryBar.getQueryString()).to.eql('');
+ });
});
}
From a4b74bd3980cce77d4e9d77b10643bfe76dde126 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Tue, 5 Oct 2021 13:30:56 +0200
Subject: [PATCH 07/78] [8.0] Remove legacy logging (#112305)
* remove kbn-legacy-logging package
* remove legacy service
* remove legacy appender
* remove LegacyObjectToConfigAdapter
* gix types
* remove @hapi/good / @hapi/good-squeeze / @hapi/podium
* remove `default` appender validation for `root` logger
* remove old config key from kibana-docker
* fix FTR config
* fix dev server
* remove reference from readme
* fix unit test
* clean CLI args and remove quiet option
* fix type
* fix status test config
* remove from test config
* fix snapshot
* use another regexp
* update generated doc
* fix createRootWithSettings
* fix some integration tests
* another IT fix
* yet another IT fix
* (will be reverted) add assertion for CI failure
* Revert "(will be reverted) add assertion for CI failure"
This reverts commit 78d5560f9e9cb42030f001919f6ab090dbbbcb3b.
* switch back to json layout for test
* remove legacy logging config deprecations
* address some review comments
* update documentation
* update kibana.yml config examples
* add config example for `metrics.ops`
Co-authored-by: Tyler Smalley
---
.github/CODEOWNERS | 1 -
config/kibana.yml | 30 +-
.../logging-configuration-migration.asciidoc | 4 -
.../monorepo-packages.asciidoc | 1 -
...a-plugin-core-server.appenderconfigtype.md | 2 +-
docs/settings/logging-settings.asciidoc | 10 -
.../production.asciidoc | 11 +-
package.json | 4 -
packages/BUILD.bazel | 1 -
packages/kbn-cli-dev-mode/src/bootstrap.ts | 2 +-
.../kbn-cli-dev-mode/src/cli_dev_mode.test.ts | 1 -
packages/kbn-cli-dev-mode/src/cli_dev_mode.ts | 5 +-
.../kbn-cli-dev-mode/src/dev_server.test.ts | 1 -
packages/kbn-cli-dev-mode/src/log.ts | 8 +-
.../src/using_server_process.ts | 2 +-
packages/kbn-config/src/__mocks__/env.ts | 1 -
.../src/__snapshots__/env.test.ts.snap | 6 -
packages/kbn-config/src/config_service.ts | 4 +-
packages/kbn-config/src/env.ts | 2 -
packages/kbn-config/src/index.ts | 1 -
...gacy_object_to_config_adapter.test.ts.snap | 95 -----
packages/kbn-config/src/legacy/index.ts | 12 -
.../legacy_object_to_config_adapter.test.ts | 161 --------
.../legacy/legacy_object_to_config_adapter.ts | 65 ----
packages/kbn-legacy-logging/BUILD.bazel | 107 ------
packages/kbn-legacy-logging/README.md | 4 -
packages/kbn-legacy-logging/jest.config.js | 13 -
packages/kbn-legacy-logging/package.json | 8 -
.../src/get_logging_config.ts | 85 -----
packages/kbn-legacy-logging/src/index.ts | 14 -
.../src/legacy_logging_server.test.ts | 105 ------
.../src/legacy_logging_server.ts | 140 -------
packages/kbn-legacy-logging/src/log_events.ts | 71 ----
packages/kbn-legacy-logging/src/log_format.ts | 176 ---------
.../src/log_format_json.test.ts | 281 --------------
.../kbn-legacy-logging/src/log_format_json.ts | 23 --
.../src/log_format_string.test.ts | 64 ----
.../src/log_format_string.ts | 65 ----
.../src/log_interceptor.test.ts | 153 --------
.../kbn-legacy-logging/src/log_interceptor.ts | 144 -------
.../src/log_reporter.test.ts | 131 -------
.../kbn-legacy-logging/src/log_reporter.ts | 49 ---
packages/kbn-legacy-logging/src/metadata.ts | 42 ---
.../kbn-legacy-logging/src/rotate/index.ts | 41 --
.../src/rotate/log_rotator.test.ts | 261 -------------
.../src/rotate/log_rotator.ts | 352 ------------------
packages/kbn-legacy-logging/src/schema.ts | 97 -----
.../src/setup_logging.test.ts | 35 --
.../kbn-legacy-logging/src/setup_logging.ts | 41 --
.../src/utils/apply_filters_to_keys.test.ts | 49 ---
.../src/utils/apply_filters_to_keys.ts | 50 ---
.../src/utils/get_payload_size.test.ts | 158 --------
.../src/utils/get_payload_size.ts | 71 ----
.../kbn-legacy-logging/src/utils/index.ts | 10 -
packages/kbn-legacy-logging/tsconfig.json | 15 -
.../__fixtures__/invalid_config.yml | 10 +
.../reload_logging_config/kibana.test.yml | 13 -
.../integration_tests/invalid_config.test.ts | 39 +-
.../reload_logging_config.test.ts | 76 ----
src/cli/serve/serve.js | 16 +-
.../deprecation/core_deprecations.test.ts | 227 +----------
.../config/deprecation/core_deprecations.ts | 251 -------------
src/core/server/config/index.ts | 1 -
.../config_deprecation.test.ts | 8 +-
.../elasticsearch/elasticsearch_config.ts | 2 +-
.../http/integration_tests/logging.test.ts | 4 -
src/core/server/legacy/index.ts | 11 -
.../legacy/integration_tests/logging.test.ts | 234 ------------
src/core/server/legacy/legacy_service.mock.ts | 21 --
.../legacy/legacy_service.test.mocks.ts | 18 -
src/core/server/legacy/legacy_service.test.ts | 197 ----------
src/core/server/legacy/legacy_service.ts | 75 ----
.../legacy_appender.test.ts.snap | 142 -------
.../logging/appenders/legacy_appender.test.ts | 135 -------
.../logging/appenders/legacy_appender.ts | 52 ---
src/core/server/logging/README.mdx | 5 -
.../logging/appenders/appenders.test.ts | 8 -
.../server/logging/appenders/appenders.ts | 8 -
.../logging/integration_tests/logging.test.ts | 1 -
.../rolling_file_appender.test.ts | 1 -
.../server/logging/logging_config.test.ts | 27 +-
src/core/server/logging/logging_config.ts | 27 +-
.../server/logging/logging_system.test.ts | 5 -
src/core/server/server.api.md | 3 +-
src/core/server/server.test.mocks.ts | 21 +-
src/core/server/server.test.ts | 5 -
src/core/server/server.ts | 8 -
src/core/test_helpers/kbn_server.ts | 7 +-
.../resources/base/bin/kibana-docker | 11 -
.../server/collectors/config_usage/README.md | 1 -
test/common/config.js | 1 -
.../http/platform/config.status.ts | 2 +-
x-pack/plugins/security/server/config.test.ts | 2 +-
yarn.lock | 30 +-
94 files changed, 108 insertions(+), 4882 deletions(-)
delete mode 100644 packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
delete mode 100644 packages/kbn-config/src/legacy/index.ts
delete mode 100644 packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts
delete mode 100644 packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts
delete mode 100644 packages/kbn-legacy-logging/BUILD.bazel
delete mode 100644 packages/kbn-legacy-logging/README.md
delete mode 100644 packages/kbn-legacy-logging/jest.config.js
delete mode 100644 packages/kbn-legacy-logging/package.json
delete mode 100644 packages/kbn-legacy-logging/src/get_logging_config.ts
delete mode 100644 packages/kbn-legacy-logging/src/index.ts
delete mode 100644 packages/kbn-legacy-logging/src/legacy_logging_server.test.ts
delete mode 100644 packages/kbn-legacy-logging/src/legacy_logging_server.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_events.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_format.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_format_json.test.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_format_json.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_format_string.test.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_format_string.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_interceptor.test.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_interceptor.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_reporter.test.ts
delete mode 100644 packages/kbn-legacy-logging/src/log_reporter.ts
delete mode 100644 packages/kbn-legacy-logging/src/metadata.ts
delete mode 100644 packages/kbn-legacy-logging/src/rotate/index.ts
delete mode 100644 packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts
delete mode 100644 packages/kbn-legacy-logging/src/rotate/log_rotator.ts
delete mode 100644 packages/kbn-legacy-logging/src/schema.ts
delete mode 100644 packages/kbn-legacy-logging/src/setup_logging.test.ts
delete mode 100644 packages/kbn-legacy-logging/src/setup_logging.ts
delete mode 100644 packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.test.ts
delete mode 100644 packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.ts
delete mode 100644 packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts
delete mode 100644 packages/kbn-legacy-logging/src/utils/get_payload_size.ts
delete mode 100644 packages/kbn-legacy-logging/src/utils/index.ts
delete mode 100644 packages/kbn-legacy-logging/tsconfig.json
delete mode 100644 src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml
delete mode 100644 src/core/server/legacy/index.ts
delete mode 100644 src/core/server/legacy/integration_tests/logging.test.ts
delete mode 100644 src/core/server/legacy/legacy_service.mock.ts
delete mode 100644 src/core/server/legacy/legacy_service.test.mocks.ts
delete mode 100644 src/core/server/legacy/legacy_service.test.ts
delete mode 100644 src/core/server/legacy/legacy_service.ts
delete mode 100644 src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap
delete mode 100644 src/core/server/legacy/logging/appenders/legacy_appender.test.ts
delete mode 100644 src/core/server/legacy/logging/appenders/legacy_appender.ts
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 149f5cd74d8c0..244689025173f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -243,7 +243,6 @@
/packages/kbn-std/ @elastic/kibana-core
/packages/kbn-config/ @elastic/kibana-core
/packages/kbn-logging/ @elastic/kibana-core
-/packages/kbn-legacy-logging/ @elastic/kibana-core
/packages/kbn-crypto/ @elastic/kibana-core
/packages/kbn-http-tools/ @elastic/kibana-core
/src/plugins/saved_objects_management/ @elastic/kibana-core
diff --git a/config/kibana.yml b/config/kibana.yml
index dea9849f17b28..13a4b9bb98e85 100644
--- a/config/kibana.yml
+++ b/config/kibana.yml
@@ -84,24 +84,32 @@
# Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable.
#elasticsearch.shardTimeout: 30000
-# Logs queries sent to Elasticsearch. Requires logging.verbose set to true.
-#elasticsearch.logQueries: false
-
# Specifies the path where Kibana creates the process ID file.
#pid.file: /run/kibana/kibana.pid
+# Set the value of this setting to off to suppress all logging output, or to debug to log everything.
+# logging.root.level: debug
+
# Enables you to specify a file where Kibana stores log output.
-#logging.dest: stdout
+# logging.appenders.default:
+# type: file
+# fileName: /var/logs/kibana.log
+
-# Set the value of this setting to true to suppress all logging output.
-#logging.silent: false
+# Logs queries sent to Elasticsearch.
+# logging.loggers:
+# - name: elasticsearch.queries
+# level: debug
-# Set the value of this setting to true to suppress all logging output other than error messages.
-#logging.quiet: false
+# Logs http responses.
+# logging.loggers:
+# - name: http.server.response
+# level: debug
-# Set the value of this setting to true to log all events, including system usage information
-# and all requests.
-#logging.verbose: false
+# Logs system usage information.
+# logging.loggers:
+# - name: metrics.ops
+# level: debug
# Set the interval in milliseconds to sample system and process performance
# metrics. Minimum is 100ms. Defaults to 5000.
diff --git a/docs/developer/architecture/core/logging-configuration-migration.asciidoc b/docs/developer/architecture/core/logging-configuration-migration.asciidoc
index 19f10a881d5e8..db02b4d4e507f 100644
--- a/docs/developer/architecture/core/logging-configuration-migration.asciidoc
+++ b/docs/developer/architecture/core/logging-configuration-migration.asciidoc
@@ -76,9 +76,5 @@ you can override the flags with:
|--verbose| --logging.root.level=debug --logging.root.appenders[0]=default --logging.root.appenders[1]=custom | --verbose
-|--quiet| --logging.root.level=error --logging.root.appenders[0]=default --logging.root.appenders[1]=custom | not supported
-
|--silent| --logging.root.level=off | --silent
|===
-
-NOTE: To preserve backwards compatibility, you are required to pass the root `default` appender until the legacy logging system is removed in `v8.0`.
diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc
index b42bc980c8758..7754463339771 100644
--- a/docs/developer/getting-started/monorepo-packages.asciidoc
+++ b/docs/developer/getting-started/monorepo-packages.asciidoc
@@ -74,7 +74,6 @@ yarn kbn watch
- @kbn/i18n
- @kbn/interpreter
- @kbn/io-ts-utils
-- @kbn/legacy-logging
- @kbn/logging
- @kbn/mapbox-gl
- @kbn/monaco
diff --git a/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md b/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md
index f6de959589eca..7d9772af91c38 100644
--- a/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md
+++ b/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md
@@ -8,5 +8,5 @@
Signature:
```typescript
-export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig;
+export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig;
```
diff --git a/docs/settings/logging-settings.asciidoc b/docs/settings/logging-settings.asciidoc
index 77f3bd90a911a..177d1bc8db118 100644
--- a/docs/settings/logging-settings.asciidoc
+++ b/docs/settings/logging-settings.asciidoc
@@ -12,16 +12,6 @@
Refer to the <> for common configuration use cases. To learn more about possible configuration values, go to {kibana-ref}/logging-service.html[{kib}'s Logging service].
-[[log-settings-compatibility]]
-==== Backwards compatibility
-Compatibility with the legacy logging system is assured until the end of the `v7` version.
-All log messages handled by `root` context (default) are forwarded to the legacy logging service.
-The logging configuration is validated against the predefined schema and if there are
-any issues with it, {kib} will fail to start with the detailed error message.
-
-NOTE: When you switch to the new logging configuration, you will start seeing duplicate log entries in both formats.
-These will be removed when the `default` appender is no longer required.
-
[[log-settings-examples]]
==== Examples
Here are some configuration examples for the most common logging use cases:
diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc
index 455e07e452807..db8d0738323ff 100644
--- a/docs/user/production-considerations/production.asciidoc
+++ b/docs/user/production-considerations/production.asciidoc
@@ -32,12 +32,21 @@ server.name
Settings unique across each host (for example, running multiple installations on the same virtual machine):
[source,js]
--------
-logging.dest
path.data
pid.file
server.port
--------
+When using a file appender, the target file must also be unique:
+[source,yaml]
+--------
+logging:
+ appenders:
+ default:
+ type: file
+ fileName: /unique/path/per/instance
+--------
+
Settings that must be the same:
[source,js]
--------
diff --git a/package.json b/package.json
index f04a8423196fd..37a5239cf068a 100644
--- a/package.json
+++ b/package.json
@@ -101,7 +101,6 @@
"@elastic/ems-client": "7.15.0",
"@elastic/eui": "38.0.1",
"@elastic/filesaver": "1.1.2",
- "@elastic/good": "^9.0.1-kibana3",
"@elastic/maki": "6.3.0",
"@elastic/node-crypto": "1.2.1",
"@elastic/numeral": "^2.5.1",
@@ -113,12 +112,10 @@
"@hapi/accept": "^5.0.2",
"@hapi/boom": "^9.1.4",
"@hapi/cookie": "^11.0.2",
- "@hapi/good-squeeze": "6.0.0",
"@hapi/h2o2": "^9.1.0",
"@hapi/hapi": "^20.2.0",
"@hapi/hoek": "^9.2.0",
"@hapi/inert": "^6.0.4",
- "@hapi/podium": "^4.1.3",
"@hapi/wreck": "^17.1.0",
"@kbn/ace": "link:bazel-bin/packages/kbn-ace",
"@kbn/alerts": "link:bazel-bin/packages/kbn-alerts",
@@ -133,7 +130,6 @@
"@kbn/i18n": "link:bazel-bin/packages/kbn-i18n",
"@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter",
"@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils",
- "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging",
"@kbn/logging": "link:bazel-bin/packages/kbn-logging",
"@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl",
"@kbn/monaco": "link:bazel-bin/packages/kbn-monaco",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index 36bdee5303cb7..75c8d700e2843 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -29,7 +29,6 @@ filegroup(
"//packages/kbn-i18n:build",
"//packages/kbn-interpreter:build",
"//packages/kbn-io-ts-utils:build",
- "//packages/kbn-legacy-logging:build",
"//packages/kbn-logging:build",
"//packages/kbn-mapbox-gl:build",
"//packages/kbn-monaco:build",
diff --git a/packages/kbn-cli-dev-mode/src/bootstrap.ts b/packages/kbn-cli-dev-mode/src/bootstrap.ts
index 86a276c64f1f5..0428051b77e31 100644
--- a/packages/kbn-cli-dev-mode/src/bootstrap.ts
+++ b/packages/kbn-cli-dev-mode/src/bootstrap.ts
@@ -20,7 +20,7 @@ interface BootstrapArgs {
}
export async function bootstrapDevMode({ configs, cliArgs, applyConfigOverrides }: BootstrapArgs) {
- const log = new CliLog(!!cliArgs.quiet, !!cliArgs.silent);
+ const log = new CliLog(!!cliArgs.silent);
const env = Env.createDefault(REPO_ROOT, {
configs,
diff --git a/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts
index 8937eadfa4ee3..e5e009e51e69e 100644
--- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts
+++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts
@@ -74,7 +74,6 @@ const createCliArgs = (parts: Partial = {}): SomeCliArgs => ({
runExamples: false,
watch: true,
silent: false,
- quiet: false,
...parts,
});
diff --git a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts
index 28f38592ff3c4..2396b316aa3a2 100644
--- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts
+++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts
@@ -48,7 +48,6 @@ const GRACEFUL_TIMEOUT = 30000;
export type SomeCliArgs = Pick<
CliArgs,
- | 'quiet'
| 'silent'
| 'verbose'
| 'disableOptimizer'
@@ -108,7 +107,7 @@ export class CliDevMode {
private subscription?: Rx.Subscription;
constructor({ cliArgs, config, log }: { cliArgs: SomeCliArgs; config: CliDevConfig; log?: Log }) {
- this.log = log || new CliLog(!!cliArgs.quiet, !!cliArgs.silent);
+ this.log = log || new CliLog(!!cliArgs.silent);
if (cliArgs.basePath) {
this.basePathProxy = new BasePathProxyServer(this.log, config.http, config.dev);
@@ -163,7 +162,7 @@ export class CliDevMode {
runExamples: cliArgs.runExamples,
cache: cliArgs.cache,
dist: cliArgs.dist,
- quiet: !!cliArgs.quiet,
+ quiet: false,
silent: !!cliArgs.silent,
verbose: !!cliArgs.verbose,
watch: cliArgs.watch,
diff --git a/packages/kbn-cli-dev-mode/src/dev_server.test.ts b/packages/kbn-cli-dev-mode/src/dev_server.test.ts
index 9962a9a285a42..92dbe484eb005 100644
--- a/packages/kbn-cli-dev-mode/src/dev_server.test.ts
+++ b/packages/kbn-cli-dev-mode/src/dev_server.test.ts
@@ -130,7 +130,6 @@ describe('#run$', () => {
Array [
"foo",
"bar",
- "--logging.json=false",
],
Object {
"env": Object {
diff --git a/packages/kbn-cli-dev-mode/src/log.ts b/packages/kbn-cli-dev-mode/src/log.ts
index 86956abec202a..2cbd02b94a844 100644
--- a/packages/kbn-cli-dev-mode/src/log.ts
+++ b/packages/kbn-cli-dev-mode/src/log.ts
@@ -21,7 +21,7 @@ export interface Log {
export class CliLog implements Log {
public toolingLog = new ToolingLog({
- level: this.silent ? 'silent' : this.quiet ? 'error' : 'info',
+ level: this.silent ? 'silent' : 'info',
writeTo: {
write: (msg) => {
this.write(msg);
@@ -29,10 +29,10 @@ export class CliLog implements Log {
},
});
- constructor(private readonly quiet: boolean, private readonly silent: boolean) {}
+ constructor(private readonly silent: boolean) {}
good(label: string, ...args: any[]) {
- if (this.quiet || this.silent) {
+ if (this.silent) {
return;
}
@@ -41,7 +41,7 @@ export class CliLog implements Log {
}
warn(label: string, ...args: any[]) {
- if (this.quiet || this.silent) {
+ if (this.silent) {
return;
}
diff --git a/packages/kbn-cli-dev-mode/src/using_server_process.ts b/packages/kbn-cli-dev-mode/src/using_server_process.ts
index 0d0227c63adc2..eb997295035d8 100644
--- a/packages/kbn-cli-dev-mode/src/using_server_process.ts
+++ b/packages/kbn-cli-dev-mode/src/using_server_process.ts
@@ -25,7 +25,7 @@ export function usingServerProcess(
) {
return Rx.using(
(): ProcResource => {
- const proc = execa.node(script, [...argv, '--logging.json=false'], {
+ const proc = execa.node(script, argv, {
stdio: 'pipe',
nodeOptions: [
...process.execArgv,
diff --git a/packages/kbn-config/src/__mocks__/env.ts b/packages/kbn-config/src/__mocks__/env.ts
index 6f05f8f1f5a45..124a798501a96 100644
--- a/packages/kbn-config/src/__mocks__/env.ts
+++ b/packages/kbn-config/src/__mocks__/env.ts
@@ -19,7 +19,6 @@ export function getEnvOptions(options: DeepPartial = {}): EnvOptions
configs: options.configs || [],
cliArgs: {
dev: true,
- quiet: false,
silent: false,
watch: false,
basePath: false,
diff --git a/packages/kbn-config/src/__snapshots__/env.test.ts.snap b/packages/kbn-config/src/__snapshots__/env.test.ts.snap
index 570ed948774cc..a8e2eb62dbedb 100644
--- a/packages/kbn-config/src/__snapshots__/env.test.ts.snap
+++ b/packages/kbn-config/src/__snapshots__/env.test.ts.snap
@@ -11,7 +11,6 @@ Env {
"dist": false,
"envName": "development",
"oss": false,
- "quiet": false,
"runExamples": false,
"silent": false,
"watch": false,
@@ -54,7 +53,6 @@ Env {
"dist": false,
"envName": "production",
"oss": false,
- "quiet": false,
"runExamples": false,
"silent": false,
"watch": false,
@@ -96,7 +94,6 @@ Env {
"disableOptimizer": true,
"dist": false,
"oss": false,
- "quiet": false,
"runExamples": false,
"silent": false,
"watch": false,
@@ -138,7 +135,6 @@ Env {
"disableOptimizer": true,
"dist": false,
"oss": false,
- "quiet": false,
"runExamples": false,
"silent": false,
"watch": false,
@@ -180,7 +176,6 @@ Env {
"disableOptimizer": true,
"dist": false,
"oss": false,
- "quiet": false,
"runExamples": false,
"silent": false,
"watch": false,
@@ -222,7 +217,6 @@ Env {
"disableOptimizer": true,
"dist": false,
"oss": false,
- "quiet": false,
"runExamples": false,
"silent": false,
"watch": false,
diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts
index 5883ce8ab513c..72725fad39610 100644
--- a/packages/kbn-config/src/config_service.ts
+++ b/packages/kbn-config/src/config_service.ts
@@ -24,7 +24,7 @@ import {
DeprecatedConfigDetails,
ChangedDeprecatedPaths,
} from './deprecation';
-import { LegacyObjectToConfigAdapter } from './legacy';
+import { ObjectToConfigAdapter } from './object_to_config_adapter';
/** @internal */
export type IConfigService = PublicMethodsOf;
@@ -71,7 +71,7 @@ export class ConfigService {
map(([rawConfig, deprecations]) => {
const migrated = applyDeprecations(rawConfig, deprecations);
this.deprecatedConfigPaths.next(migrated.changedPaths);
- return new LegacyObjectToConfigAdapter(migrated.config);
+ return new ObjectToConfigAdapter(migrated.config);
}),
tap((config) => {
this.lastConfig = config;
diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts
index 053bb93ce158c..73f32606c463f 100644
--- a/packages/kbn-config/src/env.ts
+++ b/packages/kbn-config/src/env.ts
@@ -21,8 +21,6 @@ export interface EnvOptions {
export interface CliArgs {
dev: boolean;
envName?: string;
- /** @deprecated */
- quiet?: boolean;
silent?: boolean;
verbose?: boolean;
watch: boolean;
diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts
index 08cf12343f459..89f70ab9b6984 100644
--- a/packages/kbn-config/src/index.ts
+++ b/packages/kbn-config/src/index.ts
@@ -30,5 +30,4 @@ export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './c
export { ObjectToConfigAdapter } from './object_to_config_adapter';
export { CliArgs, Env, RawPackageInfo } from './env';
export { EnvironmentMode, PackageInfo } from './types';
-export { LegacyObjectToConfigAdapter, LegacyLoggingConfig } from './legacy';
export { getPluginSearchPaths } from './plugins';
diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
deleted file mode 100644
index 17ac75e9f3d9e..0000000000000
--- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
+++ /dev/null
@@ -1,95 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`#get correctly handles silent logging config. 1`] = `
-Object {
- "appenders": Object {
- "default": Object {
- "legacyLoggingConfig": Object {
- "silent": true,
- },
- "type": "legacy-appender",
- },
- },
- "loggers": undefined,
- "root": Object {
- "level": "off",
- },
- "silent": true,
-}
-`;
-
-exports[`#get correctly handles verbose file logging config with json format. 1`] = `
-Object {
- "appenders": Object {
- "default": Object {
- "legacyLoggingConfig": Object {
- "dest": "/some/path.log",
- "json": true,
- "verbose": true,
- },
- "type": "legacy-appender",
- },
- },
- "dest": "/some/path.log",
- "json": true,
- "loggers": undefined,
- "root": Object {
- "level": "all",
- },
- "verbose": true,
-}
-`;
-
-exports[`#getFlattenedPaths returns all paths of the underlying object. 1`] = `
-Array [
- "known",
- "knownContainer.sub1",
- "knownContainer.sub2",
- "legacy.known",
-]
-`;
-
-exports[`#set correctly sets values for existing paths. 1`] = `
-Object {
- "known": "value",
- "knownContainer": Object {
- "sub1": "sub-value-1",
- "sub2": "sub-value-2",
- },
-}
-`;
-
-exports[`#set correctly sets values for paths that do not exist. 1`] = `
-Object {
- "unknown": Object {
- "sub1": "sub-value-1",
- "sub2": "sub-value-2",
- },
-}
-`;
-
-exports[`#toRaw returns a deep copy of the underlying raw config object. 1`] = `
-Object {
- "known": "foo",
- "knownContainer": Object {
- "sub1": "bar",
- "sub2": "baz",
- },
- "legacy": Object {
- "known": "baz",
- },
-}
-`;
-
-exports[`#toRaw returns a deep copy of the underlying raw config object. 2`] = `
-Object {
- "known": "bar",
- "knownContainer": Object {
- "sub1": "baz",
- "sub2": "baz",
- },
- "legacy": Object {
- "known": "baz",
- },
-}
-`;
diff --git a/packages/kbn-config/src/legacy/index.ts b/packages/kbn-config/src/legacy/index.ts
deleted file mode 100644
index f6906f81d1821..0000000000000
--- a/packages/kbn-config/src/legacy/index.ts
+++ /dev/null
@@ -1,12 +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.
- */
-
-export {
- LegacyObjectToConfigAdapter,
- LegacyLoggingConfig,
-} from './legacy_object_to_config_adapter';
diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts
deleted file mode 100644
index 47151503e1634..0000000000000
--- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts
+++ /dev/null
@@ -1,161 +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 { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter';
-
-describe('#get', () => {
- test('correctly handles paths that do not exist.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({});
-
- expect(configAdapter.get('one')).not.toBeDefined();
- expect(configAdapter.get(['one', 'two'])).not.toBeDefined();
- expect(configAdapter.get(['one.three'])).not.toBeDefined();
- });
-
- test('correctly handles paths that do not need to be transformed.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({
- one: 'value-one',
- two: {
- sub: 'value-two-sub',
- },
- container: {
- value: 'some',
- },
- });
-
- expect(configAdapter.get('one')).toEqual('value-one');
- expect(configAdapter.get(['two', 'sub'])).toEqual('value-two-sub');
- expect(configAdapter.get('two.sub')).toEqual('value-two-sub');
- expect(configAdapter.get('container')).toEqual({ value: 'some' });
- });
-
- test('correctly handles csp config.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({
- csp: {
- rules: ['strict'],
- },
- });
-
- expect(configAdapter.get('csp')).toMatchInlineSnapshot(`
- Object {
- "rules": Array [
- "strict",
- ],
- }
- `);
- });
-
- test('correctly handles silent logging config.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({
- logging: { silent: true },
- });
-
- expect(configAdapter.get('logging')).toMatchSnapshot();
- });
-
- test('correctly handles verbose file logging config with json format.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({
- logging: { verbose: true, json: true, dest: '/some/path.log' },
- });
-
- expect(configAdapter.get('logging')).toMatchSnapshot();
- });
-});
-
-describe('#set', () => {
- test('correctly sets values for paths that do not exist.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({});
-
- configAdapter.set('unknown', 'value');
- configAdapter.set(['unknown', 'sub1'], 'sub-value-1');
- configAdapter.set('unknown.sub2', 'sub-value-2');
-
- expect(configAdapter.toRaw()).toMatchSnapshot();
- });
-
- test('correctly sets values for existing paths.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({
- known: '',
- knownContainer: {
- sub1: 'sub-1',
- sub2: 'sub-2',
- },
- });
-
- configAdapter.set('known', 'value');
- configAdapter.set(['knownContainer', 'sub1'], 'sub-value-1');
- configAdapter.set('knownContainer.sub2', 'sub-value-2');
-
- expect(configAdapter.toRaw()).toMatchSnapshot();
- });
-});
-
-describe('#has', () => {
- test('returns false if config is not set', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({});
-
- expect(configAdapter.has('unknown')).toBe(false);
- expect(configAdapter.has(['unknown', 'sub1'])).toBe(false);
- expect(configAdapter.has('unknown.sub2')).toBe(false);
- });
-
- test('returns true if config is set.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({
- known: 'foo',
- knownContainer: {
- sub1: 'bar',
- sub2: 'baz',
- },
- });
-
- expect(configAdapter.has('known')).toBe(true);
- expect(configAdapter.has(['knownContainer', 'sub1'])).toBe(true);
- expect(configAdapter.has('knownContainer.sub2')).toBe(true);
- });
-});
-
-describe('#toRaw', () => {
- test('returns a deep copy of the underlying raw config object.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({
- known: 'foo',
- knownContainer: {
- sub1: 'bar',
- sub2: 'baz',
- },
- legacy: { known: 'baz' },
- });
-
- const firstRawCopy = configAdapter.toRaw();
-
- configAdapter.set('known', 'bar');
- configAdapter.set(['knownContainer', 'sub1'], 'baz');
-
- const secondRawCopy = configAdapter.toRaw();
-
- expect(firstRawCopy).not.toBe(secondRawCopy);
- expect(firstRawCopy.knownContainer).not.toBe(secondRawCopy.knownContainer);
-
- expect(firstRawCopy).toMatchSnapshot();
- expect(secondRawCopy).toMatchSnapshot();
- });
-});
-
-describe('#getFlattenedPaths', () => {
- test('returns all paths of the underlying object.', () => {
- const configAdapter = new LegacyObjectToConfigAdapter({
- known: 'foo',
- knownContainer: {
- sub1: 'bar',
- sub2: 'baz',
- },
- legacy: { known: 'baz' },
- });
-
- expect(configAdapter.getFlattenedPaths()).toMatchSnapshot();
- });
-});
diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts
deleted file mode 100644
index bc6fd49e2498a..0000000000000
--- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts
+++ /dev/null
@@ -1,65 +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 { ConfigPath } from '../config';
-import { ObjectToConfigAdapter } from '../object_to_config_adapter';
-
-/**
- * Represents logging config supported by the legacy platform.
- */
-export interface LegacyLoggingConfig {
- silent?: boolean;
- verbose?: boolean;
- quiet?: boolean;
- dest?: string;
- json?: boolean;
- events?: Record;
-}
-
-type MixedLoggingConfig = LegacyLoggingConfig & Record;
-
-/**
- * Represents adapter between config provided by legacy platform and `Config`
- * supported by the current platform.
- * @internal
- */
-export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
- private static transformLogging(configValue: MixedLoggingConfig = {}) {
- const { appenders, root, loggers, ...legacyLoggingConfig } = configValue;
-
- const loggingConfig = {
- appenders: {
- ...appenders,
- default: { type: 'legacy-appender', legacyLoggingConfig },
- },
- root: { level: 'info', ...root },
- loggers,
- ...legacyLoggingConfig,
- };
-
- if (configValue.silent) {
- loggingConfig.root.level = 'off';
- } else if (configValue.quiet) {
- loggingConfig.root.level = 'error';
- } else if (configValue.verbose) {
- loggingConfig.root.level = 'all';
- }
-
- return loggingConfig;
- }
-
- public get(configPath: ConfigPath) {
- const configValue = super.get(configPath);
- switch (configPath) {
- case 'logging':
- return LegacyObjectToConfigAdapter.transformLogging(configValue as LegacyLoggingConfig);
- default:
- return configValue;
- }
- }
-}
diff --git a/packages/kbn-legacy-logging/BUILD.bazel b/packages/kbn-legacy-logging/BUILD.bazel
deleted file mode 100644
index c4927fe076e15..0000000000000
--- a/packages/kbn-legacy-logging/BUILD.bazel
+++ /dev/null
@@ -1,107 +0,0 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
-load("//src/dev/bazel:index.bzl", "jsts_transpiler")
-
-PKG_BASE_NAME = "kbn-legacy-logging"
-PKG_REQUIRE_NAME = "@kbn/legacy-logging"
-
-SOURCE_FILES = glob(
- [
- "src/**/*.ts",
- ],
- exclude = ["**/*.test.*"],
-)
-
-SRCS = SOURCE_FILES
-
-filegroup(
- name = "srcs",
- srcs = SRCS,
-)
-
-NPM_MODULE_EXTRA_FILES = [
- "package.json",
- "README.md"
-]
-
-RUNTIME_DEPS = [
- "//packages/kbn-config-schema",
- "//packages/kbn-utils",
- "@npm//@elastic/numeral",
- "@npm//@hapi/hapi",
- "@npm//@hapi/podium",
- "@npm//chokidar",
- "@npm//lodash",
- "@npm//moment-timezone",
- "@npm//query-string",
- "@npm//rxjs",
- "@npm//tslib",
-]
-
-TYPES_DEPS = [
- "//packages/kbn-config-schema",
- "//packages/kbn-utils",
- "@npm//@elastic/numeral",
- "@npm//@hapi/podium",
- "@npm//chokidar",
- "@npm//query-string",
- "@npm//rxjs",
- "@npm//tslib",
- "@npm//@types/hapi__hapi",
- "@npm//@types/jest",
- "@npm//@types/lodash",
- "@npm//@types/moment-timezone",
- "@npm//@types/node",
-]
-
-jsts_transpiler(
- name = "target_node",
- srcs = SRCS,
- build_pkg_name = package_name(),
-)
-
-ts_config(
- name = "tsconfig",
- src = "tsconfig.json",
- deps = [
- "//:tsconfig.base.json",
- "//:tsconfig.bazel.json",
- ],
-)
-
-ts_project(
- name = "tsc_types",
- args = ['--pretty'],
- srcs = SRCS,
- deps = TYPES_DEPS,
- declaration = True,
- declaration_map = True,
- emit_declaration_only = True,
- out_dir = "target_types",
- source_map = True,
- root_dir = "src",
- tsconfig = ":tsconfig",
-)
-
-js_library(
- name = PKG_BASE_NAME,
- srcs = NPM_MODULE_EXTRA_FILES,
- deps = RUNTIME_DEPS + [":target_node", ":tsc_types"],
- package_name = PKG_REQUIRE_NAME,
- visibility = ["//visibility:public"],
-)
-
-pkg_npm(
- name = "npm_module",
- deps = [
- ":%s" % PKG_BASE_NAME,
- ]
-)
-
-filegroup(
- name = "build",
- srcs = [
- ":npm_module",
- ],
- visibility = ["//visibility:public"],
-)
diff --git a/packages/kbn-legacy-logging/README.md b/packages/kbn-legacy-logging/README.md
deleted file mode 100644
index 4c5989fc892dc..0000000000000
--- a/packages/kbn-legacy-logging/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# @kbn/legacy-logging
-
-This package contains the implementation of the legacy logging
-system, based on `@hapi/good`
\ No newline at end of file
diff --git a/packages/kbn-legacy-logging/jest.config.js b/packages/kbn-legacy-logging/jest.config.js
deleted file mode 100644
index d00b1c56dae81..0000000000000
--- a/packages/kbn-legacy-logging/jest.config.js
+++ /dev/null
@@ -1,13 +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.
- */
-
-module.exports = {
- preset: '@kbn/test',
- rootDir: '../..',
- roots: ['/packages/kbn-legacy-logging'],
-};
diff --git a/packages/kbn-legacy-logging/package.json b/packages/kbn-legacy-logging/package.json
deleted file mode 100644
index 6e846ffc5bfaf..0000000000000
--- a/packages/kbn-legacy-logging/package.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "name": "@kbn/legacy-logging",
- "version": "1.0.0",
- "private": true,
- "license": "SSPL-1.0 OR Elastic License 2.0",
- "main": "./target_node/index.js",
- "types": "./target_types/index.d.ts"
-}
diff --git a/packages/kbn-legacy-logging/src/get_logging_config.ts b/packages/kbn-legacy-logging/src/get_logging_config.ts
deleted file mode 100644
index f74bc5904e24b..0000000000000
--- a/packages/kbn-legacy-logging/src/get_logging_config.ts
+++ /dev/null
@@ -1,85 +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 _ from 'lodash';
-import { getLogReporter } from './log_reporter';
-import { LegacyLoggingConfig } from './schema';
-
-/**
- * Returns the `@hapi/good` plugin configuration to be used for the legacy logging
- * @param config
- */
-export function getLoggingConfiguration(config: LegacyLoggingConfig, opsInterval: number) {
- const events = config.events;
-
- if (config.silent) {
- _.defaults(events, {});
- } else if (config.quiet) {
- _.defaults(events, {
- log: ['listening', 'error', 'fatal'],
- request: ['error'],
- error: '*',
- });
- } else if (config.verbose) {
- _.defaults(events, {
- error: '*',
- log: '*',
- // To avoid duplicate logs, we explicitly disable these in verbose
- // mode as they are already provided by the new logging config under
- // the `http.server.response` and `metrics.ops` contexts.
- ops: '!',
- request: '!',
- response: '!',
- });
- } else {
- _.defaults(events, {
- log: ['info', 'warning', 'error', 'fatal'],
- request: ['info', 'warning', 'error', 'fatal'],
- error: '*',
- });
- }
-
- const loggerStream = getLogReporter({
- config: {
- json: config.json,
- dest: config.dest,
- timezone: config.timezone,
-
- // I'm adding the default here because if you add another filter
- // using the commandline it will remove authorization. I want users
- // to have to explicitly set --logging.filter.authorization=none or
- // --logging.filter.cookie=none to have it show up in the logs.
- filter: _.defaults(config.filter, {
- authorization: 'remove',
- cookie: 'remove',
- }),
- },
- events: _.transform(
- events,
- function (filtered: Record, val: string, key: string) {
- // provide a string compatible way to remove events
- if (val !== '!') filtered[key] = val;
- },
- {}
- ),
- });
-
- const options = {
- ops: {
- interval: opsInterval,
- },
- includes: {
- request: ['headers', 'payload'],
- response: ['headers', 'payload'],
- },
- reporters: {
- logReporter: [loggerStream],
- },
- };
- return options;
-}
diff --git a/packages/kbn-legacy-logging/src/index.ts b/packages/kbn-legacy-logging/src/index.ts
deleted file mode 100644
index 670df4e95f337..0000000000000
--- a/packages/kbn-legacy-logging/src/index.ts
+++ /dev/null
@@ -1,14 +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.
- */
-
-export { LegacyLoggingConfig, legacyLoggingConfigSchema } from './schema';
-export { attachMetaData } from './metadata';
-export { setupLoggingRotate } from './rotate';
-export { setupLogging, reconfigureLogging } from './setup_logging';
-export { getLoggingConfiguration } from './get_logging_config';
-export { LegacyLoggingServer } from './legacy_logging_server';
diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.test.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.test.ts
deleted file mode 100644
index 40019fc90ff42..0000000000000
--- a/packages/kbn-legacy-logging/src/legacy_logging_server.test.ts
+++ /dev/null
@@ -1,105 +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.
- */
-
-jest.mock('./setup_logging');
-
-import { LegacyLoggingServer, LogRecord } from './legacy_logging_server';
-
-test('correctly forwards log records.', () => {
- const loggingServer = new LegacyLoggingServer({ events: {} });
- const onLogMock = jest.fn();
- loggingServer.events.on('log', onLogMock);
-
- const timestamp = 1554433221100;
- const firstLogRecord: LogRecord = {
- timestamp: new Date(timestamp),
- pid: 5355,
- level: {
- id: 'info',
- value: 5,
- },
- context: 'some-context',
- message: 'some-message',
- };
-
- const secondLogRecord: LogRecord = {
- timestamp: new Date(timestamp),
- pid: 5355,
- level: {
- id: 'error',
- value: 3,
- },
- context: 'some-context.sub-context',
- message: 'some-message',
- meta: { unknown: 2 },
- error: new Error('some-error'),
- };
-
- const thirdLogRecord: LogRecord = {
- timestamp: new Date(timestamp),
- pid: 5355,
- level: {
- id: 'trace',
- value: 7,
- },
- context: 'some-context.sub-context',
- message: 'some-message',
- meta: { tags: ['important', 'tags'], unknown: 2 },
- };
-
- loggingServer.log(firstLogRecord);
- loggingServer.log(secondLogRecord);
- loggingServer.log(thirdLogRecord);
-
- expect(onLogMock).toHaveBeenCalledTimes(3);
-
- const [[firstCall], [secondCall], [thirdCall]] = onLogMock.mock.calls;
- expect(firstCall).toMatchInlineSnapshot(`
-Object {
- "data": "some-message",
- "tags": Array [
- "info",
- "some-context",
- ],
- "timestamp": 1554433221100,
-}
-`);
-
- expect(secondCall).toMatchInlineSnapshot(`
-Object {
- "data": [Error: some-error],
- "tags": Array [
- "error",
- "some-context",
- "sub-context",
- ],
- "timestamp": 1554433221100,
-}
-`);
-
- expect(thirdCall).toMatchInlineSnapshot(`
-Object {
- "data": Object {
- Symbol(log message with metadata): Object {
- "message": "some-message",
- "metadata": Object {
- "unknown": 2,
- },
- },
- },
- "tags": Array [
- "debug",
- "some-context",
- "sub-context",
- "important",
- "tags",
- ],
- "timestamp": 1554433221100,
-}
-`);
-});
diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts
deleted file mode 100644
index f6c42dd1b161f..0000000000000
--- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts
+++ /dev/null
@@ -1,140 +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 { ServerExtType, Server } from '@hapi/hapi';
-import Podium from '@hapi/podium';
-import { setupLogging } from './setup_logging';
-import { attachMetaData } from './metadata';
-import { legacyLoggingConfigSchema } from './schema';
-
-// these LogXXX types are duplicated to avoid a cross dependency with the @kbn/logging package.
-// typescript will error if they diverge at some point.
-type LogLevelId = 'all' | 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'off';
-
-interface LogLevel {
- id: LogLevelId;
- value: number;
-}
-
-export interface LogRecord {
- timestamp: Date;
- level: LogLevel;
- context: string;
- message: string;
- error?: Error;
- meta?: { [name: string]: any };
- pid: number;
-}
-
-const isEmptyObject = (obj: object) => Object.keys(obj).length === 0;
-
-function getDataToLog(error: Error | undefined, metadata: object, message: string) {
- if (error) {
- return error;
- }
- if (!isEmptyObject(metadata)) {
- return attachMetaData(message, metadata);
- }
- return message;
-}
-
-interface PluginRegisterParams {
- plugin: {
- register: (
- server: LegacyLoggingServer,
- options: PluginRegisterParams['options']
- ) => Promise;
- };
- options: Record;
-}
-
-/**
- * Converts core log level to a one that's known to the legacy platform.
- * @param level Log level from the core.
- */
-function getLegacyLogLevel(level: LogLevel) {
- const logLevel = level.id.toLowerCase();
- if (logLevel === 'warn') {
- return 'warning';
- }
-
- if (logLevel === 'trace') {
- return 'debug';
- }
-
- return logLevel;
-}
-
-/**
- * The "legacy" Kibana uses Hapi server + even-better plugin to log, so we should
- * use the same approach here to make log records generated by the core to look the
- * same as the rest of the records generated by the "legacy" Kibana. But to reduce
- * overhead of having full blown Hapi server instance we create our own "light" version.
- * @internal
- */
-export class LegacyLoggingServer {
- public connections = [];
- // Emulates Hapi's usage of the podium event bus.
- public events: Podium = new Podium(['log', 'request', 'response']);
-
- private onPostStopCallback?: () => void;
-
- constructor(legacyLoggingConfig: any) {
- // We set `ops.interval` to max allowed number and `ops` filter to value
- // that doesn't exist to avoid logging of ops at all, if turned on it will be
- // logged by the "legacy" Kibana.
- const loggingConfig = legacyLoggingConfigSchema.validate({
- ...legacyLoggingConfig,
- events: {
- ...legacyLoggingConfig.events,
- ops: '__no-ops__',
- },
- });
-
- setupLogging(this as unknown as Server, loggingConfig, 2147483647);
- }
-
- public register({ plugin: { register }, options }: PluginRegisterParams): Promise {
- return register(this, options);
- }
-
- public log({ level, context, message, error, timestamp, meta = {} }: LogRecord) {
- const { tags = [], ...metadata } = meta;
-
- this.events
- .emit('log', {
- data: getDataToLog(error, metadata, message),
- tags: [getLegacyLogLevel(level), ...context.split('.'), ...tags],
- timestamp: timestamp.getTime(),
- })
- .catch((err) => {
- // eslint-disable-next-line no-console
- console.error('An unexpected error occurred while writing to the log:', err.stack);
- process.exit(1);
- });
- }
-
- public stop() {
- // Tell the plugin we're stopping.
- if (this.onPostStopCallback !== undefined) {
- this.onPostStopCallback();
- }
- }
-
- public ext(eventName: ServerExtType, callback: () => void) {
- // method is called by plugin that's being registered.
- if (eventName === 'onPostStop') {
- this.onPostStopCallback = callback;
- }
- // We don't care about any others the plugin registers
- }
-
- public expose() {
- // method is called by plugin that's being registered.
- }
-}
diff --git a/packages/kbn-legacy-logging/src/log_events.ts b/packages/kbn-legacy-logging/src/log_events.ts
deleted file mode 100644
index 193bfbea42ace..0000000000000
--- a/packages/kbn-legacy-logging/src/log_events.ts
+++ /dev/null
@@ -1,71 +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 type { ResponseObject } from '@hapi/hapi';
-import { EventData, isEventData } from './metadata';
-
-export interface BaseEvent {
- event: string;
- timestamp: number;
- pid: number;
- tags?: string[];
-}
-
-export interface ResponseEvent extends BaseEvent {
- event: 'response';
- method: 'GET' | 'POST' | 'PUT' | 'DELETE';
- statusCode: number;
- path: string;
- headers: Record;
- responseHeaders: Record;
- responsePayload: ResponseObject['source'];
- responseTime: string;
- query: Record;
-}
-
-export interface OpsEvent extends BaseEvent {
- event: 'ops';
- os: {
- load: string[];
- };
- proc: Record;
- load: string;
-}
-
-export interface ErrorEvent extends BaseEvent {
- event: 'error';
- error: Error;
- url: string;
-}
-
-export interface UndeclaredErrorEvent extends BaseEvent {
- error: Error;
-}
-
-export interface LogEvent extends BaseEvent {
- data: EventData;
-}
-
-export interface UnkownEvent extends BaseEvent {
- data: string | Record;
-}
-
-export type AnyEvent =
- | ResponseEvent
- | OpsEvent
- | ErrorEvent
- | UndeclaredErrorEvent
- | LogEvent
- | UnkownEvent;
-
-export const isResponseEvent = (e: AnyEvent): e is ResponseEvent => e.event === 'response';
-export const isOpsEvent = (e: AnyEvent): e is OpsEvent => e.event === 'ops';
-export const isErrorEvent = (e: AnyEvent): e is ErrorEvent => e.event === 'error';
-export const isLogEvent = (e: AnyEvent): e is LogEvent => isEventData((e as LogEvent).data);
-export const isUndeclaredErrorEvent = (e: AnyEvent): e is UndeclaredErrorEvent =>
- (e as any).error instanceof Error;
diff --git a/packages/kbn-legacy-logging/src/log_format.ts b/packages/kbn-legacy-logging/src/log_format.ts
deleted file mode 100644
index a0eaf023dff19..0000000000000
--- a/packages/kbn-legacy-logging/src/log_format.ts
+++ /dev/null
@@ -1,176 +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 Stream from 'stream';
-import moment from 'moment-timezone';
-import _ from 'lodash';
-import queryString from 'query-string';
-import numeral from '@elastic/numeral';
-import chalk from 'chalk';
-import { inspect } from 'util';
-
-import { applyFiltersToKeys, getResponsePayloadBytes } from './utils';
-import { getLogEventData } from './metadata';
-import { LegacyLoggingConfig } from './schema';
-import {
- AnyEvent,
- ResponseEvent,
- isResponseEvent,
- isOpsEvent,
- isErrorEvent,
- isLogEvent,
- isUndeclaredErrorEvent,
-} from './log_events';
-
-export type LogFormatConfig = Pick;
-
-function serializeError(err: any = {}) {
- return {
- message: err.message,
- name: err.name,
- stack: err.stack,
- code: err.code,
- signal: err.signal,
- };
-}
-
-const levelColor = function (code: number) {
- if (code < 299) return chalk.green(String(code));
- if (code < 399) return chalk.yellow(String(code));
- if (code < 499) return chalk.magentaBright(String(code));
- return chalk.red(String(code));
-};
-
-export abstract class BaseLogFormat extends Stream.Transform {
- constructor(private readonly config: LogFormatConfig) {
- super({
- readableObjectMode: false,
- writableObjectMode: true,
- });
- }
-
- abstract format(data: Record): string;
-
- filter(data: Record) {
- if (!this.config.filter) {
- return data;
- }
- return applyFiltersToKeys(data, this.config.filter);
- }
-
- _transform(event: AnyEvent, enc: string, next: Stream.TransformCallback) {
- const data = this.filter(this.readEvent(event));
- this.push(this.format(data) + '\n');
- next();
- }
-
- getContentLength({ responsePayload, responseHeaders }: ResponseEvent): number | undefined {
- try {
- return getResponsePayloadBytes(responsePayload, responseHeaders);
- } catch (e) {
- // We intentionally swallow any errors as this information is
- // only a nicety for logging purposes, and should not cause the
- // server to crash if it cannot be determined.
- this.push(
- this.format({
- type: 'log',
- tags: ['warning', 'logging'],
- message: `Failed to calculate response payload bytes. [${e}]`,
- }) + '\n'
- );
- }
- }
-
- extractAndFormatTimestamp(data: Record, format?: string) {
- const { timezone } = this.config;
- const date = moment(data['@timestamp']);
- if (timezone) {
- date.tz(timezone);
- }
- return date.format(format);
- }
-
- readEvent(event: AnyEvent) {
- const data: Record = {
- type: event.event,
- '@timestamp': event.timestamp,
- tags: [...(event.tags || [])],
- pid: event.pid,
- };
-
- if (isResponseEvent(event)) {
- _.defaults(data, _.pick(event, ['method', 'statusCode']));
-
- const source = _.get(event, 'source', {});
- data.req = {
- url: event.path,
- method: event.method || '',
- headers: event.headers,
- remoteAddress: source.remoteAddress,
- userAgent: source.userAgent,
- referer: source.referer,
- };
-
- data.res = {
- statusCode: event.statusCode,
- responseTime: event.responseTime,
- contentLength: this.getContentLength(event),
- };
-
- const query = queryString.stringify(event.query, { sort: false });
- if (query) {
- data.req.url += '?' + query;
- }
-
- data.message = data.req.method.toUpperCase() + ' ';
- data.message += data.req.url;
- data.message += ' ';
- data.message += levelColor(data.res.statusCode);
- data.message += ' ';
- data.message += chalk.gray(data.res.responseTime + 'ms');
- if (data.res.contentLength) {
- data.message += chalk.gray(' - ' + numeral(data.res.contentLength).format('0.0b'));
- }
- } else if (isOpsEvent(event)) {
- _.defaults(data, _.pick(event, ['pid', 'os', 'proc', 'load']));
- data.message = chalk.gray('memory: ');
- data.message += numeral(_.get(data, 'proc.mem.heapUsed')).format('0.0b');
- data.message += ' ';
- data.message += chalk.gray('uptime: ');
- data.message += numeral(_.get(data, 'proc.uptime')).format('00:00:00');
- data.message += ' ';
- data.message += chalk.gray('load: [');
- data.message += _.get(data, 'os.load', [])
- .map((val: number) => {
- return numeral(val).format('0.00');
- })
- .join(' ');
- data.message += chalk.gray(']');
- data.message += ' ';
- data.message += chalk.gray('delay: ');
- data.message += numeral(_.get(data, 'proc.delay')).format('0.000');
- } else if (isErrorEvent(event)) {
- data.level = 'error';
- data.error = serializeError(event.error);
- data.url = event.url;
- const message = _.get(event, 'error.message');
- data.message = message || 'Unknown error (no message)';
- } else if (isUndeclaredErrorEvent(event)) {
- data.type = 'error';
- data.level = _.includes(event.tags, 'fatal') ? 'fatal' : 'error';
- data.error = serializeError(event.error);
- const message = _.get(event, 'error.message');
- data.message = message || 'Unknown error object (no message)';
- } else if (isLogEvent(event)) {
- _.assign(data, getLogEventData(event.data));
- } else {
- data.message = _.isString(event.data) ? event.data : inspect(event.data);
- }
- return data;
- }
-}
diff --git a/packages/kbn-legacy-logging/src/log_format_json.test.ts b/packages/kbn-legacy-logging/src/log_format_json.test.ts
deleted file mode 100644
index 3255c5d17bb30..0000000000000
--- a/packages/kbn-legacy-logging/src/log_format_json.test.ts
+++ /dev/null
@@ -1,281 +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 moment from 'moment';
-
-import { attachMetaData } from './metadata';
-import { createListStream, createPromiseFromStreams } from '@kbn/utils';
-import { KbnLoggerJsonFormat } from './log_format_json';
-
-const time = +moment('2010-01-01T05:15:59Z', moment.ISO_8601);
-
-const makeEvent = (eventType: string) => ({
- event: eventType,
- timestamp: time,
-});
-
-describe('KbnLoggerJsonFormat', () => {
- const config: any = {};
-
- describe('event types and messages', () => {
- let format: KbnLoggerJsonFormat;
- beforeEach(() => {
- format = new KbnLoggerJsonFormat(config);
- });
-
- it('log', async () => {
- const result = await createPromiseFromStreams([
- createListStream([makeEvent('log')]),
- format,
- ]);
- const { type, message } = JSON.parse(result);
-
- expect(type).toBe('log');
- expect(message).toBe('undefined');
- });
-
- describe('response', () => {
- it('handles a response object', async () => {
- const event = {
- ...makeEvent('response'),
- statusCode: 200,
- contentLength: 800,
- responseTime: 12000,
- method: 'GET',
- path: '/path/to/resource',
- responsePayload: '1234567879890',
- source: {
- remoteAddress: '127.0.0.1',
- userAgent: 'Test Thing',
- referer: 'elastic.co',
- },
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { type, method, statusCode, message, req } = JSON.parse(result);
-
- expect(type).toBe('response');
- expect(method).toBe('GET');
- expect(statusCode).toBe(200);
- expect(message).toBe('GET /path/to/resource 200 12000ms - 13.0B');
- expect(req.remoteAddress).toBe('127.0.0.1');
- expect(req.userAgent).toBe('Test Thing');
- });
-
- it('leaves payload size empty if not available', async () => {
- const event = {
- ...makeEvent('response'),
- statusCode: 200,
- responseTime: 12000,
- method: 'GET',
- path: '/path/to/resource',
- responsePayload: null,
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- expect(JSON.parse(result).message).toBe('GET /path/to/resource 200 12000ms');
- });
- });
-
- it('ops', async () => {
- const event = {
- ...makeEvent('ops'),
- os: {
- load: [1, 1, 2],
- },
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { type, message } = JSON.parse(result);
-
- expect(type).toBe('ops');
- expect(message).toBe('memory: 0.0B uptime: 0:00:00 load: [1.00 1.00 2.00] delay: 0.000');
- });
-
- describe('with metadata', () => {
- it('logs an event with meta data', async () => {
- const event = {
- data: attachMetaData('message for event', {
- prop1: 'value1',
- prop2: 'value2',
- }),
- tags: ['tag1', 'tag2'],
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { level, message, prop1, prop2, tags } = JSON.parse(result);
-
- expect(level).toBe(undefined);
- expect(message).toBe('message for event');
- expect(prop1).toBe('value1');
- expect(prop2).toBe('value2');
- expect(tags).toEqual(['tag1', 'tag2']);
- });
-
- it('meta data rewrites event fields', async () => {
- const event = {
- data: attachMetaData('message for event', {
- tags: ['meta-data-tag'],
- prop1: 'value1',
- prop2: 'value2',
- }),
- tags: ['tag1', 'tag2'],
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { level, message, prop1, prop2, tags } = JSON.parse(result);
-
- expect(level).toBe(undefined);
- expect(message).toBe('message for event');
- expect(prop1).toBe('value1');
- expect(prop2).toBe('value2');
- expect(tags).toEqual(['meta-data-tag']);
- });
-
- it('logs an event with empty meta data', async () => {
- const event = {
- data: attachMetaData('message for event'),
- tags: ['tag1', 'tag2'],
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { level, message, prop1, prop2, tags } = JSON.parse(result);
-
- expect(level).toBe(undefined);
- expect(message).toBe('message for event');
- expect(prop1).toBe(undefined);
- expect(prop2).toBe(undefined);
- expect(tags).toEqual(['tag1', 'tag2']);
- });
-
- it('does not log meta data for an error event', async () => {
- const event = {
- error: new Error('reason'),
- data: attachMetaData('message for event', {
- prop1: 'value1',
- prop2: 'value2',
- }),
- tags: ['tag1', 'tag2'],
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { level, message, prop1, prop2, tags } = JSON.parse(result);
-
- expect(level).toBe('error');
- expect(message).toBe('reason');
- expect(prop1).toBe(undefined);
- expect(prop2).toBe(undefined);
- expect(tags).toEqual(['tag1', 'tag2']);
- });
- });
-
- describe('errors', () => {
- it('error type', async () => {
- const event = {
- ...makeEvent('error'),
- error: {
- message: 'test error 0',
- },
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { level, message, error } = JSON.parse(result);
-
- expect(level).toBe('error');
- expect(message).toBe('test error 0');
- expect(error).toEqual({ message: 'test error 0' });
- });
-
- it('with no message', async () => {
- const event = {
- event: 'error',
- error: {},
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { level, message, error } = JSON.parse(result);
-
- expect(level).toBe('error');
- expect(message).toBe('Unknown error (no message)');
- expect(error).toEqual({});
- });
-
- it('event error instanceof Error', async () => {
- const event = {
- error: new Error('test error 2') as any,
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { level, message, error } = JSON.parse(result);
-
- expect(level).toBe('error');
- expect(message).toBe('test error 2');
-
- expect(error.message).toBe(event.error.message);
- expect(error.name).toBe(event.error.name);
- expect(error.stack).toBe(event.error.stack);
- expect(error.code).toBe(event.error.code);
- expect(error.signal).toBe(event.error.signal);
- });
-
- it('event error instanceof Error - fatal', async () => {
- const event = {
- error: new Error('test error 2') as any,
- tags: ['fatal', 'tag2'],
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { tags, level, message, error } = JSON.parse(result);
-
- expect(tags).toEqual(['fatal', 'tag2']);
- expect(level).toBe('fatal');
- expect(message).toBe('test error 2');
-
- expect(error.message).toBe(event.error.message);
- expect(error.name).toBe(event.error.name);
- expect(error.stack).toBe(event.error.stack);
- expect(error.code).toBe(event.error.code);
- expect(error.signal).toBe(event.error.signal);
- });
-
- it('event error instanceof Error, no message', async () => {
- const event = {
- error: new Error('') as any,
- };
- const result = await createPromiseFromStreams([createListStream([event]), format]);
- const { level, message, error } = JSON.parse(result);
-
- expect(level).toBe('error');
- expect(message).toBe('Unknown error object (no message)');
-
- expect(error.message).toBe(event.error.message);
- expect(error.name).toBe(event.error.name);
- expect(error.stack).toBe(event.error.stack);
- expect(error.code).toBe(event.error.code);
- expect(error.signal).toBe(event.error.signal);
- });
- });
- });
-
- describe('timezone', () => {
- it('logs in UTC', async () => {
- const format = new KbnLoggerJsonFormat({
- timezone: 'UTC',
- } as any);
-
- const result = await createPromiseFromStreams([
- createListStream([makeEvent('log')]),
- format,
- ]);
-
- const { '@timestamp': timestamp } = JSON.parse(result);
- expect(timestamp).toBe(moment.utc(time).format());
- });
-
- it('logs in local timezone timezone is undefined', async () => {
- const format = new KbnLoggerJsonFormat({} as any);
-
- const result = await createPromiseFromStreams([
- createListStream([makeEvent('log')]),
- format,
- ]);
-
- const { '@timestamp': timestamp } = JSON.parse(result);
- expect(timestamp).toBe(moment(time).format());
- });
- });
-});
diff --git a/packages/kbn-legacy-logging/src/log_format_json.ts b/packages/kbn-legacy-logging/src/log_format_json.ts
deleted file mode 100644
index 427415d1715a6..0000000000000
--- a/packages/kbn-legacy-logging/src/log_format_json.ts
+++ /dev/null
@@ -1,23 +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.
- */
-
-// @ts-expect-error missing type def
-import stringify from 'json-stringify-safe';
-import { BaseLogFormat } from './log_format';
-
-const stripColors = function (string: string) {
- return string.replace(/\u001b[^m]+m/g, '');
-};
-
-export class KbnLoggerJsonFormat extends BaseLogFormat {
- format(data: Record) {
- data.message = stripColors(data.message);
- data['@timestamp'] = this.extractAndFormatTimestamp(data);
- return stringify(data);
- }
-}
diff --git a/packages/kbn-legacy-logging/src/log_format_string.test.ts b/packages/kbn-legacy-logging/src/log_format_string.test.ts
deleted file mode 100644
index 3ea02c2cfb286..0000000000000
--- a/packages/kbn-legacy-logging/src/log_format_string.test.ts
+++ /dev/null
@@ -1,64 +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 moment from 'moment';
-
-import { attachMetaData } from './metadata';
-import { createListStream, createPromiseFromStreams } from '@kbn/utils';
-import { KbnLoggerStringFormat } from './log_format_string';
-
-const time = +moment('2010-01-01T05:15:59Z', moment.ISO_8601);
-
-const makeEvent = () => ({
- event: 'log',
- timestamp: time,
- tags: ['tag'],
- pid: 1,
- data: 'my log message',
-});
-
-describe('KbnLoggerStringFormat', () => {
- it('logs in UTC', async () => {
- const format = new KbnLoggerStringFormat({
- timezone: 'UTC',
- } as any);
-
- const result = await createPromiseFromStreams([createListStream([makeEvent()]), format]);
-
- expect(String(result)).toContain(moment.utc(time).format('HH:mm:ss.SSS'));
- });
-
- it('logs in local timezone when timezone is undefined', async () => {
- const format = new KbnLoggerStringFormat({} as any);
-
- const result = await createPromiseFromStreams([createListStream([makeEvent()]), format]);
-
- expect(String(result)).toContain(moment(time).format('HH:mm:ss.SSS'));
- });
- describe('with metadata', () => {
- it('does not log meta data', async () => {
- const format = new KbnLoggerStringFormat({} as any);
- const event = {
- data: attachMetaData('message for event', {
- prop1: 'value1',
- }),
- tags: ['tag1', 'tag2'],
- };
-
- const result = await createPromiseFromStreams([createListStream([event]), format]);
-
- const resultString = String(result);
- expect(resultString).toContain('tag1');
- expect(resultString).toContain('tag2');
- expect(resultString).toContain('message for event');
-
- expect(resultString).not.toContain('value1');
- expect(resultString).not.toContain('prop1');
- });
- });
-});
diff --git a/packages/kbn-legacy-logging/src/log_format_string.ts b/packages/kbn-legacy-logging/src/log_format_string.ts
deleted file mode 100644
index da21e56e00340..0000000000000
--- a/packages/kbn-legacy-logging/src/log_format_string.ts
+++ /dev/null
@@ -1,65 +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 _ from 'lodash';
-import chalk from 'chalk';
-
-import { BaseLogFormat } from './log_format';
-
-const statuses = ['err', 'info', 'error', 'warning', 'fatal', 'status', 'debug'];
-
-const typeColors: Record = {
- log: 'white',
- req: 'green',
- res: 'green',
- ops: 'cyan',
- config: 'cyan',
- err: 'red',
- info: 'green',
- error: 'red',
- warning: 'red',
- fatal: 'magentaBright',
- status: 'yellowBright',
- debug: 'gray',
- server: 'gray',
- optmzr: 'white',
- manager: 'green',
- optimize: 'magentaBright',
- listening: 'magentaBright',
- scss: 'magentaBright',
-};
-
-const color = _.memoize((name: string): ((...text: string[]) => string) => {
- // @ts-expect-error couldn't even get rid of the error with an any cast
- return chalk[typeColors[name]] || _.identity;
-});
-
-const type = _.memoize((t: string) => {
- return color(t)(_.pad(t, 7).slice(0, 7));
-});
-
-const prefix = process.env.isDevCliChild ? `${type('server')} ` : '';
-
-export class KbnLoggerStringFormat extends BaseLogFormat {
- format(data: Record) {
- const time = color('time')(this.extractAndFormatTimestamp(data, 'HH:mm:ss.SSS'));
- const msg = data.error ? color('error')(data.error.stack) : color('message')(data.message);
-
- const tags = _(data.tags)
- .sortBy(function (tag) {
- if (color(tag) === _.identity) return `2${tag}`;
- if (_.includes(statuses, tag)) return `0${tag}`;
- return `1${tag}`;
- })
- .reduce(function (s, t) {
- return s + `[${color(t)(t)}]`;
- }, '');
-
- return `${prefix}${type(data.type)} [${time}] ${tags} ${msg}`;
- }
-}
diff --git a/packages/kbn-legacy-logging/src/log_interceptor.test.ts b/packages/kbn-legacy-logging/src/log_interceptor.test.ts
deleted file mode 100644
index 53d622444ece8..0000000000000
--- a/packages/kbn-legacy-logging/src/log_interceptor.test.ts
+++ /dev/null
@@ -1,153 +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 { ErrorEvent } from './log_events';
-import { LogInterceptor } from './log_interceptor';
-
-function stubClientErrorEvent(errorMeta: Record): ErrorEvent {
- const error = new Error();
- Object.assign(error, errorMeta);
- return {
- event: 'error',
- url: '',
- pid: 1234,
- timestamp: Date.now(),
- tags: ['connection', 'client', 'error'],
- error,
- };
-}
-
-const stubEconnresetEvent = () => stubClientErrorEvent({ code: 'ECONNRESET' });
-const stubEpipeEvent = () => stubClientErrorEvent({ errno: 'EPIPE' });
-const stubEcanceledEvent = () => stubClientErrorEvent({ errno: 'ECANCELED' });
-
-function assertDowngraded(transformed: Record) {
- expect(!!transformed).toBe(true);
- expect(transformed).toHaveProperty('event', 'log');
- expect(transformed).toHaveProperty('tags');
- expect(transformed.tags).not.toContain('error');
-}
-
-describe('server logging LogInterceptor', () => {
- describe('#downgradeIfEconnreset()', () => {
- it('transforms ECONNRESET events', () => {
- const interceptor = new LogInterceptor();
- const event = stubEconnresetEvent();
- assertDowngraded(interceptor.downgradeIfEconnreset(event)!);
- });
-
- it('does not match if the tags are not in order', () => {
- const interceptor = new LogInterceptor();
- const event = stubEconnresetEvent();
- event.tags = [...event.tags!.slice(1), event.tags![0]];
- expect(interceptor.downgradeIfEconnreset(event)).toBe(null);
- });
-
- it('ignores non ECONNRESET events', () => {
- const interceptor = new LogInterceptor();
- const event = stubClientErrorEvent({ errno: 'not ECONNRESET' });
- expect(interceptor.downgradeIfEconnreset(event)).toBe(null);
- });
-
- it('ignores if tags are wrong', () => {
- const interceptor = new LogInterceptor();
- const event = stubEconnresetEvent();
- event.tags = ['different', 'tags'];
- expect(interceptor.downgradeIfEconnreset(event)).toBe(null);
- });
- });
-
- describe('#downgradeIfEpipe()', () => {
- it('transforms EPIPE events', () => {
- const interceptor = new LogInterceptor();
- const event = stubEpipeEvent();
- assertDowngraded(interceptor.downgradeIfEpipe(event)!);
- });
-
- it('does not match if the tags are not in order', () => {
- const interceptor = new LogInterceptor();
- const event = stubEpipeEvent();
- event.tags = [...event.tags!.slice(1), event.tags![0]];
- expect(interceptor.downgradeIfEpipe(event)).toBe(null);
- });
-
- it('ignores non EPIPE events', () => {
- const interceptor = new LogInterceptor();
- const event = stubClientErrorEvent({ errno: 'not EPIPE' });
- expect(interceptor.downgradeIfEpipe(event)).toBe(null);
- });
-
- it('ignores if tags are wrong', () => {
- const interceptor = new LogInterceptor();
- const event = stubEpipeEvent();
- event.tags = ['different', 'tags'];
- expect(interceptor.downgradeIfEpipe(event)).toBe(null);
- });
- });
-
- describe('#downgradeIfEcanceled()', () => {
- it('transforms ECANCELED events', () => {
- const interceptor = new LogInterceptor();
- const event = stubEcanceledEvent();
- assertDowngraded(interceptor.downgradeIfEcanceled(event)!);
- });
-
- it('does not match if the tags are not in order', () => {
- const interceptor = new LogInterceptor();
- const event = stubEcanceledEvent();
- event.tags = [...event.tags!.slice(1), event.tags![0]];
- expect(interceptor.downgradeIfEcanceled(event)).toBe(null);
- });
-
- it('ignores non ECANCELED events', () => {
- const interceptor = new LogInterceptor();
- const event = stubClientErrorEvent({ errno: 'not ECANCELLED' });
- expect(interceptor.downgradeIfEcanceled(event)).toBe(null);
- });
-
- it('ignores if tags are wrong', () => {
- const interceptor = new LogInterceptor();
- const event = stubEcanceledEvent();
- event.tags = ['different', 'tags'];
- expect(interceptor.downgradeIfEcanceled(event)).toBe(null);
- });
- });
-
- describe('#downgradeIfHTTPSWhenHTTP', () => {
- it('transforms https requests when serving http errors', () => {
- const interceptor = new LogInterceptor();
- const event = stubClientErrorEvent({ message: 'Parse Error', code: 'HPE_INVALID_METHOD' });
- assertDowngraded(interceptor.downgradeIfHTTPSWhenHTTP(event)!);
- });
-
- it('ignores non events', () => {
- const interceptor = new LogInterceptor();
- const event = stubClientErrorEvent({
- message: 'Parse Error',
- code: 'NOT_HPE_INVALID_METHOD',
- });
- expect(interceptor.downgradeIfEcanceled(event)).toBe(null);
- });
- });
-
- describe('#downgradeIfHTTPWhenHTTPS', () => {
- it('transforms http requests when serving https errors', () => {
- const message =
- '4584650176:error:1408F09C:SSL routines:ssl3_get_record:http request:../deps/openssl/openssl/ssl/record/ssl3_record.c:322:\n';
- const interceptor = new LogInterceptor();
- const event = stubClientErrorEvent({ message });
- assertDowngraded(interceptor.downgradeIfHTTPWhenHTTPS(event)!);
- });
-
- it('ignores non events', () => {
- const interceptor = new LogInterceptor();
- const event = stubClientErrorEvent({ message: 'Not error' });
- expect(interceptor.downgradeIfEcanceled(event)).toBe(null);
- });
- });
-});
diff --git a/packages/kbn-legacy-logging/src/log_interceptor.ts b/packages/kbn-legacy-logging/src/log_interceptor.ts
deleted file mode 100644
index 1085806135ca6..0000000000000
--- a/packages/kbn-legacy-logging/src/log_interceptor.ts
+++ /dev/null
@@ -1,144 +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 Stream from 'stream';
-import { get, isEqual } from 'lodash';
-import { AnyEvent } from './log_events';
-
-/**
- * Matches error messages when clients connect via HTTP instead of HTTPS; see unit test for full message. Warning: this can change when Node
- * and its bundled OpenSSL binary are upgraded.
- */
-const OPENSSL_GET_RECORD_REGEX = /ssl3_get_record:http/;
-
-function doTagsMatch(event: AnyEvent, tags: string[]) {
- return isEqual(event.tags, tags);
-}
-
-function doesMessageMatch(errorMessage: string, match: RegExp | string) {
- if (!errorMessage) {
- return false;
- }
- if (match instanceof RegExp) {
- return match.test(errorMessage);
- }
- return errorMessage === match;
-}
-
-// converts the given event into a debug log if it's an error of the given type
-function downgradeIfErrorType(errorType: string, event: AnyEvent) {
- const isClientError = doTagsMatch(event, ['connection', 'client', 'error']);
- if (!isClientError) {
- return null;
- }
-
- const matchesErrorType =
- get(event, 'error.code') === errorType || get(event, 'error.errno') === errorType;
- if (!matchesErrorType) {
- return null;
- }
-
- const errorTypeTag = errorType.toLowerCase();
-
- return {
- event: 'log',
- pid: event.pid,
- timestamp: event.timestamp,
- tags: ['debug', 'connection', errorTypeTag],
- data: `${errorType}: Socket was closed by the client (probably the browser) before it could be read completely`,
- };
-}
-
-function downgradeIfErrorMessage(match: RegExp | string, event: AnyEvent) {
- const isClientError = doTagsMatch(event, ['connection', 'client', 'error']);
- const errorMessage = get(event, 'error.message');
- const matchesErrorMessage = isClientError && doesMessageMatch(errorMessage, match);
-
- if (!matchesErrorMessage) {
- return null;
- }
-
- return {
- event: 'log',
- pid: event.pid,
- timestamp: event.timestamp,
- tags: ['debug', 'connection'],
- data: errorMessage,
- };
-}
-
-export class LogInterceptor extends Stream.Transform {
- constructor() {
- super({
- readableObjectMode: true,
- writableObjectMode: true,
- });
- }
-
- /**
- * Since the upgrade to hapi 14, any socket read
- * error is surfaced as a generic "client error"
- * but "ECONNRESET" specifically is not useful for the
- * logs unless you are trying to debug edge-case behaviors.
- *
- * For that reason, we downgrade this from error to debug level
- *
- * @param {object} - log event
- */
- downgradeIfEconnreset(event: AnyEvent) {
- return downgradeIfErrorType('ECONNRESET', event);
- }
-
- /**
- * Since the upgrade to hapi 14, any socket write
- * error is surfaced as a generic "client error"
- * but "EPIPE" specifically is not useful for the
- * logs unless you are trying to debug edge-case behaviors.
- *
- * For that reason, we downgrade this from error to debug level
- *
- * @param {object} - log event
- */
- downgradeIfEpipe(event: AnyEvent) {
- return downgradeIfErrorType('EPIPE', event);
- }
-
- /**
- * Since the upgrade to hapi 14, any socket write
- * error is surfaced as a generic "client error"
- * but "ECANCELED" specifically is not useful for the
- * logs unless you are trying to debug edge-case behaviors.
- *
- * For that reason, we downgrade this from error to debug level
- *
- * @param {object} - log event
- */
- downgradeIfEcanceled(event: AnyEvent) {
- return downgradeIfErrorType('ECANCELED', event);
- }
-
- downgradeIfHTTPSWhenHTTP(event: AnyEvent) {
- return downgradeIfErrorType('HPE_INVALID_METHOD', event);
- }
-
- downgradeIfHTTPWhenHTTPS(event: AnyEvent) {
- return downgradeIfErrorMessage(OPENSSL_GET_RECORD_REGEX, event);
- }
-
- _transform(event: AnyEvent, enc: string, next: Stream.TransformCallback) {
- const downgraded =
- this.downgradeIfEconnreset(event) ||
- this.downgradeIfEpipe(event) ||
- this.downgradeIfEcanceled(event) ||
- this.downgradeIfHTTPSWhenHTTP(event) ||
- this.downgradeIfHTTPWhenHTTPS(event);
-
- this.push(downgraded || event);
- next();
- }
-}
diff --git a/packages/kbn-legacy-logging/src/log_reporter.test.ts b/packages/kbn-legacy-logging/src/log_reporter.test.ts
deleted file mode 100644
index a2ad8984ba244..0000000000000
--- a/packages/kbn-legacy-logging/src/log_reporter.test.ts
+++ /dev/null
@@ -1,131 +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 os from 'os';
-import path from 'path';
-import fs from 'fs';
-
-import stripAnsi from 'strip-ansi';
-
-import { getLogReporter } from './log_reporter';
-
-const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
-
-describe('getLogReporter', () => {
- it('should log to stdout (not json)', async () => {
- const lines: string[] = [];
- const origWrite = process.stdout.write;
- process.stdout.write = (buffer: string | Uint8Array): boolean => {
- lines.push(stripAnsi(buffer.toString()).trim());
- return true;
- };
-
- const loggerStream = getLogReporter({
- config: {
- json: false,
- dest: 'stdout',
- filter: {},
- },
- events: { log: '*' },
- });
-
- loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' });
-
- await sleep(500);
-
- process.stdout.write = origWrite;
- expect(lines.length).toBe(1);
- expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] hello world$/);
- });
-
- it('should log to stdout (as json)', async () => {
- const lines: string[] = [];
- const origWrite = process.stdout.write;
- process.stdout.write = (buffer: string | Uint8Array): boolean => {
- lines.push(JSON.parse(buffer.toString().trim()));
- return true;
- };
-
- const loggerStream = getLogReporter({
- config: {
- json: true,
- dest: 'stdout',
- filter: {},
- },
- events: { log: '*' },
- });
-
- loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' });
-
- await sleep(500);
-
- process.stdout.write = origWrite;
- expect(lines.length).toBe(1);
- expect(lines[0]).toMatchObject({
- type: 'log',
- tags: ['foo'],
- message: 'hello world',
- });
- });
-
- it('should log to custom file (not json)', async () => {
- const dir = os.tmpdir();
- const logfile = `dest-${Date.now()}.log`;
- const dest = path.join(dir, logfile);
-
- const loggerStream = getLogReporter({
- config: {
- json: false,
- dest,
- filter: {},
- },
- events: { log: '*' },
- });
-
- loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' });
-
- await sleep(500);
-
- const lines = stripAnsi(fs.readFileSync(dest, { encoding: 'utf8' }))
- .trim()
- .split(os.EOL);
- expect(lines.length).toBe(1);
- expect(lines[0]).toMatch(/^log \[[^\]]*\] \[foo\] hello world$/);
- });
-
- it('should log to custom file (as json)', async () => {
- const dir = os.tmpdir();
- const logfile = `dest-${Date.now()}.log`;
- const dest = path.join(dir, logfile);
-
- const loggerStream = getLogReporter({
- config: {
- json: true,
- dest,
- filter: {},
- },
- events: { log: '*' },
- });
-
- loggerStream.end({ event: 'log', tags: ['foo'], data: 'hello world' });
-
- await sleep(500);
-
- const lines = fs
- .readFileSync(dest, { encoding: 'utf8' })
- .trim()
- .split(os.EOL)
- .map((data) => JSON.parse(data));
- expect(lines.length).toBe(1);
- expect(lines[0]).toMatchObject({
- type: 'log',
- tags: ['foo'],
- message: 'hello world',
- });
- });
-});
diff --git a/packages/kbn-legacy-logging/src/log_reporter.ts b/packages/kbn-legacy-logging/src/log_reporter.ts
deleted file mode 100644
index d42fb78f1647b..0000000000000
--- a/packages/kbn-legacy-logging/src/log_reporter.ts
+++ /dev/null
@@ -1,49 +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 { createWriteStream } from 'fs';
-import { pipeline } from 'stream';
-
-// @ts-expect-error missing type def
-import { Squeeze } from '@hapi/good-squeeze';
-
-import { KbnLoggerJsonFormat } from './log_format_json';
-import { KbnLoggerStringFormat } from './log_format_string';
-import { LogInterceptor } from './log_interceptor';
-import { LogFormatConfig } from './log_format';
-
-export function getLogReporter({ events, config }: { events: any; config: LogFormatConfig }) {
- const squeeze = new Squeeze(events);
- const format = config.json ? new KbnLoggerJsonFormat(config) : new KbnLoggerStringFormat(config);
- const logInterceptor = new LogInterceptor();
-
- if (config.dest === 'stdout') {
- pipeline(logInterceptor, squeeze, format, onFinished);
- // The `pipeline` function is used to properly close all streams in the
- // pipeline in case one of them ends or fails. Since stdout obviously
- // shouldn't be closed in case of a failure in one of the other streams,
- // we're not including that in the call to `pipeline`, but rely on the old
- // `pipe` function instead.
- format.pipe(process.stdout);
- } else {
- const dest = createWriteStream(config.dest, {
- flags: 'a',
- encoding: 'utf8',
- });
- pipeline(logInterceptor, squeeze, format, dest, onFinished);
- }
-
- return logInterceptor;
-}
-
-function onFinished(err: NodeJS.ErrnoException | null) {
- if (err) {
- // eslint-disable-next-line no-console
- console.error('An unexpected error occurred in the logging pipeline:', err.stack);
- }
-}
diff --git a/packages/kbn-legacy-logging/src/metadata.ts b/packages/kbn-legacy-logging/src/metadata.ts
deleted file mode 100644
index 0f41673ef6723..0000000000000
--- a/packages/kbn-legacy-logging/src/metadata.ts
+++ /dev/null
@@ -1,42 +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 { isPlainObject } from 'lodash';
-
-export const metadataSymbol = Symbol('log message with metadata');
-
-export interface EventData {
- [metadataSymbol]?: EventMetadata;
- [key: string]: any;
-}
-
-export interface EventMetadata {
- message: string;
- metadata: Record;
-}
-
-export const isEventData = (eventData: EventData) => {
- return Boolean(isPlainObject(eventData) && eventData[metadataSymbol]);
-};
-
-export const getLogEventData = (eventData: EventData) => {
- const { message, metadata } = eventData[metadataSymbol]!;
- return {
- ...metadata,
- message,
- };
-};
-
-export const attachMetaData = (message: string, metadata: Record = {}) => {
- return {
- [metadataSymbol]: {
- message,
- metadata,
- },
- };
-};
diff --git a/packages/kbn-legacy-logging/src/rotate/index.ts b/packages/kbn-legacy-logging/src/rotate/index.ts
deleted file mode 100644
index 39305dcccf788..0000000000000
--- a/packages/kbn-legacy-logging/src/rotate/index.ts
+++ /dev/null
@@ -1,41 +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 { Server } from '@hapi/hapi';
-import { LogRotator } from './log_rotator';
-import { LegacyLoggingConfig } from '../schema';
-
-let logRotator: LogRotator;
-
-export async function setupLoggingRotate(server: Server, config: LegacyLoggingConfig) {
- // If log rotate is not enabled we skip
- if (!config.rotate.enabled) {
- return;
- }
-
- // We don't want to run logging rotate server if
- // we are not logging to a file
- if (config.dest === 'stdout') {
- server.log(
- ['warning', 'logging:rotate'],
- 'Log rotation is enabled but logging.dest is configured for stdout. Set logging.dest to a file for this setting to take effect.'
- );
- return;
- }
-
- // Enable Logging Rotate Service
- // We need the master process and it can
- // try to setupLoggingRotate more than once,
- // so we'll need to assure it only loads once.
- if (!logRotator) {
- logRotator = new LogRotator(config, server);
- await logRotator.start();
- }
-
- return logRotator;
-}
diff --git a/packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts
deleted file mode 100644
index ce9a24e63455f..0000000000000
--- a/packages/kbn-legacy-logging/src/rotate/log_rotator.test.ts
+++ /dev/null
@@ -1,261 +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 del from 'del';
-import fs, { existsSync, mkdirSync, statSync, writeFileSync } from 'fs';
-import { tmpdir } from 'os';
-import { dirname, join } from 'path';
-import { LogRotator } from './log_rotator';
-import { LegacyLoggingConfig } from '../schema';
-
-const mockOn = jest.fn();
-jest.mock('chokidar', () => ({
- watch: jest.fn(() => ({
- on: mockOn,
- close: jest.fn(),
- })),
-}));
-
-jest.mock('lodash', () => ({
- ...(jest.requireActual('lodash') as any),
- throttle: (fn: any) => fn,
-}));
-
-const tempDir = join(tmpdir(), 'kbn_log_rotator_test');
-const testFilePath = join(tempDir, 'log_rotator_test_log_file.log');
-
-const createLogRotatorConfig = (logFilePath: string): LegacyLoggingConfig => {
- return {
- dest: logFilePath,
- rotate: {
- enabled: true,
- keepFiles: 2,
- everyBytes: 2,
- usePolling: false,
- pollingInterval: 10000,
- pollingPolicyTestTimeout: 4000,
- },
- } as LegacyLoggingConfig;
-};
-
-const mockServer: any = {
- log: jest.fn(),
-};
-
-const writeBytesToFile = (filePath: string, numberOfBytes: number) => {
- writeFileSync(filePath, 'a'.repeat(numberOfBytes), { flag: 'a' });
-};
-
-describe('LogRotator', () => {
- beforeEach(() => {
- mkdirSync(tempDir, { recursive: true });
- writeFileSync(testFilePath, '');
- });
-
- afterEach(() => {
- del.sync(tempDir, { force: true });
- mockOn.mockClear();
- });
-
- it('rotates log file when bigger than set limit on start', async () => {
- writeBytesToFile(testFilePath, 3);
-
- const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer);
- jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {});
-
- await logRotator.start();
-
- expect(logRotator.running).toBe(true);
-
- await logRotator.stop();
-
- expect(existsSync(join(tempDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy();
- });
-
- it('rotates log file when equal than set limit over time', async () => {
- writeBytesToFile(testFilePath, 1);
-
- const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer);
- jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {});
- await logRotator.start();
-
- expect(logRotator.running).toBe(true);
-
- const testLogFileDir = dirname(testFilePath);
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeFalsy();
-
- writeBytesToFile(testFilePath, 1);
-
- // ['change', [asyncFunction]]
- const onChangeCb = mockOn.mock.calls[0][1];
- await onChangeCb(testLogFileDir, { size: 2 });
-
- await logRotator.stop();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy();
- });
-
- it('rotates log file when file size is bigger than limit', async () => {
- writeBytesToFile(testFilePath, 1);
-
- const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer);
- jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {});
- await logRotator.start();
-
- expect(logRotator.running).toBe(true);
-
- const testLogFileDir = dirname(testFilePath);
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeFalsy();
-
- writeBytesToFile(testFilePath, 2);
-
- // ['change', [asyncFunction]]
- const onChangeCb = mockOn.mock.calls[0][1];
- await onChangeCb(testLogFileDir, { size: 3 });
-
- await logRotator.stop();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy();
- });
-
- it('rotates log file service correctly keeps number of files', async () => {
- writeBytesToFile(testFilePath, 3);
-
- const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer);
- jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {});
- await logRotator.start();
-
- expect(logRotator.running).toBe(true);
-
- const testLogFileDir = dirname(testFilePath);
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy();
-
- writeBytesToFile(testFilePath, 2);
-
- // ['change', [asyncFunction]]
- const onChangeCb = mockOn.mock.calls[0][1];
- await onChangeCb(testLogFileDir, { size: 2 });
-
- writeBytesToFile(testFilePath, 5);
- await onChangeCb(testLogFileDir, { size: 5 });
-
- await logRotator.stop();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.1'))).toBeTruthy();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.2'))).toBeFalsy();
- expect(statSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0')).size).toBe(5);
- });
-
- it('rotates log file service correctly keeps number of files even when number setting changes', async () => {
- writeBytesToFile(testFilePath, 3);
-
- const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer);
- jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {});
- await logRotator.start();
-
- expect(logRotator.running).toBe(true);
-
- const testLogFileDir = dirname(testFilePath);
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy();
-
- writeBytesToFile(testFilePath, 2);
-
- // ['change', [asyncFunction]]
- const onChangeCb = mockOn.mock.calls[0][1];
- await onChangeCb(testLogFileDir, { size: 2 });
-
- writeBytesToFile(testFilePath, 5);
- await onChangeCb(testLogFileDir, { size: 5 });
-
- await logRotator.stop();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.1'))).toBeTruthy();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.2'))).toBeFalsy();
- expect(statSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0')).size).toBe(5);
-
- logRotator.keepFiles = 1;
- await logRotator.start();
-
- writeBytesToFile(testFilePath, 5);
- await onChangeCb(testLogFileDir, { size: 5 });
-
- await logRotator.stop();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0'))).toBeTruthy();
- expect(existsSync(join(testLogFileDir, 'log_rotator_test_log_file.log.1'))).toBeFalsy();
- expect(statSync(join(testLogFileDir, 'log_rotator_test_log_file.log.0')).size).toBe(5);
- });
-
- it('rotates log file service correctly detects usePolling when it should be false', async () => {
- writeBytesToFile(testFilePath, 1);
-
- const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer);
- jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {});
- await logRotator.start();
-
- expect(logRotator.running).toBe(true);
- expect(logRotator.usePolling).toBe(false);
-
- const shouldUsePolling = await logRotator._shouldUsePolling();
- expect(shouldUsePolling).toBe(false);
-
- await logRotator.stop();
- });
-
- it('rotates log file service correctly detects usePolling when it should be true', async () => {
- writeBytesToFile(testFilePath, 1);
-
- const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer);
- jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {});
-
- jest.spyOn(fs, 'watch').mockImplementation(
- () =>
- ({
- on: jest.fn((eventType, cb) => {
- if (eventType === 'error') {
- cb();
- }
- }),
- close: jest.fn(),
- } as any)
- );
-
- await logRotator.start();
-
- expect(logRotator.running).toBe(true);
- expect(logRotator.usePolling).toBe(false);
- expect(logRotator.shouldUsePolling).toBe(true);
-
- await logRotator.stop();
- });
-
- it('rotates log file service correctly fallback to usePolling true after defined timeout', async () => {
- jest.useFakeTimers();
- writeBytesToFile(testFilePath, 1);
-
- const logRotator = new LogRotator(createLogRotatorConfig(testFilePath), mockServer);
- jest.spyOn(logRotator, '_sendReloadLogConfigSignal').mockImplementation(() => {});
- jest.spyOn(fs, 'watch').mockImplementation(
- () =>
- ({
- on: jest.fn((ev: string) => {
- if (ev === 'error') {
- jest.runTimersToTime(15000);
- }
- }),
- close: jest.fn(),
- } as any)
- );
-
- await logRotator.start();
-
- expect(logRotator.running).toBe(true);
- expect(logRotator.usePolling).toBe(false);
- expect(logRotator.shouldUsePolling).toBe(true);
-
- await logRotator.stop();
- jest.useRealTimers();
- });
-});
diff --git a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts
deleted file mode 100644
index 4b1e34839030f..0000000000000
--- a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts
+++ /dev/null
@@ -1,352 +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 * as chokidar from 'chokidar';
-import fs from 'fs';
-import { Server } from '@hapi/hapi';
-import { throttle } from 'lodash';
-import { tmpdir } from 'os';
-import { basename, dirname, join, sep } from 'path';
-import { Observable } from 'rxjs';
-import { first } from 'rxjs/operators';
-import { promisify } from 'util';
-import { LegacyLoggingConfig } from '../schema';
-
-const mkdirAsync = promisify(fs.mkdir);
-const readdirAsync = promisify(fs.readdir);
-const renameAsync = promisify(fs.rename);
-const statAsync = promisify(fs.stat);
-const unlinkAsync = promisify(fs.unlink);
-const writeFileAsync = promisify(fs.writeFile);
-
-export class LogRotator {
- private readonly config: LegacyLoggingConfig;
- private readonly log: Server['log'];
- public logFilePath: string;
- public everyBytes: number;
- public keepFiles: number;
- public running: boolean;
- private logFileSize: number;
- public isRotating: boolean;
- public throttledRotate: () => void;
- public stalker: chokidar.FSWatcher | null;
- public usePolling: boolean;
- public pollingInterval: number;
- private stalkerUsePollingPolicyTestTimeout: NodeJS.Timeout | null;
- public shouldUsePolling: boolean;
-
- constructor(config: LegacyLoggingConfig, server: Server) {
- this.config = config;
- this.log = server.log.bind(server);
- this.logFilePath = config.dest;
- this.everyBytes = config.rotate.everyBytes;
- this.keepFiles = config.rotate.keepFiles;
- this.running = false;
- this.logFileSize = 0;
- this.isRotating = false;
- this.throttledRotate = throttle(async () => await this._rotate(), 5000);
- this.stalker = null;
- this.usePolling = config.rotate.usePolling;
- this.pollingInterval = config.rotate.pollingInterval;
- this.shouldUsePolling = false;
- this.stalkerUsePollingPolicyTestTimeout = null;
- }
-
- async start() {
- if (this.running) {
- return;
- }
-
- this.running = true;
-
- // create exit listener for cleanup purposes
- this._createExitListener();
-
- // call rotate on startup
- await this._callRotateOnStartup();
-
- // init log file size monitor
- await this._startLogFileSizeMonitor();
- }
-
- stop = () => {
- if (!this.running) {
- return;
- }
-
- // cleanup exit listener
- this._deleteExitListener();
-
- // stop log file size monitor
- this._stopLogFileSizeMonitor();
-
- this.running = false;
- };
-
- async _shouldUsePolling() {
- try {
- // Setup a test file in order to try the fs env
- // and understand if we need to usePolling or not
- const tempFileDir = tmpdir();
- const tempFile = join(tempFileDir, 'kbn_log_rotation_use_polling_test_file.log');
-
- await mkdirAsync(tempFileDir, { recursive: true });
- await writeFileAsync(tempFile, '');
-
- // setup fs.watch for the temp test file
- const testWatcher = fs.watch(tempFile, { persistent: false });
-
- // await writeFileAsync(tempFile, 'test');
-
- const usePollingTest$ = new Observable((observer) => {
- // observable complete function
- const completeFn = (completeStatus: boolean) => {
- if (this.stalkerUsePollingPolicyTestTimeout) {
- clearTimeout(this.stalkerUsePollingPolicyTestTimeout);
- }
- testWatcher.close();
-
- observer.next(completeStatus);
- observer.complete();
- };
-
- // setup conditions that would fire the observable
- this.stalkerUsePollingPolicyTestTimeout = setTimeout(
- () => completeFn(true),
- this.config.rotate.pollingPolicyTestTimeout || 15000
- );
- testWatcher.on('change', () => completeFn(false));
- testWatcher.on('error', () => completeFn(true));
-
- // fire test watcher events
- setTimeout(() => {
- fs.writeFileSync(tempFile, 'test');
- }, 0);
- });
-
- // wait for the first observable result and consider it as the result
- // for our use polling test
- const usePollingTestResult = await usePollingTest$.pipe(first()).toPromise();
-
- // delete the temp file used for the test
- await unlinkAsync(tempFile);
-
- return usePollingTestResult;
- } catch {
- return true;
- }
- }
-
- async _startLogFileSizeMonitor() {
- this.usePolling = this.config.rotate.usePolling;
- this.shouldUsePolling = await this._shouldUsePolling();
-
- if (this.usePolling && !this.shouldUsePolling) {
- this.log(
- ['warning', 'logging:rotate'],
- 'Looks like your current environment support a faster algorithm than polling. You can try to disable `usePolling`'
- );
- }
-
- if (!this.usePolling && this.shouldUsePolling) {
- this.log(
- ['error', 'logging:rotate'],
- 'Looks like within your current environment you need to use polling in order to enable log rotator. Please enable `usePolling`'
- );
- }
-
- this.stalker = chokidar.watch(this.logFilePath, {
- ignoreInitial: true,
- awaitWriteFinish: false,
- useFsEvents: false,
- usePolling: this.usePolling,
- interval: this.pollingInterval,
- binaryInterval: this.pollingInterval,
- alwaysStat: true,
- atomic: false,
- });
- this.stalker.on('change', this._logFileSizeMonitorHandler);
- }
-
- _logFileSizeMonitorHandler = async (filename: string, stats: fs.Stats) => {
- if (!filename || !stats) {
- return;
- }
-
- this.logFileSize = stats.size || 0;
- await this.throttledRotate();
- };
-
- _stopLogFileSizeMonitor() {
- if (!this.stalker) {
- return;
- }
-
- this.stalker.close();
-
- if (this.stalkerUsePollingPolicyTestTimeout) {
- clearTimeout(this.stalkerUsePollingPolicyTestTimeout);
- }
- }
-
- _createExitListener() {
- process.on('exit', this.stop);
- }
-
- _deleteExitListener() {
- process.removeListener('exit', this.stop);
- }
-
- async _getLogFileSizeAndCreateIfNeeded() {
- try {
- const logFileStats = await statAsync(this.logFilePath);
- return logFileStats.size;
- } catch {
- // touch the file to make the watcher being able to register
- // change events
- await writeFileAsync(this.logFilePath, '');
- return 0;
- }
- }
-
- async _callRotateOnStartup() {
- this.logFileSize = await this._getLogFileSizeAndCreateIfNeeded();
- await this._rotate();
- }
-
- _shouldRotate() {
- // should rotate evaluation
- // 1. should rotate if current log size exceeds
- // the defined one on everyBytes
- // 2. should not rotate if is already rotating or if any
- // of the conditions on 1. do not apply
- if (this.isRotating) {
- return false;
- }
-
- return this.logFileSize >= this.everyBytes;
- }
-
- async _rotate() {
- if (!this._shouldRotate()) {
- return;
- }
-
- await this._rotateNow();
- }
-
- async _rotateNow() {
- // rotate process
- // 1. get rotated files metadata (list of log rotated files present on the log folder, numerical sorted)
- // 2. delete last file
- // 3. rename all files to the correct index +1
- // 4. rename + compress current log into 1
- // 5. send SIGHUP to reload log config
-
- // rotate process is starting
- this.isRotating = true;
-
- // get rotated files metadata
- const foundRotatedFiles = await this._readRotatedFilesMetadata();
-
- // delete number of rotated files exceeding the keepFiles limit setting
- const rotatedFiles: string[] = await this._deleteFoundRotatedFilesAboveKeepFilesLimit(
- foundRotatedFiles
- );
-
- // delete last file
- await this._deleteLastRotatedFile(rotatedFiles);
-
- // rename all files to correct index + 1
- // and normalize numbering if by some reason
- // (for example log file deletion) that numbering
- // was interrupted
- await this._renameRotatedFilesByOne(rotatedFiles);
-
- // rename current log into 0
- await this._rotateCurrentLogFile();
-
- // send SIGHUP to reload log configuration
- this._sendReloadLogConfigSignal();
-
- // Reset log file size
- this.logFileSize = 0;
-
- // rotate process is finished
- this.isRotating = false;
- }
-
- async _readRotatedFilesMetadata() {
- const logFileBaseName = basename(this.logFilePath);
- const logFilesFolder = dirname(this.logFilePath);
- const foundLogFiles: string[] = await readdirAsync(logFilesFolder);
-
- return (
- foundLogFiles
- .filter((file) => new RegExp(`${logFileBaseName}\\.\\d`).test(file))
- // we use .slice(-1) here in order to retrieve the last number match in the read filenames
- .sort((a, b) => Number(a.match(/(\d+)/g)!.slice(-1)) - Number(b.match(/(\d+)/g)!.slice(-1)))
- .map((filename) => `${logFilesFolder}${sep}${filename}`)
- );
- }
-
- async _deleteFoundRotatedFilesAboveKeepFilesLimit(foundRotatedFiles: string[]) {
- if (foundRotatedFiles.length <= this.keepFiles) {
- return foundRotatedFiles;
- }
-
- const finalRotatedFiles = foundRotatedFiles.slice(0, this.keepFiles);
- const rotatedFilesToDelete = foundRotatedFiles.slice(
- finalRotatedFiles.length,
- foundRotatedFiles.length
- );
-
- await Promise.all(
- rotatedFilesToDelete.map((rotatedFilePath: string) => unlinkAsync(rotatedFilePath))
- );
-
- return finalRotatedFiles;
- }
-
- async _deleteLastRotatedFile(rotatedFiles: string[]) {
- if (rotatedFiles.length < this.keepFiles) {
- return;
- }
-
- const lastFilePath: string = rotatedFiles.pop() as string;
- await unlinkAsync(lastFilePath);
- }
-
- async _renameRotatedFilesByOne(rotatedFiles: string[]) {
- const logFileBaseName = basename(this.logFilePath);
- const logFilesFolder = dirname(this.logFilePath);
-
- for (let i = rotatedFiles.length - 1; i >= 0; i--) {
- const oldFilePath = rotatedFiles[i];
- const newFilePath = `${logFilesFolder}${sep}${logFileBaseName}.${i + 1}`;
- await renameAsync(oldFilePath, newFilePath);
- }
- }
-
- async _rotateCurrentLogFile() {
- const newFilePath = `${this.logFilePath}.0`;
- await renameAsync(this.logFilePath, newFilePath);
- }
-
- _sendReloadLogConfigSignal() {
- if (!process.env.isDevCliChild || !process.send) {
- process.emit('SIGHUP', 'SIGHUP');
- return;
- }
-
- // Send a special message to the cluster manager
- // so it can forward it correctly
- // It will only run when we are under cluster mode (not under a production environment)
- process.send(['RELOAD_LOGGING_CONFIG_FROM_SERVER_WORKER']);
- }
-}
diff --git a/packages/kbn-legacy-logging/src/schema.ts b/packages/kbn-legacy-logging/src/schema.ts
deleted file mode 100644
index 0330708e746c0..0000000000000
--- a/packages/kbn-legacy-logging/src/schema.ts
+++ /dev/null
@@ -1,97 +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 { schema } from '@kbn/config-schema';
-
-/**
- * @deprecated
- *
- * Legacy logging has been deprecated and will be removed in 8.0.
- * Set up logging from the platform logging instead
- */
-export interface LegacyLoggingConfig {
- silent: boolean;
- quiet: boolean;
- verbose: boolean;
- events: Record;
- dest: string;
- filter: Record;
- json: boolean;
- timezone?: string;
- rotate: {
- enabled: boolean;
- everyBytes: number;
- keepFiles: number;
- pollingInterval: number;
- usePolling: boolean;
- pollingPolicyTestTimeout?: number;
- };
-}
-
-export const legacyLoggingConfigSchema = schema.object({
- silent: schema.boolean({ defaultValue: false }),
- quiet: schema.conditional(
- schema.siblingRef('silent'),
- true,
- schema.boolean({
- defaultValue: true,
- validate: (quiet) => {
- if (!quiet) {
- return 'must be true when `silent` is true';
- }
- },
- }),
- schema.boolean({ defaultValue: false })
- ),
- verbose: schema.conditional(
- schema.siblingRef('quiet'),
- true,
- schema.boolean({
- defaultValue: false,
- validate: (verbose) => {
- if (verbose) {
- return 'must be false when `quiet` is true';
- }
- },
- }),
- schema.boolean({ defaultValue: false })
- ),
- events: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
- dest: schema.string({ defaultValue: 'stdout' }),
- filter: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
- json: schema.conditional(
- schema.siblingRef('dest'),
- 'stdout',
- schema.boolean({
- defaultValue: !process.stdout.isTTY,
- }),
- schema.boolean({
- defaultValue: true,
- })
- ),
- timezone: schema.maybe(schema.string()),
- rotate: schema.object({
- enabled: schema.boolean({ defaultValue: false }),
- everyBytes: schema.number({
- min: 1048576, // > 1MB
- max: 1073741825, // < 1GB
- defaultValue: 10485760, // 10MB
- }),
- keepFiles: schema.number({
- min: 2,
- max: 1024,
- defaultValue: 7,
- }),
- pollingInterval: schema.number({
- min: 5000,
- max: 3600000,
- defaultValue: 10000,
- }),
- usePolling: schema.boolean({ defaultValue: false }),
- }),
-});
diff --git a/packages/kbn-legacy-logging/src/setup_logging.test.ts b/packages/kbn-legacy-logging/src/setup_logging.test.ts
deleted file mode 100644
index 8e1d76477f64a..0000000000000
--- a/packages/kbn-legacy-logging/src/setup_logging.test.ts
+++ /dev/null
@@ -1,35 +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 { Server } from '@hapi/hapi';
-import { reconfigureLogging, setupLogging } from './setup_logging';
-import { LegacyLoggingConfig } from './schema';
-
-describe('reconfigureLogging', () => {
- test(`doesn't throw an error`, () => {
- const server = new Server();
- const config: LegacyLoggingConfig = {
- silent: false,
- quiet: false,
- verbose: true,
- events: {},
- dest: '/tmp/foo',
- filter: {},
- json: true,
- rotate: {
- enabled: false,
- everyBytes: 0,
- keepFiles: 0,
- pollingInterval: 0,
- usePolling: false,
- },
- };
- setupLogging(server, config, 10);
- reconfigureLogging(server, { ...config, dest: '/tmp/bar' }, 0);
- });
-});
diff --git a/packages/kbn-legacy-logging/src/setup_logging.ts b/packages/kbn-legacy-logging/src/setup_logging.ts
deleted file mode 100644
index a045469e81251..0000000000000
--- a/packages/kbn-legacy-logging/src/setup_logging.ts
+++ /dev/null
@@ -1,41 +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.
- */
-
-// @ts-expect-error missing typedef
-import { plugin as good } from '@elastic/good';
-import { Server } from '@hapi/hapi';
-import { LegacyLoggingConfig } from './schema';
-import { getLoggingConfiguration } from './get_logging_config';
-
-export async function setupLogging(
- server: Server,
- config: LegacyLoggingConfig,
- opsInterval: number
-) {
- // NOTE: legacy logger creates a new stream for each new access
- // In https://github.com/elastic/kibana/pull/55937 we reach the max listeners
- // default limit of 10 for process.stdout which starts a long warning/error
- // thrown every time we start the server.
- // In order to keep using the legacy logger until we remove it I'm just adding
- // a new hard limit here.
- process.stdout.setMaxListeners(60);
-
- return await server.register({
- plugin: good,
- options: getLoggingConfiguration(config, opsInterval),
- });
-}
-
-export function reconfigureLogging(
- server: Server,
- config: LegacyLoggingConfig,
- opsInterval: number
-) {
- const loggingOptions = getLoggingConfiguration(config, opsInterval);
- (server.plugins as any).good.reconfigure(loggingOptions);
-}
diff --git a/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.test.ts b/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.test.ts
deleted file mode 100644
index b662c88eba7b7..0000000000000
--- a/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.test.ts
+++ /dev/null
@@ -1,49 +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 { applyFiltersToKeys } from './apply_filters_to_keys';
-
-describe('applyFiltersToKeys(obj, actionsByKey)', function () {
- it('applies for each key+prop in actionsByKey', function () {
- const data = applyFiltersToKeys(
- {
- a: {
- b: {
- c: 1,
- },
- d: {
- e: 'foobar',
- },
- },
- req: {
- headers: {
- authorization: 'Basic dskd939k2i',
- },
- },
- },
- {
- b: 'remove',
- e: 'censor',
- authorization: '/([^\\s]+)$/',
- }
- );
-
- expect(data).toEqual({
- a: {
- d: {
- e: 'XXXXXX',
- },
- },
- req: {
- headers: {
- authorization: 'Basic XXXXXXXXXX',
- },
- },
- });
- });
-});
diff --git a/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.ts b/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.ts
deleted file mode 100644
index 578fa3a835129..0000000000000
--- a/packages/kbn-legacy-logging/src/utils/apply_filters_to_keys.ts
+++ /dev/null
@@ -1,50 +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.
- */
-
-function toPojo(obj: Record) {
- return JSON.parse(JSON.stringify(obj));
-}
-
-function replacer(match: string, group: any[]) {
- return new Array(group.length + 1).join('X');
-}
-
-function apply(obj: Record, key: string, action: string) {
- for (const k in obj) {
- if (obj.hasOwnProperty(k)) {
- let val = obj[k];
- if (k === key) {
- if (action === 'remove') {
- delete obj[k];
- } else if (action === 'censor' && typeof val === 'object') {
- delete obj[key];
- } else if (action === 'censor') {
- obj[k] = ('' + val).replace(/./g, 'X');
- } else if (/\/.+\//.test(action)) {
- const matches = action.match(/\/(.+)\//);
- if (matches) {
- const regex = new RegExp(matches[1]);
- obj[k] = ('' + val).replace(regex, replacer);
- }
- }
- } else if (typeof val === 'object') {
- val = apply(val as Record, key, action);
- }
- }
- }
- return obj;
-}
-
-export function applyFiltersToKeys(
- obj: Record,
- actionsByKey: Record
-) {
- return Object.keys(actionsByKey).reduce((output, key) => {
- return apply(output, key, actionsByKey[key]);
- }, toPojo(obj));
-}
diff --git a/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts b/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts
deleted file mode 100644
index 01d2cf29758db..0000000000000
--- a/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts
+++ /dev/null
@@ -1,158 +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 mockFs from 'mock-fs';
-import { createReadStream } from 'fs';
-import { PassThrough } from 'stream';
-import { createGzip, createGunzip } from 'zlib';
-
-import { getResponsePayloadBytes } from './get_payload_size';
-
-describe('getPayloadSize', () => {
- describe('handles Buffers', () => {
- test('with ascii characters', () => {
- const payload = 'heya';
- const result = getResponsePayloadBytes(Buffer.from(payload));
- expect(result).toBe(4);
- });
-
- test('with special characters', () => {
- const payload = '¡hola!';
- const result = getResponsePayloadBytes(Buffer.from(payload));
- expect(result).toBe(7);
- });
- });
-
- describe('handles streams', () => {
- afterEach(() => mockFs.restore());
-
- test('ignores streams that are not fs or zlib streams', async () => {
- const result = getResponsePayloadBytes(new PassThrough());
- expect(result).toBe(undefined);
- });
-
- describe('fs streams', () => {
- test('with ascii characters', async () => {
- mockFs({ 'test.txt': 'heya' });
- const readStream = createReadStream('test.txt');
-
- let data = '';
- for await (const chunk of readStream) {
- data += chunk;
- }
-
- const result = getResponsePayloadBytes(readStream);
- expect(result).toBe(Buffer.byteLength(data));
- });
-
- test('with special characters', async () => {
- mockFs({ 'test.txt': '¡hola!' });
- const readStream = createReadStream('test.txt');
-
- let data = '';
- for await (const chunk of readStream) {
- data += chunk;
- }
-
- const result = getResponsePayloadBytes(readStream);
- expect(result).toBe(Buffer.byteLength(data));
- });
-
- describe('zlib streams', () => {
- test('with ascii characters', async () => {
- mockFs({ 'test.txt': 'heya' });
- const readStream = createReadStream('test.txt');
- const source = readStream.pipe(createGzip()).pipe(createGunzip());
-
- let data = '';
- for await (const chunk of source) {
- data += chunk;
- }
-
- const result = getResponsePayloadBytes(source);
-
- expect(data).toBe('heya');
- expect(result).toBe(source.bytesWritten);
- });
-
- test('with special characters', async () => {
- mockFs({ 'test.txt': '¡hola!' });
- const readStream = createReadStream('test.txt');
- const source = readStream.pipe(createGzip()).pipe(createGunzip());
-
- let data = '';
- for await (const chunk of source) {
- data += chunk;
- }
-
- const result = getResponsePayloadBytes(source);
-
- expect(data).toBe('¡hola!');
- expect(result).toBe(source.bytesWritten);
- });
- });
- });
- });
-
- describe('handles plain responses', () => {
- test('when source is text', () => {
- const result = getResponsePayloadBytes('heya');
- expect(result).toBe(4);
- });
-
- test('when source contains special characters', () => {
- const result = getResponsePayloadBytes('¡hola!');
- expect(result).toBe(7);
- });
-
- test('when source is object', () => {
- const payload = { message: 'heya' };
- const result = getResponsePayloadBytes(payload);
- expect(result).toBe(JSON.stringify(payload).length);
- });
-
- test('when source is array object', () => {
- const payload = [{ message: 'hey' }, { message: 'ya' }];
- const result = getResponsePayloadBytes(payload);
- expect(result).toBe(JSON.stringify(payload).length);
- });
-
- test('returns undefined when source is not plain object', () => {
- class TestClass {
- constructor() {}
- }
- const result = getResponsePayloadBytes(new TestClass());
- expect(result).toBe(undefined);
- });
- });
-
- describe('handles content-length header', () => {
- test('always provides content-length header if available', () => {
- const headers = { 'content-length': '123' };
- const result = getResponsePayloadBytes('heya', headers);
- expect(result).toBe(123);
- });
-
- test('uses first value when hapi header is an array', () => {
- const headers = { 'content-length': ['123', '456'] };
- const result = getResponsePayloadBytes(null, headers);
- expect(result).toBe(123);
- });
-
- test('returns undefined if length is NaN', () => {
- const headers = { 'content-length': 'oops' };
- const result = getResponsePayloadBytes(null, headers);
- expect(result).toBeUndefined();
- });
- });
-
- test('defaults to undefined', () => {
- const result = getResponsePayloadBytes(null);
- expect(result).toBeUndefined();
- });
-});
diff --git a/packages/kbn-legacy-logging/src/utils/get_payload_size.ts b/packages/kbn-legacy-logging/src/utils/get_payload_size.ts
deleted file mode 100644
index acc517c74c2d4..0000000000000
--- a/packages/kbn-legacy-logging/src/utils/get_payload_size.ts
+++ /dev/null
@@ -1,71 +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 { isPlainObject } from 'lodash';
-import { ReadStream } from 'fs';
-import { Zlib } from 'zlib';
-import type { ResponseObject } from '@hapi/hapi';
-
-const isBuffer = (obj: unknown): obj is Buffer => Buffer.isBuffer(obj);
-const isFsReadStream = (obj: unknown): obj is ReadStream =>
- typeof obj === 'object' && obj !== null && 'bytesRead' in obj && obj instanceof ReadStream;
-const isZlibStream = (obj: unknown): obj is Zlib => {
- return typeof obj === 'object' && obj !== null && 'bytesWritten' in obj;
-};
-const isString = (obj: unknown): obj is string => typeof obj === 'string';
-
-/**
- * Attempts to determine the size (in bytes) of a hapi/good
- * responsePayload based on the payload type. Falls back to
- * `undefined` if the size cannot be determined.
- *
- * This is similar to the implementation in `core/server/http/logging`,
- * however it uses more duck typing as we do not have access to the
- * entire hapi request object like we do in the HttpServer.
- *
- * @param headers responseHeaders from hapi/good event
- * @param payload responsePayload from hapi/good event
- *
- * @internal
- */
-export function getResponsePayloadBytes(
- payload: ResponseObject['source'],
- headers: Record = {}
-): number | undefined {
- const contentLength = headers['content-length'];
- if (contentLength) {
- const val = parseInt(
- // hapi response headers can be `string | string[]`, so we need to handle both cases
- Array.isArray(contentLength) ? String(contentLength) : contentLength,
- 10
- );
- return !isNaN(val) ? val : undefined;
- }
-
- if (isBuffer(payload)) {
- return payload.byteLength;
- }
-
- if (isFsReadStream(payload)) {
- return payload.bytesRead;
- }
-
- if (isZlibStream(payload)) {
- return payload.bytesWritten;
- }
-
- if (isString(payload)) {
- return Buffer.byteLength(payload);
- }
-
- if (isPlainObject(payload) || Array.isArray(payload)) {
- return Buffer.byteLength(JSON.stringify(payload));
- }
-
- return undefined;
-}
diff --git a/packages/kbn-legacy-logging/src/utils/index.ts b/packages/kbn-legacy-logging/src/utils/index.ts
deleted file mode 100644
index 3036671121fe0..0000000000000
--- a/packages/kbn-legacy-logging/src/utils/index.ts
+++ /dev/null
@@ -1,10 +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.
- */
-
-export { applyFiltersToKeys } from './apply_filters_to_keys';
-export { getResponsePayloadBytes } from './get_payload_size';
diff --git a/packages/kbn-legacy-logging/tsconfig.json b/packages/kbn-legacy-logging/tsconfig.json
deleted file mode 100644
index 55047dbcadc91..0000000000000
--- a/packages/kbn-legacy-logging/tsconfig.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "extends": "../../tsconfig.bazel.json",
- "compilerOptions": {
- "declaration": true,
- "declarationMap": true,
- "emitDeclarationOnly": true,
- "outDir": "target_types",
- "rootDir": "src",
- "sourceMap": true,
- "sourceRoot": "../../../../packages/kbn-legacy-logging/src",
- "stripInternal": false,
- "types": ["jest", "node"]
- },
- "include": ["src/**/*"]
-}
diff --git a/src/cli/serve/integration_tests/__fixtures__/invalid_config.yml b/src/cli/serve/integration_tests/__fixtures__/invalid_config.yml
index df9ea641cd3fe..d8e59ced89c80 100644
--- a/src/cli/serve/integration_tests/__fixtures__/invalid_config.yml
+++ b/src/cli/serve/integration_tests/__fixtures__/invalid_config.yml
@@ -1,3 +1,13 @@
+logging:
+ root:
+ level: fatal
+ appenders: [console-json]
+ appenders:
+ console-json:
+ type: console
+ layout:
+ type: json
+
unknown:
key: 1
diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml
deleted file mode 100644
index 1761a7984e0e7..0000000000000
--- a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-server:
- autoListen: false
- port: 8274
-logging:
- json: true
-optimize:
- enabled: false
-plugins:
- initialize: false
-migrations:
- skip: true
-elasticsearch:
- skipStartupConnectionCheck: true
diff --git a/src/cli/serve/integration_tests/invalid_config.test.ts b/src/cli/serve/integration_tests/invalid_config.test.ts
index 724998699da85..2de902582a548 100644
--- a/src/cli/serve/integration_tests/invalid_config.test.ts
+++ b/src/cli/serve/integration_tests/invalid_config.test.ts
@@ -14,14 +14,15 @@ const INVALID_CONFIG_PATH = require.resolve('./__fixtures__/invalid_config.yml')
interface LogEntry {
message: string;
- tags?: string[];
- type: string;
+ log: {
+ level: string;
+ };
}
-describe('cli invalid config support', function () {
+describe('cli invalid config support', () => {
it(
- 'exits with statusCode 64 and logs a single line when config is invalid',
- function () {
+ 'exits with statusCode 64 and logs an error when config is invalid',
+ () => {
// Unused keys only throw once LegacyService starts, so disable migrations so that Core
// will finish the start lifecycle without a running Elasticsearch instance.
const { error, status, stdout, stderr } = spawnSync(
@@ -31,41 +32,27 @@ describe('cli invalid config support', function () {
cwd: REPO_ROOT,
}
);
+ expect(error).toBe(undefined);
- let fatalLogLine;
+ let fatalLogEntries;
try {
- [fatalLogLine] = stdout
+ fatalLogEntries = stdout
.toString('utf8')
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line) as LogEntry)
- .filter((line) => line.tags?.includes('fatal'))
- .map((obj) => ({
- ...obj,
- pid: '## PID ##',
- '@timestamp': '## @timestamp ##',
- error: '## Error with stack trace ##',
- }));
+ .filter((line) => line.log.level === 'FATAL');
} catch (e) {
throw new Error(
`error parsing log output:\n\n${e.stack}\n\nstdout: \n${stdout}\n\nstderr:\n${stderr}`
);
}
- expect(error).toBe(undefined);
-
- if (!fatalLogLine) {
- throw new Error(
- `cli did not log the expected fatal error message:\n\nstdout: \n${stdout}\n\nstderr:\n${stderr}`
- );
- }
-
- expect(fatalLogLine.message).toContain(
- 'Error: Unknown configuration key(s): "unknown.key", "other.unknown.key", "other.third", "some.flat.key", ' +
+ expect(fatalLogEntries).toHaveLength(1);
+ expect(fatalLogEntries[0].message).toContain(
+ 'Unknown configuration key(s): "unknown.key", "other.unknown.key", "other.third", "some.flat.key", ' +
'"some.array". Check for spelling errors and ensure that expected plugins are installed.'
);
- expect(fatalLogLine.tags).toEqual(['fatal', 'root']);
- expect(fatalLogLine.type).toEqual('log');
expect(status).toBe(64);
},
diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts
index 80ce52661565c..4cee7dfae4126 100644
--- a/src/cli/serve/integration_tests/reload_logging_config.test.ts
+++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts
@@ -17,7 +17,6 @@ import { map, filter, take } from 'rxjs/operators';
import { safeDump } from 'js-yaml';
import { getConfigFromFiles } from '@kbn/config';
-const legacyConfig = follow('__fixtures__/reload_logging_config/kibana.test.yml');
const configFileLogConsole = follow(
'__fixtures__/reload_logging_config/kibana_log_console.test.yml'
);
@@ -96,81 +95,6 @@ describe.skip('Server logging configuration', function () {
return;
}
- describe('legacy logging', () => {
- it(
- 'should be reloadable via SIGHUP process signaling',
- async function () {
- const configFilePath = Path.resolve(tempDir, 'kibana.yml');
- Fs.copyFileSync(legacyConfig, configFilePath);
-
- child = Child.spawn(process.execPath, [
- kibanaPath,
- '--oss',
- '--config',
- configFilePath,
- '--verbose',
- ]);
-
- // TypeScript note: As long as the child stdio[1] is 'pipe', then stdout will not be null
- const message$ = Rx.fromEvent(child.stdout!, 'data').pipe(
- map((messages) => String(messages).split('\n').filter(Boolean))
- );
-
- await message$
- .pipe(
- // We know the sighup handler will be registered before this message logged
- filter((messages: string[]) => messages.some((m) => m.includes('setting up root'))),
- take(1)
- )
- .toPromise();
-
- const lastMessage = await message$.pipe(take(1)).toPromise();
- expect(containsJsonOnly(lastMessage)).toBe(true);
-
- createConfigManager(configFilePath).modify((oldConfig) => {
- oldConfig.logging.json = false;
- return oldConfig;
- });
-
- child.kill('SIGHUP');
-
- await message$
- .pipe(
- filter((messages) => !containsJsonOnly(messages)),
- take(1)
- )
- .toPromise();
- },
- minute
- );
-
- it(
- 'should recreate file handle on SIGHUP',
- async function () {
- const logPath = Path.resolve(tempDir, 'kibana.log');
- const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log');
-
- child = Child.spawn(process.execPath, [
- kibanaPath,
- '--oss',
- '--config',
- legacyConfig,
- '--logging.dest',
- logPath,
- '--verbose',
- ]);
-
- await watchFileUntil(logPath, /setting up root/, 30 * second);
- // once the server is running, archive the log file and issue SIGHUP
- Fs.renameSync(logPath, logPathArchived);
- child.kill('SIGHUP');
-
- await watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 30 * second);
- },
- minute
- );
- });
-
describe('platform logging', () => {
it(
'should be reloadable via SIGHUP process signaling',
diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js
index 705acfe4fdf54..8b346d38cfea8 100644
--- a/src/cli/serve/serve.js
+++ b/src/cli/serve/serve.js
@@ -124,17 +124,12 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
if (opts.elasticsearch) set('elasticsearch.hosts', opts.elasticsearch.split(','));
if (opts.port) set('server.port', opts.port);
if (opts.host) set('server.host', opts.host);
+
if (opts.silent) {
- set('logging.silent', true);
set('logging.root.level', 'off');
}
if (opts.verbose) {
- if (has('logging.root.appenders')) {
- set('logging.root.level', 'all');
- } else {
- // Only set logging.verbose to true for legacy logging when KP logging isn't configured.
- set('logging.verbose', true);
- }
+ set('logging.root.level', 'all');
}
set('plugins.paths', _.compact([].concat(get('plugins.paths'), opts.pluginPath)));
@@ -159,9 +154,8 @@ export default function (program) {
[getConfigPath()]
)
.option('-p, --port ', 'The port to bind to', parseInt)
- .option('-q, --quiet', 'Deprecated, set logging level in your configuration')
- .option('-Q, --silent', 'Prevent all logging')
- .option('--verbose', 'Turns on verbose logging')
+ .option('-Q, --silent', 'Set the root logger level to off')
+ .option('--verbose', 'Set the root logger level to all')
.option('-H, --host ', 'The host to bind to')
.option(
'-l, --log-file ',
@@ -217,8 +211,6 @@ export default function (program) {
const cliArgs = {
dev: !!opts.dev,
envName: unknownOptions.env ? unknownOptions.env.name : undefined,
- // no longer supported
- quiet: !!opts.quiet,
silent: !!opts.silent,
verbose: !!opts.verbose,
watch: !!opts.watch,
diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts
index d3a4d7f997062..4e99f46ea05ff 100644
--- a/src/core/server/config/deprecation/core_deprecations.test.ts
+++ b/src/core/server/config/deprecation/core_deprecations.test.ts
@@ -8,6 +8,7 @@
import { getDeprecationsForGlobalSettings } from '../test_utils';
import { coreDeprecationProvider } from './core_deprecations';
+
const initialEnv = { ...process.env };
const applyCoreDeprecations = (settings?: Record) =>
@@ -203,230 +204,4 @@ describe('core deprecations', () => {
).toEqual([`worker-src blob:`]);
});
});
-
- describe('logging.events.ops', () => {
- it('warns when ops events are used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { events: { ops: '*' } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.events.ops\\" has been deprecated and will be removed in 8.0. To access ops data moving forward, please enable debug logs for the \\"metrics.ops\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx",
- ]
- `);
- });
- });
-
- describe('logging.events.request and logging.events.response', () => {
- it('warns when request and response events are used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { events: { request: '*', response: '*' } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx",
- ]
- `);
- });
-
- it('warns when only request event is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { events: { request: '*' } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx",
- ]
- `);
- });
-
- it('warns when only response event is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { events: { response: '*' } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.events.request\\" and \\"logging.events.response\\" have been deprecated and will be removed in 8.0. To access request and/or response data moving forward, please enable debug logs for the \\"http.server.response\\" context in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx",
- ]
- `);
- });
- });
-
- describe('logging.timezone', () => {
- it('warns when ops events are used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { timezone: 'GMT' },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.timezone\\" has been deprecated and will be removed in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx",
- ]
- `);
- });
- });
-
- describe('logging.dest', () => {
- it('warns when dest is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { dest: 'stdout' },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx",
- ]
- `);
- });
- it('warns when dest path is given', () => {
- const { messages } = applyCoreDeprecations({
- logging: { dest: '/log-log.txt' },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx",
- ]
- `);
- });
- });
-
- describe('logging.quiet, logging.silent and logging.verbose', () => {
- it('warns when quiet is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { quiet: true },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.quiet\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:error\\" in your logging configuration. ",
- ]
- `);
- });
- it('warns when silent is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { silent: true },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.silent\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:off\\" in your logging configuration. ",
- ]
- `);
- });
- it('warns when verbose is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { verbose: true },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.verbose\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:all\\" in your logging configuration. ",
- ]
- `);
- });
- });
-
- describe('logging.json', () => {
- it('warns when json is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { json: true },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.json\\" has been deprecated and will be removed in 8.0. To specify log message format moving forward, you can configure the \\"appender.layout\\" property for every custom appender in your logging configuration. There is currently no default layout for custom appenders and each one must be declared explicitly. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx",
- ]
- `);
- });
- });
-
- describe('logging.rotate.enabled, logging.rotate.usePolling, logging.rotate.pollingInterval, logging.rotate.everyBytes and logging.rotate.keepFiles', () => {
- it('warns when logging.rotate configurations are used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { rotate: { enabled: true } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender",
- ]
- `);
- });
-
- it('warns when logging.rotate polling configurations are used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { rotate: { enabled: true, usePolling: true, pollingInterval: 5000 } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender",
- ]
- `);
- });
-
- it('warns when logging.rotate.everyBytes configurations are used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { rotate: { enabled: true, everyBytes: 1048576 } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender",
- ]
- `);
- });
-
- it('warns when logging.rotate.keepFiles is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { rotate: { enabled: true, keepFiles: 1024 } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.rotate\\" and sub-options have been deprecated and will be removed in 8.0. Moving forward, you can enable log rotation using the \\"rolling-file\\" appender for a logger in your logging configuration. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender",
- ]
- `);
- });
- });
-
- describe('logging.events.log', () => {
- it('warns when events.log is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { events: { log: ['info'] } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.events.log\\" has been deprecated and will be removed in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration.",
- ]
- `);
- });
- });
-
- describe('logging.events.error', () => {
- it('warns when events.error is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { events: { error: ['some error'] } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.events.error\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level: error\\" in your logging configuration.",
- ]
- `);
- });
- });
-
- describe('logging.filter', () => {
- it('warns when filter.cookie is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { filter: { cookie: 'none' } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.filter\\" has been deprecated and will be removed in 8.0.",
- ]
- `);
- });
-
- it('warns when filter.authorization is used', () => {
- const { messages } = applyCoreDeprecations({
- logging: { filter: { authorization: 'none' } },
- });
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "\\"logging.filter\\" has been deprecated and will be removed in 8.0.",
- ]
- `);
- });
- });
});
diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts
index 6e7365d0d5cbf..674812bd0957b 100644
--- a/src/core/server/config/deprecation/core_deprecations.ts
+++ b/src/core/server/config/deprecation/core_deprecations.ts
@@ -113,245 +113,6 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati
}
};
-const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.events?.ops) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents',
- message:
- '"logging.events.ops" has been deprecated and will be removed ' +
- 'in 8.0. To access ops data moving forward, please enable debug logs for the ' +
- '"metrics.ops" context in your logging configuration. For more details, see ' +
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.events.ops" from your kibana settings.`,
- `Enable debug logs for the "metrics.ops" context in your logging configuration`,
- ],
- },
- });
- }
-};
-
-const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.events?.request || settings.logging?.events?.response) {
- const removeConfigsSteps = [];
-
- if (settings.logging?.events?.request) {
- removeConfigsSteps.push(`Remove "logging.events.request" from your kibana configs.`);
- }
-
- if (settings.logging?.events?.response) {
- removeConfigsSteps.push(`Remove "logging.events.response" from your kibana configs.`);
- }
-
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents',
- message:
- '"logging.events.request" and "logging.events.response" have been deprecated and will be removed ' +
- 'in 8.0. To access request and/or response data moving forward, please enable debug logs for the ' +
- '"http.server.response" context in your logging configuration. For more details, see ' +
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx',
- correctiveActions: {
- manualSteps: [
- ...removeConfigsSteps,
- `enable debug logs for the "http.server.response" context in your logging configuration.`,
- ],
- },
- });
- }
-};
-
-const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.timezone) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingtimezone',
- message:
- '"logging.timezone" has been deprecated and will be removed ' +
- 'in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern ' +
- 'in your logging configuration. For more details, see ' +
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.timezone" from your kibana configs.`,
- `To set the timezone add a timezone date modifier to the log pattern in your logging configuration.`,
- ],
- },
- });
- }
-};
-
-const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.dest) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingdest',
- message:
- '"logging.dest" has been deprecated and will be removed ' +
- 'in 8.0. To set the destination moving forward, you can use the "console" appender ' +
- 'in your logging configuration or define a custom one. For more details, see ' +
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.dest" from your kibana configs.`,
- `To set the destination use the "console" appender in your logging configuration or define a custom one.`,
- ],
- },
- });
- }
-};
-
-const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.quiet) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingquiet',
- message:
- '"logging.quiet" has been deprecated and will be removed ' +
- 'in 8.0. Moving forward, you can use "logging.root.level:error" in your logging configuration. ',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.quiet" from your kibana configs.`,
- `Use "logging.root.level:error" in your logging configuration.`,
- ],
- },
- });
- }
-};
-
-const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.silent) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingsilent',
- message:
- '"logging.silent" has been deprecated and will be removed ' +
- 'in 8.0. Moving forward, you can use "logging.root.level:off" in your logging configuration. ',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.silent" from your kibana configs.`,
- `Use "logging.root.level:off" in your logging configuration.`,
- ],
- },
- });
- }
-};
-
-const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.verbose) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingverbose',
- message:
- '"logging.verbose" has been deprecated and will be removed ' +
- 'in 8.0. Moving forward, you can use "logging.root.level:all" in your logging configuration. ',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.verbose" from your kibana configs.`,
- `Use "logging.root.level:all" in your logging configuration.`,
- ],
- },
- });
- }
-};
-
-const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- // We silence the deprecation warning when running in development mode because
- // the dev CLI code in src/dev/cli_dev_mode/using_server_process.ts manually
- // specifies `--logging.json=false`. Since it's executed in a child process, the
- // ` legacyLoggingConfigSchema` returns `true` for the TTY check on `process.stdout.isTTY`
- if (settings.logging?.json && settings.env !== 'development') {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx',
- message:
- '"logging.json" has been deprecated and will be removed ' +
- 'in 8.0. To specify log message format moving forward, ' +
- 'you can configure the "appender.layout" property for every custom appender in your logging configuration. ' +
- 'There is currently no default layout for custom appenders and each one must be declared explicitly. ' +
- 'For more details, see ' +
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.json" from your kibana configs.`,
- `Configure the "appender.layout" property for every custom appender in your logging configuration.`,
- ],
- },
- });
- }
-};
-
-const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.rotate) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender',
- message:
- '"logging.rotate" and sub-options have been deprecated and will be removed in 8.0. ' +
- 'Moving forward, you can enable log rotation using the "rolling-file" appender for a logger ' +
- 'in your logging configuration. For more details, see ' +
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.rotate" from your kibana configs.`,
- `Enable log rotation using the "rolling-file" appender for a logger in your logging configuration.`,
- ],
- },
- });
- }
-};
-
-const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.events?.log) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents',
- message:
- '"logging.events.log" has been deprecated and will be removed ' +
- 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration.',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.events.log" from your kibana configs.`,
- `Customize log levels can be per-logger using the new logging configuration.`,
- ],
- },
- });
- }
-};
-
-const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.events?.error) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents',
- message:
- '"logging.events.error" has been deprecated and will be removed ' +
- 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration.',
- correctiveActions: {
- manualSteps: [
- `Remove "logging.events.error" from your kibana configs.`,
- `Use "logging.root.level: error" in your logging configuration.`,
- ],
- },
- });
- }
-};
-
-const logFilterDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (settings.logging?.filter) {
- addDeprecation({
- documentationUrl:
- 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingfilter',
- message: '"logging.filter" has been deprecated and will be removed in 8.0.',
- correctiveActions: {
- manualSteps: [`Remove "logging.filter" from your kibana configs.`],
- },
- });
- }
-};
-
export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [
rename('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'),
rename('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'),
@@ -360,16 +121,4 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unu
kibanaPathConf,
rewriteBasePathDeprecation,
cspRulesDeprecation,
- opsLoggingEventDeprecation,
- requestLoggingEventDeprecation,
- timezoneLoggingDeprecation,
- destLoggingDeprecation,
- quietLoggingDeprecation,
- silentLoggingDeprecation,
- verboseLoggingDeprecation,
- jsonLoggingDeprecation,
- logRotateDeprecation,
- logEventsLogDeprecation,
- logEventsErrorDeprecation,
- logFilterDeprecation,
];
diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts
index 686564c6d678a..7254dd5222b84 100644
--- a/src/core/server/config/index.ts
+++ b/src/core/server/config/index.ts
@@ -30,5 +30,4 @@ export type {
ConfigDeprecationFactory,
EnvironmentMode,
PackageInfo,
- LegacyObjectToConfigAdapter,
} from '@kbn/config';
diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts
index 0138c6e7ef154..5036fa4742b59 100644
--- a/src/core/server/config/integration_tests/config_deprecation.test.ts
+++ b/src/core/server/config/integration_tests/config_deprecation.test.ts
@@ -23,17 +23,13 @@ describe('configuration deprecations', () => {
}
});
- it('should not log deprecation warnings for default configuration that is not one of `logging.verbose`, `logging.quiet` or `logging.silent`', async () => {
+ it('should not log deprecation warnings for default configuration', async () => {
root = kbnTestServer.createRoot();
await root.preboot();
await root.setup();
const logs = loggingSystemMock.collect(mockLoggingSystem);
- expect(logs.warn.flat()).toMatchInlineSnapshot(`
- Array [
- "\\"logging.silent\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:off\\" in your logging configuration. ",
- ]
- `);
+ expect(logs.warn.flat()).toHaveLength(0);
});
});
diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts
index 995b3ffbd947d..7470ff7081717 100644
--- a/src/core/server/elasticsearch/elasticsearch_config.ts
+++ b/src/core/server/elasticsearch/elasticsearch_config.ts
@@ -211,7 +211,7 @@ const deprecations: ConfigDeprecationProvider = () => [
});
} else if (es.logQueries === true) {
addDeprecation({
- message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".`,
+ message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers".`,
correctiveActions: {
manualSteps: [
`Remove Setting [${fromPath}.logQueries] from your kibana configs`,
diff --git a/src/core/server/http/integration_tests/logging.test.ts b/src/core/server/http/integration_tests/logging.test.ts
index 12d555a240cde..20e0175d4b19d 100644
--- a/src/core/server/http/integration_tests/logging.test.ts
+++ b/src/core/server/http/integration_tests/logging.test.ts
@@ -51,7 +51,6 @@ describe('request logging', () => {
it('logs at the correct level and with the correct context', async () => {
const root = kbnTestServer.createRoot({
logging: {
- silent: true,
appenders: {
'test-console': {
type: 'console',
@@ -99,7 +98,6 @@ describe('request logging', () => {
let root: ReturnType;
const config = {
logging: {
- silent: true,
appenders: {
'test-console': {
type: 'console',
@@ -300,7 +298,6 @@ describe('request logging', () => {
it('filters sensitive request headers when RewriteAppender is configured', async () => {
root = kbnTestServer.createRoot({
logging: {
- silent: true,
appenders: {
'test-console': {
type: 'console',
@@ -402,7 +399,6 @@ describe('request logging', () => {
it('filters sensitive response headers when RewriteAppender is configured', async () => {
root = kbnTestServer.createRoot({
logging: {
- silent: true,
appenders: {
'test-console': {
type: 'console',
diff --git a/src/core/server/legacy/index.ts b/src/core/server/legacy/index.ts
deleted file mode 100644
index 39ffef501a9ec..0000000000000
--- a/src/core/server/legacy/index.ts
+++ /dev/null
@@ -1,11 +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.
- */
-
-/** @internal */
-export type { ILegacyService } from './legacy_service';
-export { LegacyService } from './legacy_service';
diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts
deleted file mode 100644
index a79e434ce4576..0000000000000
--- a/src/core/server/legacy/integration_tests/logging.test.ts
+++ /dev/null
@@ -1,234 +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 { LegacyLoggingConfig } from '@kbn/config';
-import * as kbnTestServer from '../../../test_helpers/kbn_server';
-
-import {
- getPlatformLogsFromMock,
- getLegacyPlatformLogsFromMock,
-} from '../../logging/integration_tests/utils';
-
-function createRoot(legacyLoggingConfig: LegacyLoggingConfig = {}) {
- return kbnTestServer.createRoot({
- migrations: { skip: true }, // otherwise stuck in polling ES
- plugins: { initialize: false },
- elasticsearch: { skipStartupConnectionCheck: true },
- logging: {
- // legacy platform config
- silent: false,
- json: false,
- ...legacyLoggingConfig,
- events: {
- log: ['test-file-legacy'],
- },
- // platform config
- appenders: {
- 'test-console': {
- type: 'console',
- layout: {
- highlight: false,
- type: 'pattern',
- },
- },
- },
- loggers: [
- {
- name: 'test-file',
- appenders: ['test-console'],
- level: 'info',
- },
- ],
- },
- });
-}
-
-describe('logging service', () => {
- let mockConsoleLog: jest.SpyInstance;
- let mockStdout: jest.SpyInstance;
-
- beforeAll(async () => {
- mockConsoleLog = jest.spyOn(global.console, 'log');
- mockStdout = jest.spyOn(global.process.stdout, 'write');
- });
-
- afterAll(async () => {
- mockConsoleLog.mockRestore();
- mockStdout.mockRestore();
- });
-
- describe('compatibility', () => {
- describe('uses configured loggers', () => {
- let root: ReturnType;
- beforeAll(async () => {
- root = createRoot();
-
- await root.preboot();
- await root.setup();
- await root.start();
- }, 30000);
-
- afterAll(async () => {
- await root.shutdown();
- });
-
- beforeEach(() => {
- mockConsoleLog.mockClear();
- mockStdout.mockClear();
- });
-
- it('when context matches', async () => {
- root.logger.get('test-file').info('handled by NP');
-
- expect(mockConsoleLog).toHaveBeenCalledTimes(1);
- const loggedString = getPlatformLogsFromMock(mockConsoleLog);
- expect(loggedString).toMatchInlineSnapshot(`
- Array [
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] handled by NP",
- ]
- `);
- });
-
- it('falls back to the root legacy logger otherwise', async () => {
- root.logger.get('test-file-legacy').info('handled by LP');
-
- expect(mockStdout).toHaveBeenCalledTimes(1);
-
- const loggedString = getLegacyPlatformLogsFromMock(mockStdout);
- expect(loggedString).toMatchInlineSnapshot(`
- Array [
- " log [xx:xx:xx.xxx] [info][test-file-legacy] handled by LP
- ",
- ]
- `);
- });
- });
-
- describe('logging config respects legacy logging settings', () => {
- let root: ReturnType;
-
- afterEach(async () => {
- mockConsoleLog.mockClear();
- mockStdout.mockClear();
- await root.shutdown();
- });
-
- it('"silent": true', async () => {
- root = createRoot({ silent: true });
-
- await root.preboot();
- await root.setup();
- await root.start();
-
- const platformLogger = root.logger.get('test-file');
- platformLogger.info('info');
- platformLogger.warn('warn');
- platformLogger.error('error');
-
- expect(mockConsoleLog).toHaveBeenCalledTimes(3);
-
- expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(`
- Array [
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info",
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn",
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error",
- ]
- `);
-
- mockStdout.mockClear();
-
- const legacyPlatformLogger = root.logger.get('test-file-legacy');
- legacyPlatformLogger.info('info');
- legacyPlatformLogger.warn('warn');
- legacyPlatformLogger.error('error');
-
- expect(mockStdout).toHaveBeenCalledTimes(0);
- });
-
- it('"quiet": true', async () => {
- root = createRoot({ quiet: true });
-
- await root.preboot();
- await root.setup();
- await root.start();
-
- const platformLogger = root.logger.get('test-file');
- platformLogger.info('info');
- platformLogger.warn('warn');
- platformLogger.error('error');
-
- expect(mockConsoleLog).toHaveBeenCalledTimes(3);
-
- expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(`
- Array [
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info",
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn",
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error",
- ]
- `);
-
- mockStdout.mockClear();
-
- const legacyPlatformLogger = root.logger.get('test-file-legacy');
- legacyPlatformLogger.info('info');
- legacyPlatformLogger.warn('warn');
- legacyPlatformLogger.error('error');
-
- expect(mockStdout).toHaveBeenCalledTimes(1);
- expect(getLegacyPlatformLogsFromMock(mockStdout)).toMatchInlineSnapshot(`
- Array [
- " log [xx:xx:xx.xxx] [error][test-file-legacy] error
- ",
- ]
- `);
- });
-
- it('"verbose": true', async () => {
- root = createRoot({ verbose: true });
-
- await root.preboot();
- await root.setup();
- await root.start();
-
- const platformLogger = root.logger.get('test-file');
- platformLogger.info('info');
- platformLogger.warn('warn');
- platformLogger.error('error');
-
- expect(mockConsoleLog).toHaveBeenCalledTimes(3);
-
- expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(`
- Array [
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][INFO ][test-file] info",
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][WARN ][test-file] warn",
- "[xxxx-xx-xxTxx:xx:xx.xxx-xx:xx][ERROR][test-file] error",
- ]
- `);
-
- mockStdout.mockClear();
-
- const legacyPlatformLogger = root.logger.get('test-file-legacy');
- legacyPlatformLogger.info('info');
- legacyPlatformLogger.warn('warn');
- legacyPlatformLogger.error('error');
-
- expect(mockStdout).toHaveBeenCalledTimes(3);
- expect(getLegacyPlatformLogsFromMock(mockStdout)).toMatchInlineSnapshot(`
- Array [
- " log [xx:xx:xx.xxx] [info][test-file-legacy] info
- ",
- " log [xx:xx:xx.xxx] [warning][test-file-legacy] warn
- ",
- " log [xx:xx:xx.xxx] [error][test-file-legacy] error
- ",
- ]
- `);
- });
- });
- });
-});
diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts
deleted file mode 100644
index 0d72318a630e0..0000000000000
--- a/src/core/server/legacy/legacy_service.mock.ts
+++ /dev/null
@@ -1,21 +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 type { PublicMethodsOf } from '@kbn/utility-types';
-import { LegacyService } from './legacy_service';
-
-type LegacyServiceMock = jest.Mocked>;
-
-const createLegacyServiceMock = (): LegacyServiceMock => ({
- setup: jest.fn(),
- stop: jest.fn(),
-});
-
-export const legacyServiceMock = {
- create: createLegacyServiceMock,
-};
diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts
deleted file mode 100644
index 506f0fd6f96d3..0000000000000
--- a/src/core/server/legacy/legacy_service.test.mocks.ts
+++ /dev/null
@@ -1,18 +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.
- */
-
-export const reconfigureLoggingMock = jest.fn();
-export const setupLoggingMock = jest.fn();
-export const setupLoggingRotateMock = jest.fn();
-
-jest.doMock('@kbn/legacy-logging', () => ({
- ...(jest.requireActual('@kbn/legacy-logging') as any),
- reconfigureLogging: reconfigureLoggingMock,
- setupLogging: setupLoggingMock,
- setupLoggingRotate: setupLoggingRotateMock,
-}));
diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts
deleted file mode 100644
index 6b20bd7434baf..0000000000000
--- a/src/core/server/legacy/legacy_service.test.ts
+++ /dev/null
@@ -1,197 +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 {
- setupLoggingMock,
- setupLoggingRotateMock,
- reconfigureLoggingMock,
-} from './legacy_service.test.mocks';
-
-import { BehaviorSubject } from 'rxjs';
-import moment from 'moment';
-import { REPO_ROOT } from '@kbn/dev-utils';
-
-import { Config, Env, ObjectToConfigAdapter } from '../config';
-
-import { getEnvOptions, configServiceMock } from '../config/mocks';
-import { loggingSystemMock } from '../logging/logging_system.mock';
-import { httpServiceMock } from '../http/http_service.mock';
-import { LegacyService, LegacyServiceSetupDeps } from './legacy_service';
-
-let coreId: symbol;
-let env: Env;
-let config$: BehaviorSubject;
-
-let setupDeps: LegacyServiceSetupDeps;
-
-const logger = loggingSystemMock.create();
-let configService: ReturnType;
-
-beforeEach(() => {
- coreId = Symbol();
- env = Env.createDefault(REPO_ROOT, getEnvOptions());
- configService = configServiceMock.create();
-
- setupDeps = {
- http: httpServiceMock.createInternalSetupContract(),
- };
-
- config$ = new BehaviorSubject(
- new ObjectToConfigAdapter({
- elasticsearch: { hosts: ['http://127.0.0.1'] },
- server: { autoListen: true },
- })
- );
-
- configService.getConfig$.mockReturnValue(config$);
-});
-
-afterEach(() => {
- jest.clearAllMocks();
- setupLoggingMock.mockReset();
- setupLoggingRotateMock.mockReset();
- reconfigureLoggingMock.mockReset();
-});
-
-describe('#setup', () => {
- it('initializes legacy logging', async () => {
- const opsConfig = {
- interval: moment.duration(5, 'second'),
- };
- const opsConfig$ = new BehaviorSubject(opsConfig);
-
- const loggingConfig = {
- foo: 'bar',
- };
- const loggingConfig$ = new BehaviorSubject(loggingConfig);
-
- configService.atPath.mockImplementation((path) => {
- if (path === 'ops') {
- return opsConfig$;
- }
- if (path === 'logging') {
- return loggingConfig$;
- }
- return new BehaviorSubject({});
- });
-
- const legacyService = new LegacyService({
- coreId,
- env,
- logger,
- configService: configService as any,
- });
-
- await legacyService.setup(setupDeps);
-
- expect(setupLoggingMock).toHaveBeenCalledTimes(1);
- expect(setupLoggingMock).toHaveBeenCalledWith(
- setupDeps.http.server,
- loggingConfig,
- opsConfig.interval.asMilliseconds()
- );
-
- expect(setupLoggingRotateMock).toHaveBeenCalledTimes(1);
- expect(setupLoggingRotateMock).toHaveBeenCalledWith(setupDeps.http.server, loggingConfig);
- });
-
- it('reloads the logging config when the config changes', async () => {
- const opsConfig = {
- interval: moment.duration(5, 'second'),
- };
- const opsConfig$ = new BehaviorSubject(opsConfig);
-
- const loggingConfig = {
- foo: 'bar',
- };
- const loggingConfig$ = new BehaviorSubject(loggingConfig);
-
- configService.atPath.mockImplementation((path) => {
- if (path === 'ops') {
- return opsConfig$;
- }
- if (path === 'logging') {
- return loggingConfig$;
- }
- return new BehaviorSubject({});
- });
-
- const legacyService = new LegacyService({
- coreId,
- env,
- logger,
- configService: configService as any,
- });
-
- await legacyService.setup(setupDeps);
-
- expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1);
- expect(reconfigureLoggingMock).toHaveBeenCalledWith(
- setupDeps.http.server,
- loggingConfig,
- opsConfig.interval.asMilliseconds()
- );
-
- loggingConfig$.next({
- foo: 'changed',
- });
-
- expect(reconfigureLoggingMock).toHaveBeenCalledTimes(2);
- expect(reconfigureLoggingMock).toHaveBeenCalledWith(
- setupDeps.http.server,
- { foo: 'changed' },
- opsConfig.interval.asMilliseconds()
- );
- });
-
- it('stops reloading logging config once the service is stopped', async () => {
- const opsConfig = {
- interval: moment.duration(5, 'second'),
- };
- const opsConfig$ = new BehaviorSubject(opsConfig);
-
- const loggingConfig = {
- foo: 'bar',
- };
- const loggingConfig$ = new BehaviorSubject(loggingConfig);
-
- configService.atPath.mockImplementation((path) => {
- if (path === 'ops') {
- return opsConfig$;
- }
- if (path === 'logging') {
- return loggingConfig$;
- }
- return new BehaviorSubject({});
- });
-
- const legacyService = new LegacyService({
- coreId,
- env,
- logger,
- configService: configService as any,
- });
-
- await legacyService.setup(setupDeps);
-
- expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1);
- expect(reconfigureLoggingMock).toHaveBeenCalledWith(
- setupDeps.http.server,
- loggingConfig,
- opsConfig.interval.asMilliseconds()
- );
-
- await legacyService.stop();
-
- loggingConfig$.next({
- foo: 'changed',
- });
-
- expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
deleted file mode 100644
index 1d5343ff5311d..0000000000000
--- a/src/core/server/legacy/legacy_service.ts
+++ /dev/null
@@ -1,75 +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 { combineLatest, Observable, Subscription } from 'rxjs';
-import { first } from 'rxjs/operators';
-import { Server } from '@hapi/hapi';
-import type { PublicMethodsOf } from '@kbn/utility-types';
-import {
- reconfigureLogging,
- setupLogging,
- setupLoggingRotate,
- LegacyLoggingConfig,
-} from '@kbn/legacy-logging';
-
-import { CoreContext } from '../core_context';
-import { config as loggingConfig } from '../logging';
-import { opsConfig, OpsConfigType } from '../metrics';
-import { Logger } from '../logging';
-import { InternalHttpServiceSetup } from '../http';
-
-export interface LegacyServiceSetupDeps {
- http: InternalHttpServiceSetup;
-}
-
-/** @internal */
-export type ILegacyService = PublicMethodsOf;
-
-/** @internal */
-export class LegacyService {
- private readonly log: Logger;
- private readonly opsConfig$: Observable;
- private readonly legacyLoggingConfig$: Observable;
- private configSubscription?: Subscription;
-
- constructor(coreContext: CoreContext) {
- const { logger, configService } = coreContext;
-
- this.log = logger.get('legacy-service');
- this.legacyLoggingConfig$ = configService.atPath(loggingConfig.path);
- this.opsConfig$ = configService.atPath(opsConfig.path);
- }
-
- public async setup(setupDeps: LegacyServiceSetupDeps) {
- this.log.debug('setting up legacy service');
- await this.setupLegacyLogging(setupDeps.http.server);
- }
-
- private async setupLegacyLogging(server: Server) {
- const legacyLoggingConfig = await this.legacyLoggingConfig$.pipe(first()).toPromise();
- const currentOpsConfig = await this.opsConfig$.pipe(first()).toPromise();
-
- await setupLogging(server, legacyLoggingConfig, currentOpsConfig.interval.asMilliseconds());
- await setupLoggingRotate(server, legacyLoggingConfig);
-
- this.configSubscription = combineLatest([this.legacyLoggingConfig$, this.opsConfig$]).subscribe(
- ([newLoggingConfig, newOpsConfig]) => {
- reconfigureLogging(server, newLoggingConfig, newOpsConfig.interval.asMilliseconds());
- }
- );
- }
-
- public async stop() {
- this.log.debug('stopping legacy service');
-
- if (this.configSubscription !== undefined) {
- this.configSubscription.unsubscribe();
- this.configSubscription = undefined;
- }
- }
-}
diff --git a/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap b/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap
deleted file mode 100644
index 3c40362e8211e..0000000000000
--- a/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap
+++ /dev/null
@@ -1,142 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`\`append()\` correctly pushes records to legacy platform. 1`] = `
-Object {
- "context": "context-1",
- "level": LogLevel {
- "id": "trace",
- "value": 7,
- },
- "message": "message-1",
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
-
-exports[`\`append()\` correctly pushes records to legacy platform. 2`] = `
-Object {
- "context": "context-2",
- "level": LogLevel {
- "id": "debug",
- "value": 6,
- },
- "message": "message-2",
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
-
-exports[`\`append()\` correctly pushes records to legacy platform. 3`] = `
-Object {
- "context": "context-3.sub-context-3",
- "level": LogLevel {
- "id": "info",
- "value": 5,
- },
- "message": "message-3",
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
-
-exports[`\`append()\` correctly pushes records to legacy platform. 4`] = `
-Object {
- "context": "context-4.sub-context-4",
- "level": LogLevel {
- "id": "warn",
- "value": 4,
- },
- "message": "message-4",
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
-
-exports[`\`append()\` correctly pushes records to legacy platform. 5`] = `
-Object {
- "context": "context-5",
- "error": [Error: Some Error],
- "level": LogLevel {
- "id": "error",
- "value": 3,
- },
- "message": "message-5-with-error",
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
-
-exports[`\`append()\` correctly pushes records to legacy platform. 6`] = `
-Object {
- "context": "context-6",
- "level": LogLevel {
- "id": "error",
- "value": 3,
- },
- "message": "message-6-with-message",
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
-
-exports[`\`append()\` correctly pushes records to legacy platform. 7`] = `
-Object {
- "context": "context-7.sub-context-7.sub-sub-context-7",
- "error": [Error: Some Fatal Error],
- "level": LogLevel {
- "id": "fatal",
- "value": 2,
- },
- "message": "message-7-with-error",
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
-
-exports[`\`append()\` correctly pushes records to legacy platform. 8`] = `
-Object {
- "context": "context-8.sub-context-8.sub-sub-context-8",
- "level": LogLevel {
- "id": "fatal",
- "value": 2,
- },
- "message": "message-8-with-message",
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
-
-exports[`\`append()\` correctly pushes records to legacy platform. 9`] = `
-Object {
- "context": "context-9.sub-context-9",
- "level": LogLevel {
- "id": "info",
- "value": 5,
- },
- "message": "message-9-with-message",
- "meta": Object {
- "someValue": 3,
- },
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
-
-exports[`\`append()\` correctly pushes records to legacy platform. 10`] = `
-Object {
- "context": "context-10.sub-context-10",
- "level": LogLevel {
- "id": "info",
- "value": 5,
- },
- "message": "message-10-with-message",
- "meta": Object {
- "tags": Array [
- "tag1",
- "tag2",
- ],
- },
- "pid": Any,
- "timestamp": 2012-02-01T11:22:33.044Z,
-}
-`;
diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts
deleted file mode 100644
index 9213403d72d07..0000000000000
--- a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts
+++ /dev/null
@@ -1,135 +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.
- */
-
-jest.mock('@kbn/legacy-logging');
-
-import { LogRecord, LogLevel } from '../../../logging';
-import { LegacyLoggingServer } from '@kbn/legacy-logging';
-import { LegacyAppender } from './legacy_appender';
-
-afterEach(() => (LegacyLoggingServer as any).mockClear());
-
-test('`configSchema` creates correct schema.', () => {
- const appenderSchema = LegacyAppender.configSchema;
- const validConfig = { type: 'legacy-appender', legacyLoggingConfig: { verbose: true } };
- expect(appenderSchema.validate(validConfig)).toEqual({
- type: 'legacy-appender',
- legacyLoggingConfig: { verbose: true },
- });
-
- const wrongConfig = { type: 'not-legacy-appender' };
- expect(() => appenderSchema.validate(wrongConfig)).toThrow();
-});
-
-test('`append()` correctly pushes records to legacy platform.', () => {
- const timestamp = new Date(Date.UTC(2012, 1, 1, 11, 22, 33, 44));
- const records: LogRecord[] = [
- {
- context: 'context-1',
- level: LogLevel.Trace,
- message: 'message-1',
- timestamp,
- pid: 5355,
- },
- {
- context: 'context-2',
- level: LogLevel.Debug,
- message: 'message-2',
- timestamp,
- pid: 5355,
- },
- {
- context: 'context-3.sub-context-3',
- level: LogLevel.Info,
- message: 'message-3',
- timestamp,
- pid: 5355,
- },
- {
- context: 'context-4.sub-context-4',
- level: LogLevel.Warn,
- message: 'message-4',
- timestamp,
- pid: 5355,
- },
- {
- context: 'context-5',
- error: new Error('Some Error'),
- level: LogLevel.Error,
- message: 'message-5-with-error',
- timestamp,
- pid: 5355,
- },
- {
- context: 'context-6',
- level: LogLevel.Error,
- message: 'message-6-with-message',
- timestamp,
- pid: 5355,
- },
- {
- context: 'context-7.sub-context-7.sub-sub-context-7',
- error: new Error('Some Fatal Error'),
- level: LogLevel.Fatal,
- message: 'message-7-with-error',
- timestamp,
- pid: 5355,
- },
- {
- context: 'context-8.sub-context-8.sub-sub-context-8',
- level: LogLevel.Fatal,
- message: 'message-8-with-message',
- timestamp,
- pid: 5355,
- },
- {
- context: 'context-9.sub-context-9',
- level: LogLevel.Info,
- message: 'message-9-with-message',
- timestamp,
- pid: 5355,
- meta: { someValue: 3 },
- },
- {
- context: 'context-10.sub-context-10',
- level: LogLevel.Info,
- message: 'message-10-with-message',
- timestamp,
- pid: 5355,
- meta: { tags: ['tag1', 'tag2'] },
- },
- ];
-
- const appender = new LegacyAppender({ verbose: true });
- for (const record of records) {
- appender.append(record);
- }
-
- const [mockLegacyLoggingServerInstance] = (LegacyLoggingServer as any).mock.instances;
- expect(mockLegacyLoggingServerInstance.log.mock.calls).toHaveLength(records.length);
- records.forEach((r, idx) => {
- expect(mockLegacyLoggingServerInstance.log.mock.calls[idx][0]).toMatchSnapshot({
- pid: expect.any(Number),
- });
- });
-});
-
-test('legacy logging server is correctly created and disposed.', async () => {
- const mockRawLegacyLoggingConfig = { verbose: true };
- const appender = new LegacyAppender(mockRawLegacyLoggingConfig);
-
- expect(LegacyLoggingServer).toHaveBeenCalledTimes(1);
- expect(LegacyLoggingServer).toHaveBeenCalledWith(mockRawLegacyLoggingConfig);
-
- const [mockLegacyLoggingServerInstance] = (LegacyLoggingServer as any).mock.instances;
- expect(mockLegacyLoggingServerInstance.stop).not.toHaveBeenCalled();
-
- await appender.dispose();
-
- expect(mockLegacyLoggingServerInstance.stop).toHaveBeenCalledTimes(1);
-});
diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts
deleted file mode 100644
index 7e02d00c7b234..0000000000000
--- a/src/core/server/legacy/logging/appenders/legacy_appender.ts
+++ /dev/null
@@ -1,52 +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 { schema } from '@kbn/config-schema';
-import { LegacyLoggingServer } from '@kbn/legacy-logging';
-import { DisposableAppender, LogRecord } from '@kbn/logging';
-
-export interface LegacyAppenderConfig {
- type: 'legacy-appender';
- legacyLoggingConfig?: Record;
-}
-
-/**
- * Simple appender that just forwards `LogRecord` to the legacy KbnServer log.
- * @internal
- */
-export class LegacyAppender implements DisposableAppender {
- public static configSchema = schema.object({
- type: schema.literal('legacy-appender'),
- legacyLoggingConfig: schema.recordOf(schema.string(), schema.any()),
- });
-
- /**
- * Sets {@link Appender.receiveAllLevels} because legacy does its own filtering based on the legacy logging
- * configuration.
- */
- public readonly receiveAllLevels = true;
-
- private readonly loggingServer: LegacyLoggingServer;
-
- constructor(legacyLoggingConfig: any) {
- this.loggingServer = new LegacyLoggingServer(legacyLoggingConfig);
- }
-
- /**
- * Forwards `LogRecord` to the legacy platform that will layout and
- * write record to the configured destination.
- * @param record `LogRecord` instance to forward to.
- */
- public append(record: LogRecord) {
- this.loggingServer.log(record);
- }
-
- public dispose() {
- this.loggingServer.stop();
- }
-}
diff --git a/src/core/server/logging/README.mdx b/src/core/server/logging/README.mdx
index 08e4ed34204c0..11437d1e8df20 100644
--- a/src/core/server/logging/README.mdx
+++ b/src/core/server/logging/README.mdx
@@ -562,11 +562,6 @@ The log will be less verbose with `warn` level for the `server` context name:
```
### Logging config migration
-Compatibility with the legacy logging system is assured until the end of the `v7` version.
-All log messages handled by `root` context are forwarded to the legacy logging service using a `default` appender. If you re-write
-root appenders, make sure that it contains `default` appender to provide backward compatibility.
-**Note**: If you define an appender for a context name, the log messages for that specific context aren't handled by the
-`root` context anymore and not forwarded to the legacy logging service.
#### logging.dest
By default logs in *stdout*. With new Kibana logging you can use pre-existing `console` appender or
diff --git a/src/core/server/logging/appenders/appenders.test.ts b/src/core/server/logging/appenders/appenders.test.ts
index bd32e4061049b..759fcb9546f09 100644
--- a/src/core/server/logging/appenders/appenders.test.ts
+++ b/src/core/server/logging/appenders/appenders.test.ts
@@ -9,7 +9,6 @@
import { mockCreateLayout } from './appenders.test.mocks';
import { ByteSizeValue } from '@kbn/config-schema';
-import { LegacyAppender } from '../../legacy/logging/appenders/legacy_appender';
import { Appenders } from './appenders';
import { ConsoleAppender } from './console/console_appender';
import { FileAppender } from './file/file_appender';
@@ -68,13 +67,6 @@ test('`create()` creates correct appender.', () => {
});
expect(fileAppender).toBeInstanceOf(FileAppender);
- const legacyAppender = Appenders.create({
- type: 'legacy-appender',
- legacyLoggingConfig: { verbose: true },
- });
-
- expect(legacyAppender).toBeInstanceOf(LegacyAppender);
-
const rollingFileAppender = Appenders.create({
type: 'rolling-file',
fileName: 'path',
diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts
index 88df355bd5ebe..3e867739aa1c7 100644
--- a/src/core/server/logging/appenders/appenders.ts
+++ b/src/core/server/logging/appenders/appenders.ts
@@ -10,10 +10,6 @@ import { schema } from '@kbn/config-schema';
import { assertNever } from '@kbn/std';
import { DisposableAppender } from '@kbn/logging';
-import {
- LegacyAppender,
- LegacyAppenderConfig,
-} from '../../legacy/logging/appenders/legacy_appender';
import { Layouts } from '../layouts/layouts';
import { ConsoleAppender, ConsoleAppenderConfig } from './console/console_appender';
import { FileAppender, FileAppenderConfig } from './file/file_appender';
@@ -32,7 +28,6 @@ import {
export const appendersSchema = schema.oneOf([
ConsoleAppender.configSchema,
FileAppender.configSchema,
- LegacyAppender.configSchema,
RewriteAppender.configSchema,
RollingFileAppender.configSchema,
]);
@@ -41,7 +36,6 @@ export const appendersSchema = schema.oneOf([
export type AppenderConfigType =
| ConsoleAppenderConfig
| FileAppenderConfig
- | LegacyAppenderConfig
| RewriteAppenderConfig
| RollingFileAppenderConfig;
@@ -64,8 +58,6 @@ export class Appenders {
return new RewriteAppender(config);
case 'rolling-file':
return new RollingFileAppender(config);
- case 'legacy-appender':
- return new LegacyAppender(config.legacyLoggingConfig);
default:
return assertNever(config);
diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts
index ade10fc1c0257..ff681222c4f30 100644
--- a/src/core/server/logging/integration_tests/logging.test.ts
+++ b/src/core/server/logging/integration_tests/logging.test.ts
@@ -14,7 +14,6 @@ import { Subject } from 'rxjs';
function createRoot() {
return kbnTestServer.createRoot({
logging: {
- silent: true, // set "true" in kbnTestServer
appenders: {
'test-console': {
type: 'console',
diff --git a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts
index 83533e29ad12e..dc6a01b80e951 100644
--- a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts
+++ b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts
@@ -19,7 +19,6 @@ const flush = async () => delay(flushDelay);
function createRoot(appenderConfig: any) {
return kbnTestServer.createRoot({
logging: {
- silent: true, // set "true" in kbnTestServer
appenders: {
'rolling-file': appenderConfig,
},
diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts
index e0004ba992c17..41acd072b295d 100644
--- a/src/core/server/logging/logging_config.test.ts
+++ b/src/core/server/logging/logging_config.test.ts
@@ -9,35 +9,18 @@
import { LoggingConfig, config } from './logging_config';
test('`schema` creates correct schema with defaults.', () => {
- expect(config.schema.validate({})).toMatchInlineSnapshot(
- { json: expect.any(Boolean) }, // default value depends on TTY
- `
+ expect(config.schema.validate({})).toMatchInlineSnapshot(`
Object {
"appenders": Map {},
- "dest": "stdout",
- "events": Object {},
- "filter": Object {},
- "json": Any,
"loggers": Array [],
- "quiet": false,
"root": Object {
"appenders": Array [
"default",
],
"level": "info",
},
- "rotate": Object {
- "enabled": false,
- "everyBytes": 10485760,
- "keepFiles": 7,
- "pollingInterval": 10000,
- "usePolling": false,
- },
- "silent": false,
- "verbose": false,
}
- `
- );
+ `);
});
test('`schema` throws if `root` logger does not have appenders configured.', () => {
@@ -52,16 +35,14 @@ test('`schema` throws if `root` logger does not have appenders configured.', ()
);
});
-test('`schema` throws if `root` logger does not have "default" appender configured.', () => {
+test('`schema` does not throw if `root` logger does not have "default" appender configured.', () => {
expect(() =>
config.schema.validate({
root: {
appenders: ['console'],
},
})
- ).toThrowErrorMatchingInlineSnapshot(
- `"[root]: \\"default\\" appender required for migration period till the next major release"`
- );
+ ).not.toThrow();
});
test('`getParentLoggerContext()` returns correct parent context name.', () => {
diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts
index f5b75d7bb739c..a04506ad9c0f6 100644
--- a/src/core/server/logging/logging_config.ts
+++ b/src/core/server/logging/logging_config.ts
@@ -7,7 +7,6 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
-import { legacyLoggingConfigSchema } from '@kbn/legacy-logging';
import { AppenderConfigType, Appenders } from './appenders/appenders';
// We need this helper for the types to be correct
@@ -58,31 +57,23 @@ export const loggerSchema = schema.object({
/** @public */
export type LoggerConfigType = TypeOf;
+
export const config = {
path: 'logging',
- schema: legacyLoggingConfigSchema.extends({
+ schema: schema.object({
appenders: schema.mapOf(schema.string(), Appenders.configSchema, {
defaultValue: new Map(),
}),
loggers: schema.arrayOf(loggerSchema, {
defaultValue: [],
}),
- root: schema.object(
- {
- appenders: schema.arrayOf(schema.string(), {
- defaultValue: [DEFAULT_APPENDER_NAME],
- minSize: 1,
- }),
- level: levelSchema,
- },
- {
- validate(rawConfig) {
- if (!rawConfig.appenders.includes(DEFAULT_APPENDER_NAME)) {
- return `"${DEFAULT_APPENDER_NAME}" appender required for migration period till the next major release`;
- }
- },
- }
- ),
+ root: schema.object({
+ appenders: schema.arrayOf(schema.string(), {
+ defaultValue: [DEFAULT_APPENDER_NAME],
+ minSize: 1,
+ }),
+ level: levelSchema,
+ }),
}),
};
diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts
index dd546d4e7eaca..ebe06326f499d 100644
--- a/src/core/server/logging/logging_system.test.ts
+++ b/src/core/server/logging/logging_system.test.ts
@@ -15,11 +15,6 @@ jest.mock('fs', () => ({
const dynamicProps = { process: { pid: expect.any(Number) } };
-jest.mock('@kbn/legacy-logging', () => ({
- ...(jest.requireActual('@kbn/legacy-logging') as any),
- setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})),
-}));
-
const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 33, 22, 11));
let mockConsoleLog: jest.SpyInstance;
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 1ef845730e1f3..770f18d1f7e7a 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -71,12 +71,11 @@ export interface AppCategory {
// Warning: (ae-forgotten-export) The symbol "ConsoleAppenderConfig" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "FileAppenderConfig" needs to be exported by the entry point index.d.ts
-// Warning: (ae-forgotten-export) The symbol "LegacyAppenderConfig" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "RewriteAppenderConfig" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "RollingFileAppenderConfig" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
-export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig;
+export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig;
// @public @deprecated
export interface AsyncPlugin {
diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts
index 47899043dc5a5..c4f420f75b5d1 100644
--- a/src/core/server/server.test.mocks.ts
+++ b/src/core/server/server.test.mocks.ts
@@ -7,32 +7,30 @@
*/
import { httpServiceMock } from './http/http_service.mock';
+
export const mockHttpService = httpServiceMock.create();
jest.doMock('./http/http_service', () => ({
HttpService: jest.fn(() => mockHttpService),
}));
import { pluginServiceMock } from './plugins/plugins_service.mock';
+
export const mockPluginsService = pluginServiceMock.create();
jest.doMock('./plugins/plugins_service', () => ({
PluginsService: jest.fn(() => mockPluginsService),
}));
import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
+
export const mockElasticsearchService = elasticsearchServiceMock.create();
jest.doMock('./elasticsearch/elasticsearch_service', () => ({
ElasticsearchService: jest.fn(() => mockElasticsearchService),
}));
-import { legacyServiceMock } from './legacy/legacy_service.mock';
-export const mockLegacyService = legacyServiceMock.create();
-jest.mock('./legacy/legacy_service', () => ({
- LegacyService: jest.fn(() => mockLegacyService),
-}));
-
const realKbnConfig = jest.requireActual('@kbn/config');
import { configServiceMock } from './config/mocks';
+
export const mockConfigService = configServiceMock.create();
jest.doMock('@kbn/config', () => ({
...realKbnConfig,
@@ -40,18 +38,21 @@ jest.doMock('@kbn/config', () => ({
}));
import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
+
export const mockSavedObjectsService = savedObjectsServiceMock.create();
jest.doMock('./saved_objects/saved_objects_service', () => ({
SavedObjectsService: jest.fn(() => mockSavedObjectsService),
}));
import { contextServiceMock } from './context/context_service.mock';
+
export const mockContextService = contextServiceMock.create();
jest.doMock('./context/context_service', () => ({
ContextService: jest.fn(() => mockContextService),
}));
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
+
export const mockUiSettingsService = uiSettingsServiceMock.create();
jest.doMock('./ui_settings/ui_settings_service', () => ({
UiSettingsService: jest.fn(() => mockUiSettingsService),
@@ -63,46 +64,54 @@ jest.doMock('./config/ensure_valid_configuration', () => ({
}));
import { RenderingService, mockRenderingService } from './rendering/__mocks__/rendering_service';
+
export { mockRenderingService };
jest.doMock('./rendering/rendering_service', () => ({ RenderingService }));
import { environmentServiceMock } from './environment/environment_service.mock';
+
export const mockEnvironmentService = environmentServiceMock.create();
jest.doMock('./environment/environment_service', () => ({
EnvironmentService: jest.fn(() => mockEnvironmentService),
}));
import { metricsServiceMock } from './metrics/metrics_service.mock';
+
export const mockMetricsService = metricsServiceMock.create();
jest.doMock('./metrics/metrics_service', () => ({
MetricsService: jest.fn(() => mockMetricsService),
}));
import { statusServiceMock } from './status/status_service.mock';
+
export const mockStatusService = statusServiceMock.create();
jest.doMock('./status/status_service', () => ({
StatusService: jest.fn(() => mockStatusService),
}));
import { loggingServiceMock } from './logging/logging_service.mock';
+
export const mockLoggingService = loggingServiceMock.create();
jest.doMock('./logging/logging_service', () => ({
LoggingService: jest.fn(() => mockLoggingService),
}));
import { i18nServiceMock } from './i18n/i18n_service.mock';
+
export const mockI18nService = i18nServiceMock.create();
jest.doMock('./i18n/i18n_service', () => ({
I18nService: jest.fn(() => mockI18nService),
}));
import { prebootServiceMock } from './preboot/preboot_service.mock';
+
export const mockPrebootService = prebootServiceMock.create();
jest.doMock('./preboot/preboot_service', () => ({
PrebootService: jest.fn(() => mockPrebootService),
}));
import { deprecationsServiceMock } from './deprecations/deprecations_service.mock';
+
export const mockDeprecationService = deprecationsServiceMock.create();
jest.doMock('./deprecations/deprecations_service', () => ({
DeprecationsService: jest.fn(() => mockDeprecationService),
diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts
index b27c8fa769c48..112693aae0279 100644
--- a/src/core/server/server.test.ts
+++ b/src/core/server/server.test.ts
@@ -9,7 +9,6 @@
import {
mockElasticsearchService,
mockHttpService,
- mockLegacyService,
mockPluginsService,
mockConfigService,
mockSavedObjectsService,
@@ -95,7 +94,6 @@ test('sets up services on "setup"', async () => {
expect(mockHttpService.setup).not.toHaveBeenCalled();
expect(mockElasticsearchService.setup).not.toHaveBeenCalled();
expect(mockPluginsService.setup).not.toHaveBeenCalled();
- expect(mockLegacyService.setup).not.toHaveBeenCalled();
expect(mockSavedObjectsService.setup).not.toHaveBeenCalled();
expect(mockUiSettingsService.setup).not.toHaveBeenCalled();
expect(mockRenderingService.setup).not.toHaveBeenCalled();
@@ -111,7 +109,6 @@ test('sets up services on "setup"', async () => {
expect(mockHttpService.setup).toHaveBeenCalledTimes(1);
expect(mockElasticsearchService.setup).toHaveBeenCalledTimes(1);
expect(mockPluginsService.setup).toHaveBeenCalledTimes(1);
- expect(mockLegacyService.setup).toHaveBeenCalledTimes(1);
expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1);
expect(mockUiSettingsService.setup).toHaveBeenCalledTimes(1);
expect(mockRenderingService.setup).toHaveBeenCalledTimes(1);
@@ -199,7 +196,6 @@ test('stops services on "stop"', async () => {
expect(mockHttpService.stop).not.toHaveBeenCalled();
expect(mockElasticsearchService.stop).not.toHaveBeenCalled();
expect(mockPluginsService.stop).not.toHaveBeenCalled();
- expect(mockLegacyService.stop).not.toHaveBeenCalled();
expect(mockSavedObjectsService.stop).not.toHaveBeenCalled();
expect(mockUiSettingsService.stop).not.toHaveBeenCalled();
expect(mockMetricsService.stop).not.toHaveBeenCalled();
@@ -211,7 +207,6 @@ test('stops services on "stop"', async () => {
expect(mockHttpService.stop).toHaveBeenCalledTimes(1);
expect(mockElasticsearchService.stop).toHaveBeenCalledTimes(1);
expect(mockPluginsService.stop).toHaveBeenCalledTimes(1);
- expect(mockLegacyService.stop).toHaveBeenCalledTimes(1);
expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1);
expect(mockUiSettingsService.stop).toHaveBeenCalledTimes(1);
expect(mockMetricsService.stop).toHaveBeenCalledTimes(1);
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index 867446484a230..8b0714e899139 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -21,7 +21,6 @@ import { ElasticsearchService } from './elasticsearch';
import { HttpService } from './http';
import { HttpResourcesService } from './http_resources';
import { RenderingService } from './rendering';
-import { LegacyService } from './legacy';
import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging';
import { UiSettingsService } from './ui_settings';
import { PluginsService, config as pluginsConfig } from './plugins';
@@ -69,7 +68,6 @@ export class Server {
private readonly elasticsearch: ElasticsearchService;
private readonly http: HttpService;
private readonly rendering: RenderingService;
- private readonly legacy: LegacyService;
private readonly log: Logger;
private readonly plugins: PluginsService;
private readonly savedObjects: SavedObjectsService;
@@ -108,7 +106,6 @@ export class Server {
this.http = new HttpService(core);
this.rendering = new RenderingService(core);
this.plugins = new PluginsService(core);
- this.legacy = new LegacyService(core);
this.elasticsearch = new ElasticsearchService(core);
this.savedObjects = new SavedObjectsService(core);
this.uiSettings = new UiSettingsService(core);
@@ -286,10 +283,6 @@ export class Server {
const pluginsSetup = await this.plugins.setup(coreSetup);
this.#pluginsInitialized = pluginsSetup.initialized;
- await this.legacy.setup({
- http: httpSetup,
- });
-
this.registerCoreContext(coreSetup);
this.coreApp.setup(coreSetup, uiPlugins);
@@ -348,7 +341,6 @@ export class Server {
public async stop() {
this.log.debug('stopping server');
- await this.legacy.stop();
await this.http.stop(); // HTTP server has to stop before savedObjects and ES clients are closed to be able to gracefully attempt to resolve any pending requests
await this.plugins.stop();
await this.savedObjects.stop();
diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts
index 67bd6c7455d6d..58720be637e2f 100644
--- a/src/core/test_helpers/kbn_server.ts
+++ b/src/core/test_helpers/kbn_server.ts
@@ -32,7 +32,11 @@ const DEFAULTS_SETTINGS = {
port: 0,
xsrf: { disableProtection: true },
},
- logging: { silent: true },
+ logging: {
+ root: {
+ level: 'off',
+ },
+ },
plugins: {},
migrations: { skip: false },
};
@@ -45,7 +49,6 @@ export function createRootWithSettings(
configs: [],
cliArgs: {
dev: false,
- silent: false,
watch: false,
basePath: false,
runExamples: false,
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index cee43fd85c90f..f671fa963079f 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -82,24 +82,13 @@ kibana_vars=(
logging.appenders
logging.appenders.console
logging.appenders.file
- logging.dest
- logging.json
logging.loggers
logging.loggers.appenders
logging.loggers.level
logging.loggers.name
- logging.quiet
logging.root
logging.root.appenders
logging.root.level
- logging.rotate.enabled
- logging.rotate.everyBytes
- logging.rotate.keepFiles
- logging.rotate.pollingInterval
- logging.rotate.usePolling
- logging.silent
- logging.useUTC
- logging.verbose
map.includeElasticMapsService
map.proxyElasticMapsServiceInMaps
map.regionmap
diff --git a/src/plugins/kibana_usage_collection/server/collectors/config_usage/README.md b/src/plugins/kibana_usage_collection/server/collectors/config_usage/README.md
index b476244e5082f..954b12dba00f7 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/config_usage/README.md
+++ b/src/plugins/kibana_usage_collection/server/collectors/config_usage/README.md
@@ -55,7 +55,6 @@ when setting an exact config or its parent path to `false`.
"server.port": 5603,
"server.basePath": "[redacted]",
"server.rewriteBasePath": true,
- "logging.json": false,
"usageCollection.uiCounters.debug": true
}
}
diff --git a/test/common/config.js b/test/common/config.js
index eb110fad55ea8..b9ab24450ac82 100644
--- a/test/common/config.js
+++ b/test/common/config.js
@@ -28,7 +28,6 @@ export default function () {
buildArgs: [],
sourceArgs: ['--no-base-path', '--env.name=development'],
serverArgs: [
- '--logging.json=false',
`--server.port=${kbnTestConfig.getPort()}`,
'--status.allowAnonymous=true',
// We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should
diff --git a/test/server_integration/http/platform/config.status.ts b/test/server_integration/http/platform/config.status.ts
index 8cc76c901f47c..20ffc917f8244 100644
--- a/test/server_integration/http/platform/config.status.ts
+++ b/test/server_integration/http/platform/config.status.ts
@@ -51,7 +51,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
runOptions: {
...httpConfig.get('kbnTestServer.runOptions'),
// Don't wait for Kibana to be completely ready so that we can test the status timeouts
- wait: /\[Kibana\]\[http\] http server running/,
+ wait: /Kibana is now unavailable/,
},
},
};
diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts
index 3be565d59a11f..98f11d56853b2 100644
--- a/x-pack/plugins/security/server/config.test.ts
+++ b/x-pack/plugins/security/server/config.test.ts
@@ -1729,7 +1729,7 @@ describe('createConfig()', () => {
},
},
})
- ).toThrow('[audit.appender.2.type]: expected value to equal [legacy-appender]');
+ ).toThrow('[audit.appender.1.layout]: expected at least one defined value but got [undefined]');
});
it('rejects an ignore_filter when no appender is configured', () => {
diff --git a/yarn.lock b/yarn.lock
index 416d9c6b8ba5e..9f6ccfffb8113 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2404,15 +2404,6 @@
async-retry "^1.2.3"
strip-ansi "^5.2.0"
-"@elastic/good@^9.0.1-kibana3":
- version "9.0.1-kibana3"
- resolved "https://registry.yarnpkg.com/@elastic/good/-/good-9.0.1-kibana3.tgz#a70c2b30cbb4f44d1cf4a464562e0680322eac9b"
- integrity sha512-UtPKr0TmlkL1abJfO7eEVUTqXWzLKjMkz+65FvxU/Ub9kMAr4No8wHLRfDHFzBkWoDIbDWygwld011WzUnea1Q==
- dependencies:
- "@hapi/hoek" "9.x.x"
- "@hapi/oppsy" "3.x.x"
- "@hapi/validate" "1.x.x"
-
"@elastic/makelogs@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@elastic/makelogs/-/makelogs-6.0.0.tgz#d6d74d5d0f020123c54160370d49ca5e0aab1fe1"
@@ -2897,14 +2888,6 @@
resolved "https://registry.yarnpkg.com/@hapi/file/-/file-2.0.0.tgz#2ecda37d1ae9d3078a67c13b7da86e8c3237dfb9"
integrity sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ==
-"@hapi/good-squeeze@6.0.0":
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/good-squeeze/-/good-squeeze-6.0.0.tgz#bb72d6869cd7398b615a6b7270f630dc4f76aebf"
- integrity sha512-UgHAF9Lm8fJPzgf2HymtowOwNc1+IL+p08YTVR+XA4d8nmyE1t9x3RLA4riqldnOKHkVqGakJ1jGqUG7jk77Cg==
- dependencies:
- "@hapi/hoek" "9.x.x"
- fast-safe-stringify "2.x.x"
-
"@hapi/h2o2@^9.1.0":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@hapi/h2o2/-/h2o2-9.1.0.tgz#b223f4978b6f2b0d7d9db10a84a567606c4c3551"
@@ -2992,13 +2975,6 @@
"@hapi/hoek" "^9.0.4"
"@hapi/vise" "^4.0.0"
-"@hapi/oppsy@3.x.x":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@hapi/oppsy/-/oppsy-3.0.0.tgz#1ae397e200e86d0aa41055f103238ed8652947ca"
- integrity sha512-0kfUEAqIi21GzFVK2snMO07znMEBiXb+/pOx1dmgOO9TuvFstcfmHU5i56aDfiFP2DM5WzQCU2UWc2gK1lMDhQ==
- dependencies:
- "@hapi/hoek" "9.x.x"
-
"@hapi/pez@^5.0.1":
version "5.0.3"
resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-5.0.3.tgz#b75446e6fef8cbb16816573ab7da1b0522e7a2a1"
@@ -3750,10 +3726,6 @@
version "0.0.0"
uid ""
-"@kbn/legacy-logging@link:bazel-bin/packages/kbn-legacy-logging":
- version "0.0.0"
- uid ""
-
"@kbn/logging@link:bazel-bin/packages/kbn-logging":
version "0.0.0"
uid ""
@@ -14493,7 +14465,7 @@ fast-redact@^3.0.0:
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.0.tgz#ac2f9e36c9f4976f5db9fb18c6ffbaf308cf316d"
integrity sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w==
-fast-safe-stringify@2.x.x, fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7:
+fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7:
version "2.0.8"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f"
integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag==
From 699de6b6039120c8665355310c36164b253f1244 Mon Sep 17 00:00:00 2001
From: Gloria Hornero
Date: Tue, 5 Oct 2021 13:34:44 +0200
Subject: [PATCH 08/78] [Security Solution] Minimizes the number of environment
variables used in Cypress (#113854)
* cleans parameters
* updates readme
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../security_solution/cypress/README.md | 4 +-
.../security_solution/cypress/tasks/login.ts | 18 +++---
.../test/security_solution_cypress/runner.ts | 59 +++----------------
3 files changed, 21 insertions(+), 60 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md
index d70011f864860..b500091aacc2d 100644
--- a/x-pack/plugins/security_solution/cypress/README.md
+++ b/x-pack/plugins/security_solution/cypress/README.md
@@ -207,7 +207,7 @@ node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solut
# launch the cypress test runner with overridden environment variables
cd x-pack/plugins/security_solution
-CYPRESS_base_url=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= CYPRESS_protocol= CYPRESS_hostname= CYPRESS_configport= CYPRESS_KIBANA_URL= yarn cypress:run
+CYPRESS_BASE_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run
```
#### Custom Target + Headless (Firefox)
@@ -225,7 +225,7 @@ node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solut
# launch the cypress test runner with overridden environment variables
cd x-pack/plugins/security_solution
-CYPRESS_base_url=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= CYPRESS_protocol= CYPRESS_hostname= CYPRESS_configport= CYPRESS_KIBANA_URL= yarn cypress:run:firefox
+CYPRESS_BASE_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_URL=http(s)://:@ CYPRESS_ELASTICSEARCH_USERNAME= CYPRESS_ELASTICSEARCH_PASSWORD= yarn cypress:run:firefox
```
#### CCS Custom Target + Headless
diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts
index 243bfd113bfd2..5a935702131d6 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/login.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts
@@ -56,13 +56,15 @@ const LOGIN_API_ENDPOINT = '/internal/security/login';
* @param route string route to visit
*/
export const getUrlWithRoute = (role: ROLES, route: string) => {
+ const url = Cypress.config().baseUrl;
+ const kibana = new URL(url!);
const theUrl = `${Url.format({
auth: `${role}:changeme`,
username: role,
password: 'changeme',
- protocol: Cypress.env('protocol'),
- hostname: Cypress.env('hostname'),
- port: Cypress.env('configport'),
+ protocol: kibana.protocol.replace(':', ''),
+ hostname: kibana.hostname,
+ port: kibana.port,
} as UrlObject)}${route.startsWith('/') ? '' : '/'}${route}`;
cy.log(`origin: ${theUrl}`);
return theUrl;
@@ -80,11 +82,13 @@ interface User {
* @param route string route to visit
*/
export const constructUrlWithUser = (user: User, route: string) => {
- const hostname = Cypress.env('hostname');
+ const url = Cypress.config().baseUrl;
+ const kibana = new URL(url!);
+ const hostname = kibana.hostname;
const username = user.username;
const password = user.password;
- const protocol = Cypress.env('protocol');
- const port = Cypress.env('configport');
+ const protocol = kibana.protocol.replace(':', '');
+ const port = kibana.port;
const path = `${route.startsWith('/') ? '' : '/'}${route}`;
const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`;
@@ -98,7 +102,7 @@ export const getCurlScriptEnvVars = () => ({
ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'),
ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'),
ELASTICSEARCH_PASSWORD: Cypress.env('ELASTICSEARCH_PASSWORD'),
- KIBANA_URL: Cypress.env('KIBANA_URL'),
+ KIBANA_URL: Cypress.config().baseUrl,
});
export const postRoleAndUser = (role: ROLES) => {
diff --git a/x-pack/test/security_solution_cypress/runner.ts b/x-pack/test/security_solution_cypress/runner.ts
index f93e20ec382cd..4283b85af0c17 100644
--- a/x-pack/test/security_solution_cypress/runner.ts
+++ b/x-pack/test/security_solution_cypress/runner.ts
@@ -26,22 +26,10 @@ export async function SecuritySolutionCypressCliTestRunner({ getService }: FtrPr
cwd: resolve(__dirname, '../../plugins/security_solution'),
env: {
FORCE_COLOR: '1',
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_baseUrl: Url.format(config.get('servers.kibana')),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_protocol: config.get('servers.kibana.protocol'),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_hostname: config.get('servers.kibana.hostname'),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_configport: config.get('servers.kibana.port'),
+ CYPRESS_BASE_URL: Url.format(config.get('servers.kibana')),
CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')),
CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'),
CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'),
- CYPRESS_KIBANA_URL: Url.format({
- protocol: config.get('servers.kibana.protocol'),
- hostname: config.get('servers.kibana.hostname'),
- port: config.get('servers.kibana.port'),
- }),
...process.env,
},
wait: true,
@@ -65,22 +53,10 @@ export async function SecuritySolutionCypressCliFirefoxTestRunner({
cwd: resolve(__dirname, '../../plugins/security_solution'),
env: {
FORCE_COLOR: '1',
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_baseUrl: Url.format(config.get('servers.kibana')),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_protocol: config.get('servers.kibana.protocol'),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_hostname: config.get('servers.kibana.hostname'),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_configport: config.get('servers.kibana.port'),
+ CYPRESS_BASE_URL: Url.format(config.get('servers.kibana')),
CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')),
CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'),
CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'),
- CYPRESS_KIBANA_URL: Url.format({
- protocol: config.get('servers.kibana.protocol'),
- hostname: config.get('servers.kibana.hostname'),
- port: config.get('servers.kibana.port'),
- }),
...process.env,
},
wait: true,
@@ -126,22 +102,10 @@ export async function SecuritySolutionCypressVisualTestRunner({ getService }: Ft
cwd: resolve(__dirname, '../../plugins/security_solution'),
env: {
FORCE_COLOR: '1',
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_baseUrl: Url.format(config.get('servers.kibana')),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_protocol: config.get('servers.kibana.protocol'),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_hostname: config.get('servers.kibana.hostname'),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_configport: config.get('servers.kibana.port'),
+ CYPRESS_BASE_URL: Url.format(config.get('servers.kibana')),
CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')),
CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'),
CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'),
- CYPRESS_KIBANA_URL: Url.format({
- protocol: config.get('servers.kibana.protocol'),
- hostname: config.get('servers.kibana.hostname'),
- port: config.get('servers.kibana.port'),
- }),
...process.env,
},
wait: true,
@@ -153,6 +117,7 @@ export async function SecuritySolutionCypressUpgradeCliTestRunner({
getService,
}: FtrProviderContext) {
const log = getService('log');
+ const config = getService('config');
await withProcRunner(log, async (procs) => {
await procs.run('cypress', {
@@ -161,18 +126,10 @@ export async function SecuritySolutionCypressUpgradeCliTestRunner({
cwd: resolve(__dirname, '../../plugins/security_solution'),
env: {
FORCE_COLOR: '1',
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_baseUrl: process.env.TEST_KIBANA_URL,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_protocol: process.env.TEST_KIBANA_PROTOCOL,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_hostname: process.env.TEST_KIBANA_HOSTNAME,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- CYPRESS_configport: process.env.TEST_KIBANA_PORT,
- CYPRESS_ELASTICSEARCH_URL: process.env.TEST_ES_URL,
- CYPRESS_ELASTICSEARCH_USERNAME: process.env.TEST_ES_USER,
- CYPRESS_ELASTICSEARCH_PASSWORD: process.env.TEST_ES_PASS,
- CYPRESS_KIBANA_URL: process.env.TEST_KIBANA_URL,
+ CYPRESS_BASE_URL: Url.format(config.get('servers.kibana')),
+ CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')),
+ CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'),
+ CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'),
...process.env,
},
wait: true,
From 79f841e5d39989107d8ac93aec6a905ef60b857b Mon Sep 17 00:00:00 2001
From: Dario Gieselaar
Date: Tue, 5 Oct 2021 13:38:51 +0200
Subject: [PATCH 09/78] [APM] Display all properties by default in flyout
(#113221)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/core/types/elasticsearch/search.ts | 2 +-
.../elasticsearch_fieldnames.test.ts.snap | 6 +
.../apm/common/elasticsearch_fieldnames.ts | 1 +
x-pack/plugins/apm/common/processor_event.ts | 9 +
.../components/shared/KeyValueTable/index.tsx | 38 ++--
.../ErrorMetadata/ErrorMetadata.test.tsx | 139 --------------
.../MetadataTable/ErrorMetadata/index.tsx | 34 +++-
.../MetadataTable/ErrorMetadata/sections.ts | 39 ----
.../MetadataTable/MetadataTable.test.tsx | 25 +--
.../shared/MetadataTable/Section.test.tsx | 2 +-
.../shared/MetadataTable/Section.tsx | 16 +-
.../SpanMetadata/SpanMetadata.test.tsx | 107 -----------
.../MetadataTable/SpanMetadata/index.tsx | 34 +++-
.../MetadataTable/SpanMetadata/sections.ts | 29 ---
.../TransactionMetadata.test.tsx | 164 -----------------
.../TransactionMetadata/index.tsx | 33 +++-
.../TransactionMetadata/sections.ts | 47 -----
.../shared/MetadataTable/helper.test.ts | 69 +++----
.../components/shared/MetadataTable/helper.ts | 84 +++++----
.../components/shared/MetadataTable/index.tsx | 18 +-
.../shared/MetadataTable/sections.ts | 173 ------------------
.../components/shared/MetadataTable/types.ts | 13 ++
.../lib/event_metadata/get_event_metadata.ts | 66 +++++++
.../apm/server/routes/event_metadata.ts | 44 +++++
.../get_global_apm_server_route_repository.ts | 4 +-
.../translations/translations/ja-JP.json | 18 --
.../translations/translations/zh-CN.json | 18 --
.../test/apm_api_integration/tests/index.ts | 4 +
.../tests/metadata/event_metadata.ts | 129 +++++++++++++
29 files changed, 505 insertions(+), 860 deletions(-)
delete mode 100644 x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx
delete mode 100644 x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts
delete mode 100644 x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx
delete mode 100644 x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts
delete mode 100644 x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx
delete mode 100644 x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts
delete mode 100644 x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts
create mode 100644 x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts
create mode 100644 x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.ts
create mode 100644 x-pack/plugins/apm/server/routes/event_metadata.ts
create mode 100644 x-pack/test/apm_api_integration/tests/metadata/event_metadata.ts
diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts
index 88d6cda3777dd..a54f5f3758ce3 100644
--- a/src/core/types/elasticsearch/search.ts
+++ b/src/core/types/elasticsearch/search.ts
@@ -48,7 +48,7 @@ type ValueTypeOfField = T extends Record
type MaybeArray = T | T[];
-type Fields = Exclude['body']['fields'], undefined>;
+type Fields = Required['body']>['fields'];
type DocValueFields = MaybeArray;
export type SearchHit<
diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
index c4658ae2ac22c..2d1433324858b 100644
--- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
+++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
@@ -53,6 +53,8 @@ exports[`Error ERROR_EXC_TYPE 1`] = `undefined`;
exports[`Error ERROR_GROUP_ID 1`] = `"grouping key"`;
+exports[`Error ERROR_ID 1`] = `"error id"`;
+
exports[`Error ERROR_LOG_LEVEL 1`] = `undefined`;
exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`;
@@ -298,6 +300,8 @@ exports[`Span ERROR_EXC_TYPE 1`] = `undefined`;
exports[`Span ERROR_GROUP_ID 1`] = `undefined`;
+exports[`Span ERROR_ID 1`] = `undefined`;
+
exports[`Span ERROR_LOG_LEVEL 1`] = `undefined`;
exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`;
@@ -535,6 +539,8 @@ exports[`Transaction ERROR_EXC_TYPE 1`] = `undefined`;
exports[`Transaction ERROR_GROUP_ID 1`] = `undefined`;
+exports[`Transaction ERROR_ID 1`] = `undefined`;
+
exports[`Transaction ERROR_LOG_LEVEL 1`] = `undefined`;
exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`;
diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
index d1f07c28bc808..4a4cad5454c4b 100644
--- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
+++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
@@ -78,6 +78,7 @@ export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM =
// Parent ID for a transaction or span
export const PARENT_ID = 'parent.id';
+export const ERROR_ID = 'error.id';
export const ERROR_GROUP_ID = 'error.grouping_key';
export const ERROR_CULPRIT = 'error.culprit';
export const ERROR_LOG_LEVEL = 'error.log.level';
diff --git a/x-pack/plugins/apm/common/processor_event.ts b/x-pack/plugins/apm/common/processor_event.ts
index 57705e7ed4ce0..fe0d9abfa0e51 100644
--- a/x-pack/plugins/apm/common/processor_event.ts
+++ b/x-pack/plugins/apm/common/processor_event.ts
@@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import * as t from 'io-ts';
export enum ProcessorEvent {
transaction = 'transaction',
@@ -12,6 +13,14 @@ export enum ProcessorEvent {
span = 'span',
profile = 'profile',
}
+
+export const processorEventRt = t.union([
+ t.literal(ProcessorEvent.transaction),
+ t.literal(ProcessorEvent.error),
+ t.literal(ProcessorEvent.metric),
+ t.literal(ProcessorEvent.span),
+ t.literal(ProcessorEvent.profile),
+]);
/**
* Processor events that are searchable in the UI via the query bar.
*
diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx
index 13aa3696eda42..e9525728bc3c5 100644
--- a/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import { castArray } from 'lodash';
import React, { TableHTMLAttributes } from 'react';
import {
EuiTable,
@@ -26,16 +26,32 @@ export function KeyValueTable({
return (
- {keyValuePairs.map(({ key, value }) => (
-
-
- {key}
-
-
-
-
-
- ))}
+ {keyValuePairs.map(({ key, value }) => {
+ const asArray = castArray(value);
+ const valueList =
+ asArray.length <= 1 ? (
+
+ ) : (
+
+ {asArray.map((val, index) => (
+ -
+
+
+ ))}
+
+ );
+
+ return (
+
+
+ {key}
+
+
+ {valueList}
+
+
+ );
+ })}
);
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx
deleted file mode 100644
index f936941923e41..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx
+++ /dev/null
@@ -1,139 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { render } from '@testing-library/react';
-import React, { ReactNode } from 'react';
-import { MemoryRouter } from 'react-router-dom';
-import { ErrorMetadata } from '.';
-import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
-import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
-import {
- expectTextsInDocument,
- expectTextsNotInDocument,
-} from '../../../../utils/testHelpers';
-
-function Wrapper({ children }: { children?: ReactNode }) {
- return (
-
- {children}
-
- );
-}
-
-const renderOptions = {
- wrapper: Wrapper,
-};
-
-function getError() {
- return {
- labels: { someKey: 'labels value' },
- http: { someKey: 'http value' },
- host: { someKey: 'host value' },
- container: { someKey: 'container value' },
- service: { someKey: 'service value' },
- process: { someKey: 'process value' },
- agent: { someKey: 'agent value' },
- url: { someKey: 'url value' },
- user: { someKey: 'user value' },
- notIncluded: 'not included value',
- error: {
- id: '7efbc7056b746fcb',
- notIncluded: 'error not included value',
- custom: {
- someKey: 'custom value',
- },
- },
- } as unknown as APMError;
-}
-
-describe('ErrorMetadata', () => {
- it('should render a error with all sections', () => {
- const error = getError();
- const output = render(, renderOptions);
-
- // sections
- expectTextsInDocument(output, [
- 'Labels',
- 'HTTP',
- 'Host',
- 'Container',
- 'Service',
- 'Process',
- 'Agent',
- 'URL',
- 'User',
- 'Custom',
- ]);
- });
-
- it('should render a error with all included dot notation keys', () => {
- const error = getError();
- const output = render(, renderOptions);
-
- // included keys
- expectTextsInDocument(output, [
- 'labels.someKey',
- 'http.someKey',
- 'host.someKey',
- 'container.someKey',
- 'service.someKey',
- 'process.someKey',
- 'agent.someKey',
- 'url.someKey',
- 'user.someKey',
- 'error.custom.someKey',
- ]);
-
- // excluded keys
- expectTextsNotInDocument(output, ['notIncluded', 'error.notIncluded']);
- });
-
- it('should render a error with all included values', () => {
- const error = getError();
- const output = render(, renderOptions);
-
- // included values
- expectTextsInDocument(output, [
- 'labels value',
- 'http value',
- 'host value',
- 'container value',
- 'service value',
- 'process value',
- 'agent value',
- 'url value',
- 'user value',
- 'custom value',
- ]);
-
- // excluded values
- expectTextsNotInDocument(output, [
- 'not included value',
- 'error not included value',
- ]);
- });
-
- it('should render a error with only the required sections', () => {
- const error = {} as APMError;
- const output = render(, renderOptions);
-
- // required sections should be found
- expectTextsInDocument(output, ['Labels', 'User']);
-
- // optional sections should NOT be found
- expectTextsNotInDocument(output, [
- 'HTTP',
- 'Host',
- 'Container',
- 'Service',
- 'Process',
- 'Agent',
- 'URL',
- 'Custom',
- ]);
- });
-});
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx
index 196a8706d5132..f6ffc34ecee02 100644
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx
@@ -6,19 +6,41 @@
*/
import React, { useMemo } from 'react';
-import { ERROR_METADATA_SECTIONS } from './sections';
import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
-import { getSectionsWithRows } from '../helper';
+import { getSectionsFromFields } from '../helper';
import { MetadataTable } from '..';
+import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
+import { ProcessorEvent } from '../../../../../common/processor_event';
interface Props {
error: APMError;
}
export function ErrorMetadata({ error }: Props) {
- const sectionsWithRows = useMemo(
- () => getSectionsWithRows(ERROR_METADATA_SECTIONS, error),
- [error]
+ const { data: errorEvent, status } = useFetcher(
+ (callApmApi) => {
+ return callApmApi({
+ endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}',
+ params: {
+ path: {
+ processorEvent: ProcessorEvent.error,
+ id: error.error.id,
+ },
+ },
+ });
+ },
+ [error.error.id]
+ );
+
+ const sections = useMemo(
+ () => getSectionsFromFields(errorEvent?.metadata || {}),
+ [errorEvent?.metadata]
+ );
+
+ return (
+
);
- return ;
}
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts
deleted file mode 100644
index 28a64ac36660e..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts
+++ /dev/null
@@ -1,39 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- Section,
- ERROR,
- LABELS,
- HTTP,
- HOST,
- CONTAINER,
- SERVICE,
- PROCESS,
- AGENT,
- URL,
- USER,
- CUSTOM_ERROR,
- TRACE,
- TRANSACTION,
-} from '../sections';
-
-export const ERROR_METADATA_SECTIONS: Section[] = [
- { ...LABELS, required: true },
- TRACE,
- TRANSACTION,
- ERROR,
- HTTP,
- HOST,
- CONTAINER,
- SERVICE,
- PROCESS,
- AGENT,
- URL,
- { ...USER, required: true },
- CUSTOM_ERROR,
-];
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx
index 7ccde6a9a74d6..5d5976866ba24 100644
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx
@@ -11,7 +11,7 @@ import { MemoryRouter } from 'react-router-dom';
import { MetadataTable } from '.';
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
import { expectTextsInDocument } from '../../../utils/testHelpers';
-import { SectionsWithRows } from './helper';
+import type { SectionDescriptor } from './types';
function Wrapper({ children }: { children?: ReactNode }) {
return (
@@ -27,21 +27,20 @@ const renderOptions = {
describe('MetadataTable', () => {
it('shows sections', () => {
- const sectionsWithRows = [
- { key: 'foo', label: 'Foo', required: true },
+ const sections: SectionDescriptor[] = [
+ { key: 'foo', label: 'Foo', required: true, properties: [] },
{
key: 'bar',
label: 'Bar',
required: false,
- properties: ['props.A', 'props.B'],
- rows: [
- { key: 'props.A', value: 'A' },
- { key: 'props.B', value: 'B' },
+ properties: [
+ { field: 'props.A', value: ['A'] },
+ { field: 'props.B', value: ['B'] },
],
},
- ] as unknown as SectionsWithRows;
+ ];
const output = render(
- ,
+ ,
renderOptions
);
expectTextsInDocument(output, [
@@ -56,15 +55,17 @@ describe('MetadataTable', () => {
});
describe('required sections', () => {
it('shows "empty state message" if no data is available', () => {
- const sectionsWithRows = [
+ const sectionsWithRows: SectionDescriptor[] = [
{
key: 'foo',
label: 'Foo',
required: true,
+ properties: [],
},
- ] as unknown as SectionsWithRows;
+ ];
+
const output = render(
- ,
+ ,
renderOptions
);
expectTextsInDocument(output, ['Foo', 'No data available']);
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx
index d44464e2160d3..ed816b1c7a337 100644
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx
@@ -12,7 +12,7 @@ import { expectTextsInDocument } from '../../../utils/testHelpers';
describe('Section', () => {
it('shows "empty state message" if no data is available', () => {
- const component = render();
+ const component = render();
expectTextsInDocument(component, ['No data available']);
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx
index ff86083b8612d..03ae237f470c3 100644
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx
@@ -10,15 +10,21 @@ import { isEmpty } from 'lodash';
import { i18n } from '@kbn/i18n';
import { EuiText } from '@elastic/eui';
import { KeyValueTable } from '../KeyValueTable';
-import { KeyValuePair } from '../../../utils/flattenObject';
interface Props {
- keyValuePairs: KeyValuePair[];
+ properties: Array<{ field: string; value: string[] | number[] }>;
}
-export function Section({ keyValuePairs }: Props) {
- if (!isEmpty(keyValuePairs)) {
- return ;
+export function Section({ properties }: Props) {
+ if (!isEmpty(properties)) {
+ return (
+ ({
+ key: property.field,
+ value: property.value,
+ }))}
+ />
+ );
}
return (
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx
deleted file mode 100644
index 46eaba1e9e11d..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx
+++ /dev/null
@@ -1,107 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { render } from '@testing-library/react';
-import React, { ReactNode } from 'react';
-import { MemoryRouter } from 'react-router-dom';
-import { SpanMetadata } from '.';
-import { Span } from '../../../../../typings/es_schemas/ui/span';
-import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
-import {
- expectTextsInDocument,
- expectTextsNotInDocument,
-} from '../../../../utils/testHelpers';
-
-function Wrapper({ children }: { children?: ReactNode }) {
- return (
-
- {children}
-
- );
-}
-
-const renderOptions = {
- wrapper: Wrapper,
-};
-
-describe('SpanMetadata', () => {
- describe('render', () => {
- it('renders', () => {
- const span = {
- agent: {
- ephemeral_id: 'ed8e3a4f-21d2-4a1f-bbc7-fa2064d94225',
- name: 'java',
- version: '1.9.1-SNAPSHOT',
- },
- service: {
- name: 'opbeans-java',
- },
- span: {
- id: '7efbc7056b746fcb',
- message: {
- age: { ms: 1577958057123 },
- queue: { name: 'queue name' },
- },
- },
- } as unknown as Span;
- const output = render(, renderOptions);
- expectTextsInDocument(output, ['Service', 'Agent', 'Message']);
- });
- });
- describe('when a span is presented', () => {
- it('renders the span', () => {
- const span = {
- agent: {
- ephemeral_id: 'ed8e3a4f-21d2-4a1f-bbc7-fa2064d94225',
- name: 'java',
- version: '1.9.1-SNAPSHOT',
- },
- service: {
- name: 'opbeans-java',
- },
- span: {
- id: '7efbc7056b746fcb',
- http: {
- response: { status_code: 200 },
- },
- subtype: 'http',
- type: 'external',
- message: {
- age: { ms: 1577958057123 },
- queue: { name: 'queue name' },
- },
- },
- } as unknown as Span;
- const output = render(, renderOptions);
- expectTextsInDocument(output, ['Service', 'Agent', 'Span', 'Message']);
- });
- });
- describe('when there is no id inside span', () => {
- it('does not show the section', () => {
- const span = {
- agent: {
- ephemeral_id: 'ed8e3a4f-21d2-4a1f-bbc7-fa2064d94225',
- name: 'java',
- version: '1.9.1-SNAPSHOT',
- },
- service: {
- name: 'opbeans-java',
- },
- span: {
- http: {
- response: { status_code: 200 },
- },
- subtype: 'http',
- type: 'external',
- },
- } as unknown as Span;
- const output = render(, renderOptions);
- expectTextsInDocument(output, ['Service', 'Agent']);
- expectTextsNotInDocument(output, ['Span', 'Message']);
- });
- });
-});
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx
index feefcea9d38a0..bf5702b4acf3e 100644
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx
@@ -6,19 +6,41 @@
*/
import React, { useMemo } from 'react';
-import { SPAN_METADATA_SECTIONS } from './sections';
import { Span } from '../../../../../typings/es_schemas/ui/span';
-import { getSectionsWithRows } from '../helper';
+import { getSectionsFromFields } from '../helper';
import { MetadataTable } from '..';
+import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
+import { ProcessorEvent } from '../../../../../common/processor_event';
interface Props {
span: Span;
}
export function SpanMetadata({ span }: Props) {
- const sectionsWithRows = useMemo(
- () => getSectionsWithRows(SPAN_METADATA_SECTIONS, span),
- [span]
+ const { data: spanEvent, status } = useFetcher(
+ (callApmApi) => {
+ return callApmApi({
+ endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}',
+ params: {
+ path: {
+ processorEvent: ProcessorEvent.span,
+ id: span.span.id,
+ },
+ },
+ });
+ },
+ [span.span.id]
+ );
+
+ const sections = useMemo(
+ () => getSectionsFromFields(spanEvent?.metadata || {}),
+ [spanEvent?.metadata]
+ );
+
+ return (
+
);
- return ;
}
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts
deleted file mode 100644
index f19aef8e0bd8a..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts
+++ /dev/null
@@ -1,29 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- Section,
- AGENT,
- SERVICE,
- SPAN,
- LABELS,
- EVENT,
- TRANSACTION,
- TRACE,
- MESSAGE_SPAN,
-} from '../sections';
-
-export const SPAN_METADATA_SECTIONS: Section[] = [
- LABELS,
- TRACE,
- TRANSACTION,
- EVENT,
- SPAN,
- SERVICE,
- MESSAGE_SPAN,
- AGENT,
-];
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx
deleted file mode 100644
index 08253f04777d9..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx
+++ /dev/null
@@ -1,164 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { render } from '@testing-library/react';
-import React, { ReactNode } from 'react';
-import { MemoryRouter } from 'react-router-dom';
-import { TransactionMetadata } from '.';
-import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
-import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
-import {
- expectTextsInDocument,
- expectTextsNotInDocument,
-} from '../../../../utils/testHelpers';
-
-function Wrapper({ children }: { children?: ReactNode }) {
- return (
-
- {children}
-
- );
-}
-
-const renderOptions = {
- wrapper: Wrapper,
-};
-
-function getTransaction() {
- return {
- labels: { someKey: 'labels value' },
- http: { someKey: 'http value' },
- host: { someKey: 'host value' },
- container: { someKey: 'container value' },
- service: { someKey: 'service value' },
- process: { someKey: 'process value' },
- agent: { someKey: 'agent value' },
- url: { someKey: 'url value' },
- user: { someKey: 'user value' },
- notIncluded: 'not included value',
- transaction: {
- id: '7efbc7056b746fcb',
- notIncluded: 'transaction not included value',
- custom: {
- someKey: 'custom value',
- },
- message: {
- age: { ms: 1577958057123 },
- queue: { name: 'queue name' },
- },
- },
- } as unknown as Transaction;
-}
-
-describe('TransactionMetadata', () => {
- it('should render a transaction with all sections', () => {
- const transaction = getTransaction();
- const output = render(
- ,
- renderOptions
- );
-
- // sections
- expectTextsInDocument(output, [
- 'Labels',
- 'HTTP',
- 'Host',
- 'Container',
- 'Service',
- 'Process',
- 'Agent',
- 'URL',
- 'User',
- 'Custom',
- 'Message',
- ]);
- });
-
- it('should render a transaction with all included dot notation keys', () => {
- const transaction = getTransaction();
- const output = render(
- ,
- renderOptions
- );
-
- // included keys
- expectTextsInDocument(output, [
- 'labels.someKey',
- 'http.someKey',
- 'host.someKey',
- 'container.someKey',
- 'service.someKey',
- 'process.someKey',
- 'agent.someKey',
- 'url.someKey',
- 'user.someKey',
- 'transaction.custom.someKey',
- 'transaction.message.age.ms',
- 'transaction.message.queue.name',
- ]);
-
- // excluded keys
- expectTextsNotInDocument(output, [
- 'notIncluded',
- 'transaction.notIncluded',
- ]);
- });
-
- it('should render a transaction with all included values', () => {
- const transaction = getTransaction();
- const output = render(
- ,
- renderOptions
- );
-
- // included values
- expectTextsInDocument(output, [
- 'labels value',
- 'http value',
- 'host value',
- 'container value',
- 'service value',
- 'process value',
- 'agent value',
- 'url value',
- 'user value',
- 'custom value',
- '1577958057123',
- 'queue name',
- ]);
-
- // excluded values
- expectTextsNotInDocument(output, [
- 'not included value',
- 'transaction not included value',
- ]);
- });
-
- it('should render a transaction with only the required sections', () => {
- const transaction = {} as Transaction;
- const output = render(
- ,
- renderOptions
- );
-
- // required sections should be found
- expectTextsInDocument(output, ['Labels', 'User']);
-
- // optional sections should NOT be found
- expectTextsNotInDocument(output, [
- 'HTTP',
- 'Host',
- 'Container',
- 'Service',
- 'Process',
- 'Agent',
- 'URL',
- 'Custom',
- 'Message',
- ]);
- });
-});
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx
index b3a53472f0815..32c0101c73b4d 100644
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx
@@ -6,19 +6,40 @@
*/
import React, { useMemo } from 'react';
-import { TRANSACTION_METADATA_SECTIONS } from './sections';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
-import { getSectionsWithRows } from '../helper';
+import { getSectionsFromFields } from '../helper';
import { MetadataTable } from '..';
+import { ProcessorEvent } from '../../../../../common/processor_event';
+import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
interface Props {
transaction: Transaction;
}
export function TransactionMetadata({ transaction }: Props) {
- const sectionsWithRows = useMemo(
- () => getSectionsWithRows(TRANSACTION_METADATA_SECTIONS, transaction),
- [transaction]
+ const { data: transactionEvent, status } = useFetcher(
+ (callApmApi) => {
+ return callApmApi({
+ endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}',
+ params: {
+ path: {
+ processorEvent: ProcessorEvent.transaction,
+ id: transaction.transaction.id,
+ },
+ },
+ });
+ },
+ [transaction.transaction.id]
+ );
+
+ const sections = useMemo(
+ () => getSectionsFromFields(transactionEvent?.metadata || {}),
+ [transactionEvent?.metadata]
+ );
+ return (
+
);
- return ;
}
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts
deleted file mode 100644
index 2f4a3d3229857..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts
+++ /dev/null
@@ -1,47 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- Section,
- TRANSACTION,
- LABELS,
- EVENT,
- HTTP,
- HOST,
- CLIENT,
- CONTAINER,
- SERVICE,
- PROCESS,
- AGENT,
- URL,
- PAGE,
- USER,
- USER_AGENT,
- CUSTOM_TRANSACTION,
- MESSAGE_TRANSACTION,
- TRACE,
-} from '../sections';
-
-export const TRANSACTION_METADATA_SECTIONS: Section[] = [
- { ...LABELS, required: true },
- TRACE,
- TRANSACTION,
- EVENT,
- HTTP,
- HOST,
- CLIENT,
- CONTAINER,
- SERVICE,
- PROCESS,
- MESSAGE_TRANSACTION,
- AGENT,
- URL,
- { ...PAGE, key: 'transaction.page' },
- { ...USER, required: true },
- USER_AGENT,
- CUSTOM_TRANSACTION,
-];
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts
index 770b35e7d17f2..2e64c170437d8 100644
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts
@@ -5,62 +5,52 @@
* 2.0.
*/
-import { getSectionsWithRows, filterSectionsByTerm } from './helper';
-import { LABELS, HTTP, SERVICE } from './sections';
-import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
+import { filterSectionsByTerm, getSectionsFromFields } from './helper';
describe('MetadataTable Helper', () => {
- const sections = [
- { ...LABELS, required: true },
- HTTP,
- { ...SERVICE, properties: ['environment'] },
- ];
- const apmDoc = {
- http: {
- headers: {
- Connection: 'close',
- Host: 'opbeans:3000',
- request: { method: 'get' },
- },
- },
- service: {
- framework: { name: 'express' },
- environment: 'production',
- },
- } as unknown as Transaction;
- const metadataItems = getSectionsWithRows(sections, apmDoc);
+ const fields = {
+ 'http.headers.Connection': ['close'],
+ 'http.headers.Host': ['opbeans:3000'],
+ 'http.headers.request.method': ['get'],
+ 'service.framework.name': ['express'],
+ 'service.environment': ['production'],
+ };
+
+ const metadataItems = getSectionsFromFields(fields);
- it('returns flattened data and required section', () => {
+ it('returns flattened data', () => {
expect(metadataItems).toEqual([
- { key: 'labels', label: 'Labels', required: true, rows: [] },
{
key: 'http',
- label: 'HTTP',
- rows: [
- { key: 'http.headers.Connection', value: 'close' },
- { key: 'http.headers.Host', value: 'opbeans:3000' },
- { key: 'http.headers.request.method', value: 'get' },
+ label: 'http',
+ properties: [
+ { field: 'http.headers.Connection', value: ['close'] },
+ { field: 'http.headers.Host', value: ['opbeans:3000'] },
+ { field: 'http.headers.request.method', value: ['get'] },
],
},
{
key: 'service',
- label: 'Service',
- properties: ['environment'],
- rows: [{ key: 'service.environment', value: 'production' }],
+ label: 'service',
+ properties: [
+ { field: 'service.environment', value: ['production'] },
+ { field: 'service.framework.name', value: ['express'] },
+ ],
},
]);
});
+
describe('filter', () => {
it('items by key', () => {
const filteredItems = filterSectionsByTerm(metadataItems, 'http');
expect(filteredItems).toEqual([
{
key: 'http',
- label: 'HTTP',
- rows: [
- { key: 'http.headers.Connection', value: 'close' },
- { key: 'http.headers.Host', value: 'opbeans:3000' },
- { key: 'http.headers.request.method', value: 'get' },
+ label: 'http',
+ properties: [
+ { field: 'http.headers.Connection', value: ['close'] },
+ { field: 'http.headers.Host', value: ['opbeans:3000'] },
+ { field: 'http.headers.request.method', value: ['get'] },
],
},
]);
@@ -71,9 +61,8 @@ describe('MetadataTable Helper', () => {
expect(filteredItems).toEqual([
{
key: 'service',
- label: 'Service',
- properties: ['environment'],
- rows: [{ key: 'service.environment', value: 'production' }],
+ label: 'service',
+ properties: [{ field: 'service.environment', value: ['production'] }],
},
]);
});
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts
index bd115c1c7c174..c9e0f2aa66745 100644
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts
@@ -5,35 +5,52 @@
* 2.0.
*/
-import { get, pick, isEmpty } from 'lodash';
-import { Section } from './sections';
-import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
-import { APMError } from '../../../../typings/es_schemas/ui/apm_error';
-import { Span } from '../../../../typings/es_schemas/ui/span';
-import { flattenObject, KeyValuePair } from '../../../utils/flattenObject';
-
-export type SectionsWithRows = ReturnType;
-
-export const getSectionsWithRows = (
- sections: Section[],
- apmDoc: Transaction | APMError | Span
-) => {
- return sections
- .map((section) => {
- const sectionData: Record = get(apmDoc, section.key);
- const filteredData: Record | undefined =
- section.properties
- ? pick(sectionData, section.properties)
- : sectionData;
-
- const rows: KeyValuePair[] = flattenObject(filteredData, section.key);
- return { ...section, rows };
- })
- .filter(({ required, rows }) => required || !isEmpty(rows));
+import { isEmpty, groupBy, partition } from 'lodash';
+import type { SectionDescriptor } from './types';
+
+const EXCLUDED_FIELDS = ['error.exception.stacktrace', 'span.stacktrace'];
+
+export const getSectionsFromFields = (fields: Record) => {
+ const rows = Object.keys(fields)
+ .filter(
+ (field) => !EXCLUDED_FIELDS.some((excluded) => field.startsWith(excluded))
+ )
+ .sort()
+ .map((field) => {
+ return {
+ section: field.split('.')[0],
+ field,
+ value: fields[field],
+ };
+ });
+
+ const sections = Object.values(groupBy(rows, 'section')).map(
+ (rowsForSection) => {
+ const first = rowsForSection[0];
+
+ const section: SectionDescriptor = {
+ key: first.section,
+ label: first.section.toLowerCase(),
+ properties: rowsForSection.map((row) => ({
+ field: row.field,
+ value: row.value,
+ })),
+ };
+
+ return section;
+ }
+ );
+
+ const [labelSections, otherSections] = partition(
+ sections,
+ (section) => section.key === 'labels'
+ );
+
+ return [...labelSections, ...otherSections];
};
export const filterSectionsByTerm = (
- sections: SectionsWithRows,
+ sections: SectionDescriptor[],
searchTerm: string
) => {
if (!searchTerm) {
@@ -41,15 +58,16 @@ export const filterSectionsByTerm = (
}
return sections
.map((section) => {
- const { rows = [] } = section;
- const filteredRows = rows.filter(({ key, value }) => {
- const valueAsString = String(value).toLowerCase();
+ const { properties = [] } = section;
+ const filteredProps = properties.filter(({ field, value }) => {
return (
- key.toLowerCase().includes(searchTerm) ||
- valueAsString.includes(searchTerm)
+ field.toLowerCase().includes(searchTerm) ||
+ value.some((val: string | number) =>
+ String(val).toLowerCase().includes(searchTerm)
+ )
);
});
- return { ...section, rows: filteredRows };
+ return { ...section, properties: filteredProps };
})
- .filter(({ rows }) => !isEmpty(rows));
+ .filter(({ properties }) => !isEmpty(properties));
};
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx
index 45be525512d0a..248fa240fd557 100644
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx
@@ -19,18 +19,21 @@ import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import React, { useCallback } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
+import { EuiLoadingSpinner } from '@elastic/eui';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { HeightRetainer } from '../HeightRetainer';
import { fromQuery, toQuery } from '../Links/url_helpers';
-import { filterSectionsByTerm, SectionsWithRows } from './helper';
+import { filterSectionsByTerm } from './helper';
import { Section } from './Section';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
+import { SectionDescriptor } from './types';
interface Props {
- sections: SectionsWithRows;
+ sections: SectionDescriptor[];
+ isLoading: boolean;
}
-export function MetadataTable({ sections }: Props) {
+export function MetadataTable({ sections, isLoading }: Props) {
const history = useHistory();
const location = useLocation();
const { urlParams } = useUrlParams();
@@ -77,6 +80,13 @@ export function MetadataTable({ sections }: Props) {
/>
+ {isLoading && (
+
+
+
+
+
+ )}
{filteredSections.map((section) => (
@@ -84,7 +94,7 @@ export function MetadataTable({ sections }: Props) {
{section.label}
-
+
))}
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts
deleted file mode 100644
index efc2ef8bde66b..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts
+++ /dev/null
@@ -1,173 +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; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { i18n } from '@kbn/i18n';
-
-export interface Section {
- key: string;
- label: string;
- required?: boolean;
- properties?: string[];
-}
-
-export const LABELS: Section = {
- key: 'labels',
- label: i18n.translate('xpack.apm.metadataTable.section.labelsLabel', {
- defaultMessage: 'Labels',
- }),
-};
-
-export const EVENT: Section = {
- key: 'event',
- label: i18n.translate('xpack.apm.metadataTable.section.eventLabel', {
- defaultMessage: 'event',
- }),
- properties: ['outcome'],
-};
-
-export const HTTP: Section = {
- key: 'http',
- label: i18n.translate('xpack.apm.metadataTable.section.httpLabel', {
- defaultMessage: 'HTTP',
- }),
-};
-
-export const HOST: Section = {
- key: 'host',
- label: i18n.translate('xpack.apm.metadataTable.section.hostLabel', {
- defaultMessage: 'Host',
- }),
-};
-
-export const CLIENT: Section = {
- key: 'client',
- label: i18n.translate('xpack.apm.metadataTable.section.clientLabel', {
- defaultMessage: 'Client',
- }),
- properties: ['ip'],
-};
-
-export const CONTAINER: Section = {
- key: 'container',
- label: i18n.translate('xpack.apm.metadataTable.section.containerLabel', {
- defaultMessage: 'Container',
- }),
-};
-
-export const SERVICE: Section = {
- key: 'service',
- label: i18n.translate('xpack.apm.metadataTable.section.serviceLabel', {
- defaultMessage: 'Service',
- }),
-};
-
-export const PROCESS: Section = {
- key: 'process',
- label: i18n.translate('xpack.apm.metadataTable.section.processLabel', {
- defaultMessage: 'Process',
- }),
-};
-
-export const AGENT: Section = {
- key: 'agent',
- label: i18n.translate('xpack.apm.metadataTable.section.agentLabel', {
- defaultMessage: 'Agent',
- }),
-};
-
-export const URL: Section = {
- key: 'url',
- label: i18n.translate('xpack.apm.metadataTable.section.urlLabel', {
- defaultMessage: 'URL',
- }),
-};
-
-export const USER: Section = {
- key: 'user',
- label: i18n.translate('xpack.apm.metadataTable.section.userLabel', {
- defaultMessage: 'User',
- }),
-};
-
-export const USER_AGENT: Section = {
- key: 'user_agent',
- label: i18n.translate('xpack.apm.metadataTable.section.userAgentLabel', {
- defaultMessage: 'User agent',
- }),
-};
-
-export const PAGE: Section = {
- key: 'page',
- label: i18n.translate('xpack.apm.metadataTable.section.pageLabel', {
- defaultMessage: 'Page',
- }),
-};
-
-export const SPAN: Section = {
- key: 'span',
- label: i18n.translate('xpack.apm.metadataTable.section.spanLabel', {
- defaultMessage: 'Span',
- }),
- properties: ['id'],
-};
-
-export const TRANSACTION: Section = {
- key: 'transaction',
- label: i18n.translate('xpack.apm.metadataTable.section.transactionLabel', {
- defaultMessage: 'Transaction',
- }),
- properties: ['id'],
-};
-
-export const TRACE: Section = {
- key: 'trace',
- label: i18n.translate('xpack.apm.metadataTable.section.traceLabel', {
- defaultMessage: 'Trace',
- }),
- properties: ['id'],
-};
-
-export const ERROR: Section = {
- key: 'error',
- label: i18n.translate('xpack.apm.metadataTable.section.errorLabel', {
- defaultMessage: 'Error',
- }),
- properties: ['id'],
-};
-
-const customLabel = i18n.translate(
- 'xpack.apm.metadataTable.section.customLabel',
- {
- defaultMessage: 'Custom',
- }
-);
-
-export const CUSTOM_ERROR: Section = {
- key: 'error.custom',
- label: customLabel,
-};
-export const CUSTOM_TRANSACTION: Section = {
- key: 'transaction.custom',
- label: customLabel,
-};
-
-const messageLabel = i18n.translate(
- 'xpack.apm.metadataTable.section.messageLabel',
- {
- defaultMessage: 'Message',
- }
-);
-
-export const MESSAGE_TRANSACTION: Section = {
- key: 'transaction.message',
- label: messageLabel,
-};
-
-export const MESSAGE_SPAN: Section = {
- key: 'span.message',
- label: messageLabel,
-};
diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts
new file mode 100644
index 0000000000000..3ce7698460f30
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts
@@ -0,0 +1,13 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface SectionDescriptor {
+ key: string;
+ label: string;
+ required?: boolean;
+ properties: Array<{ field: string; value: string[] | number[] }>;
+}
diff --git a/x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.ts b/x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.ts
new file mode 100644
index 0000000000000..97e2e1356363f
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.ts
@@ -0,0 +1,66 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
+import {
+ ERROR_ID,
+ SPAN_ID,
+ TRANSACTION_ID,
+} from '../../../common/elasticsearch_fieldnames';
+import { ProcessorEvent } from '../../../common/processor_event';
+import type { APMEventClient } from '../helpers/create_es_client/create_apm_event_client';
+
+export async function getEventMetadata({
+ apmEventClient,
+ processorEvent,
+ id,
+}: {
+ apmEventClient: APMEventClient;
+ processorEvent: ProcessorEvent;
+ id: string;
+}) {
+ const filter: QueryDslQueryContainer[] = [];
+
+ switch (processorEvent) {
+ case ProcessorEvent.error:
+ filter.push({
+ term: { [ERROR_ID]: id },
+ });
+ break;
+
+ case ProcessorEvent.transaction:
+ filter.push({
+ term: {
+ [TRANSACTION_ID]: id,
+ },
+ });
+ break;
+
+ case ProcessorEvent.span:
+ filter.push({
+ term: { [SPAN_ID]: id },
+ });
+ break;
+ }
+
+ const response = await apmEventClient.search('get_event_metadata', {
+ apm: {
+ events: [processorEvent],
+ },
+ body: {
+ query: {
+ bool: { filter },
+ },
+ size: 1,
+ _source: false,
+ fields: [{ field: '*', include_unmapped: true }],
+ },
+ terminate_after: 1,
+ });
+
+ return response.hits.hits[0].fields;
+}
diff --git a/x-pack/plugins/apm/server/routes/event_metadata.ts b/x-pack/plugins/apm/server/routes/event_metadata.ts
new file mode 100644
index 0000000000000..8970ab8ffdeea
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/event_metadata.ts
@@ -0,0 +1,44 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import * as t from 'io-ts';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+import { createApmServerRoute } from './create_apm_server_route';
+import { getEventMetadata } from '../lib/event_metadata/get_event_metadata';
+import { processorEventRt } from '../../common/processor_event';
+import { setupRequest } from '../lib/helpers/setup_request';
+
+const eventMetadataRoute = createApmServerRoute({
+ endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}',
+ options: { tags: ['access:apm'] },
+ params: t.type({
+ path: t.type({
+ processorEvent: processorEventRt,
+ id: t.string,
+ }),
+ }),
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+
+ const {
+ path: { processorEvent, id },
+ } = resources.params;
+
+ const metadata = await getEventMetadata({
+ apmEventClient: setup.apmEventClient,
+ processorEvent,
+ id,
+ });
+
+ return {
+ metadata,
+ };
+ },
+});
+
+export const eventMetadataRouteRepository =
+ createApmServerRouteRepository().add(eventMetadataRoute);
diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
index 7aa520dd5b8a2..472e46fecfa10 100644
--- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
+++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
@@ -33,6 +33,7 @@ import { traceRouteRepository } from './traces';
import { transactionRouteRepository } from './transactions';
import { APMRouteHandlerResources } from './typings';
import { historicalDataRouteRepository } from './historical_data';
+import { eventMetadataRouteRepository } from './event_metadata';
import { suggestionsRouteRepository } from './suggestions';
const getTypedGlobalApmServerRouteRepository = () => {
@@ -58,7 +59,8 @@ const getTypedGlobalApmServerRouteRepository = () => {
.merge(apmFleetRouteRepository)
.merge(backendsRouteRepository)
.merge(fallbackToTransactionsRouteRepository)
- .merge(historicalDataRouteRepository);
+ .merge(historicalDataRouteRepository)
+ .merge(eventMetadataRouteRepository);
return repository;
};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index dcbb8ce26ee4a..bdccd8ad87760 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -6432,24 +6432,6 @@
"xpack.apm.localFilters.titles.serviceName": "サービス名",
"xpack.apm.localFilters.titles.transactionUrl": "URL",
"xpack.apm.localFiltersTitle": "フィルター",
- "xpack.apm.metadataTable.section.agentLabel": "エージェント",
- "xpack.apm.metadataTable.section.clientLabel": "クライアント",
- "xpack.apm.metadataTable.section.containerLabel": "コンテナー",
- "xpack.apm.metadataTable.section.customLabel": "カスタム",
- "xpack.apm.metadataTable.section.errorLabel": "エラー",
- "xpack.apm.metadataTable.section.hostLabel": "ホスト",
- "xpack.apm.metadataTable.section.httpLabel": "HTTP",
- "xpack.apm.metadataTable.section.labelsLabel": "ラベル",
- "xpack.apm.metadataTable.section.messageLabel": "メッセージ",
- "xpack.apm.metadataTable.section.pageLabel": "ページ",
- "xpack.apm.metadataTable.section.processLabel": "プロセス",
- "xpack.apm.metadataTable.section.serviceLabel": "サービス",
- "xpack.apm.metadataTable.section.spanLabel": "スパン",
- "xpack.apm.metadataTable.section.traceLabel": "トレース",
- "xpack.apm.metadataTable.section.transactionLabel": "トランザクション",
- "xpack.apm.metadataTable.section.urlLabel": "URL",
- "xpack.apm.metadataTable.section.userAgentLabel": "ユーザーエージェント",
- "xpack.apm.metadataTable.section.userLabel": "ユーザー",
"xpack.apm.metrics.transactionChart.machineLearningLabel": "機械学習:",
"xpack.apm.metrics.transactionChart.machineLearningTooltip": "ストリームには、平均レイテンシの想定境界が表示されます。赤色の垂直の注釈は、異常スコアが75以上の異常値を示します。",
"xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "フィルタリングで検索バーを使用しているときには、機械学習結果が表示されません",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 87c96d1efe48d..4be40151f7318 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -6483,24 +6483,6 @@
"xpack.apm.localFilters.titles.serviceName": "服务名称",
"xpack.apm.localFilters.titles.transactionUrl": "URL",
"xpack.apm.localFiltersTitle": "筛选",
- "xpack.apm.metadataTable.section.agentLabel": "代理",
- "xpack.apm.metadataTable.section.clientLabel": "客户端",
- "xpack.apm.metadataTable.section.containerLabel": "容器",
- "xpack.apm.metadataTable.section.customLabel": "定制",
- "xpack.apm.metadataTable.section.errorLabel": "错误",
- "xpack.apm.metadataTable.section.hostLabel": "主机",
- "xpack.apm.metadataTable.section.httpLabel": "HTTP",
- "xpack.apm.metadataTable.section.labelsLabel": "标签",
- "xpack.apm.metadataTable.section.messageLabel": "消息",
- "xpack.apm.metadataTable.section.pageLabel": "页",
- "xpack.apm.metadataTable.section.processLabel": "进程",
- "xpack.apm.metadataTable.section.serviceLabel": "服务",
- "xpack.apm.metadataTable.section.spanLabel": "跨度",
- "xpack.apm.metadataTable.section.traceLabel": "跟踪",
- "xpack.apm.metadataTable.section.transactionLabel": "事务",
- "xpack.apm.metadataTable.section.urlLabel": "URL",
- "xpack.apm.metadataTable.section.userAgentLabel": "用户代理",
- "xpack.apm.metadataTable.section.userLabel": "用户",
"xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning",
"xpack.apm.metrics.transactionChart.machineLearningTooltip": "流显示平均延迟的预期边界。红色垂直标注表示异常分数等于或大于 75 的异常。",
"xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "使用搜索栏筛选时,Machine Learning 结果处于隐藏状态",
diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts
index d402a74287f98..efe159b36e3d3 100644
--- a/x-pack/test/apm_api_integration/tests/index.ts
+++ b/x-pack/test/apm_api_integration/tests/index.ts
@@ -37,6 +37,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte
loadTestFile(require.resolve('./correlations/latency'));
});
+ describe('metadata/event_metadata', function () {
+ loadTestFile(require.resolve('./metadata/event_metadata'));
+ });
+
describe('metrics_charts/metrics_charts', function () {
loadTestFile(require.resolve('./metrics_charts/metrics_charts'));
});
diff --git a/x-pack/test/apm_api_integration/tests/metadata/event_metadata.ts b/x-pack/test/apm_api_integration/tests/metadata/event_metadata.ts
new file mode 100644
index 0000000000000..d979f0bad1ec6
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/metadata/event_metadata.ts
@@ -0,0 +1,129 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import expect from '@kbn/expect';
+import { PROCESSOR_EVENT } from '../../../../plugins/apm/common/elasticsearch_fieldnames';
+import { ProcessorEvent } from '../../../../plugins/apm/common/processor_event';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+import { registry } from '../../common/registry';
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const apmApiClient = getService('apmApiClient');
+ const esClient = getService('es');
+
+ async function getLastDocId(processorEvent: ProcessorEvent) {
+ const response = await esClient.search<{
+ [key: string]: { id: string };
+ }>({
+ index: ['apm-*'],
+ body: {
+ query: {
+ bool: {
+ filter: [{ term: { [PROCESSOR_EVENT]: processorEvent } }],
+ },
+ },
+ size: 1,
+ sort: {
+ '@timestamp': 'desc',
+ },
+ },
+ });
+
+ return response.body.hits.hits[0]._source![processorEvent].id;
+ }
+
+ registry.when('Event metadata', { config: 'basic', archives: ['apm_8.0.0'] }, () => {
+ it('fetches transaction metadata', async () => {
+ const id = await getLastDocId(ProcessorEvent.transaction);
+
+ const { body } = await apmApiClient.readUser({
+ endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}',
+ params: {
+ path: {
+ processorEvent: ProcessorEvent.transaction,
+ id,
+ },
+ },
+ });
+
+ expect(body).keys('metadata').ok();
+
+ expect(
+ Object.keys(body.metadata).filter((key) => {
+ return Array.isArray(body.metadata[key]);
+ })
+ );
+
+ expect(body.metadata).keys(
+ '@timestamp',
+ 'agent.name',
+ 'transaction.name',
+ 'transaction.type',
+ 'service.name'
+ );
+ });
+
+ it('fetches error metadata', async () => {
+ const id = await getLastDocId(ProcessorEvent.error);
+
+ const { body } = await apmApiClient.readUser({
+ endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}',
+ params: {
+ path: {
+ processorEvent: ProcessorEvent.error,
+ id,
+ },
+ },
+ });
+
+ expect(body).keys('metadata').ok();
+
+ expect(
+ Object.keys(body.metadata).filter((key) => {
+ return Array.isArray(body.metadata[key]);
+ })
+ );
+
+ expect(body.metadata).keys(
+ '@timestamp',
+ 'agent.name',
+ 'error.grouping_key',
+ 'error.grouping_name',
+ 'service.name'
+ );
+ });
+
+ it('fetches span metadata', async () => {
+ const id = await getLastDocId(ProcessorEvent.span);
+
+ const { body } = await apmApiClient.readUser({
+ endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}',
+ params: {
+ path: {
+ processorEvent: ProcessorEvent.span,
+ id,
+ },
+ },
+ });
+
+ expect(body).keys('metadata').ok();
+
+ expect(
+ Object.keys(body.metadata).filter((key) => {
+ return Array.isArray(body.metadata[key]);
+ })
+ );
+
+ expect(body.metadata).keys(
+ '@timestamp',
+ 'agent.name',
+ 'span.name',
+ 'span.type',
+ 'service.name'
+ );
+ });
+ });
+}
From 9920e63a25b5c719ac70ad648963f949b81dc599 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Tue, 5 Oct 2021 12:50:31 +0100
Subject: [PATCH 10/78] skip flaky suite (#60559)
---
.../apps/discover/feature_controls/discover_spaces.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts
index c245b45917497..5c6d68466dde4 100644
--- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts
+++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts
@@ -33,7 +33,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
});
- describe('space with no features disabled', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/60559
+ describe.skip('space with no features disabled', () => {
before(async () => {
// we need to load the following in every situation as deleting
// a space deletes all of the associated saved objects
From 853c5886265371a7c1f633b9d06744797455b95f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?=
Date: Tue, 5 Oct 2021 14:00:19 +0200
Subject: [PATCH 11/78] [APM] Add documentation for APM data and queries
(#113542)
---
x-pack/plugins/apm/dev_docs/apm_queries.md | 426 +++++++++++++++++++++
1 file changed, 426 insertions(+)
create mode 100644 x-pack/plugins/apm/dev_docs/apm_queries.md
diff --git a/x-pack/plugins/apm/dev_docs/apm_queries.md b/x-pack/plugins/apm/dev_docs/apm_queries.md
new file mode 100644
index 0000000000000..7d730d2ef2a77
--- /dev/null
+++ b/x-pack/plugins/apm/dev_docs/apm_queries.md
@@ -0,0 +1,426 @@
+# Transactions
+
+Transactions are stored in two different formats:
+
+#### Individual transactions document
+
+A single transaction with a latency of 2ms
+
+```json
+{
+ "@timestamp": "2021-09-01T10:00:00.000Z",
+ "processor.event": "transaction",
+ "transaction.duration.us": 2000,
+ "event.outcome": "success"
+}
+```
+
+or
+
+#### Aggregated (metric) document
+A pre-aggregated document where `_doc_count` is the number of original transactions, and `transaction.duration.histogram` is the latency distribution.
+
+```json
+{
+ "_doc_count": 2,
+ "@timestamp": "2021-09-01T10:00:00.000Z",
+ "processor.event": "metric",
+ "metricset.name": "transaction",
+ "transaction.duration.histogram": {
+ "counts": [1, 1],
+ "values": [2000, 3000]
+ },
+ "event.outcome": "success"
+}
+```
+
+The decision to use aggregated transactions or not is determined in [`getSearchAggregatedTransactions`](https://github.com/elastic/kibana/blob/a2ac439f56313b7a3fc4708f54a4deebf2615136/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts#L53-L79) and then used to [specify the index](https://github.com/elastic/kibana/blob/a2ac439f56313b7a3fc4708f54a4deebf2615136/x-pack/plugins/apm/server/lib/suggestions/get_suggestions.ts#L30-L32) and the [latency field](https://github.com/elastic/kibana/blob/a2ac439f56313b7a3fc4708f54a4deebf2615136/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts#L62-L65)
+
+### Latency
+
+Latency is the duration of a transaction. This can be calculated using transaction events or metric events (aggregated transactions).
+
+Noteworthy fields: `transaction.duration.us`, `transaction.duration.histogram`
+
+#### Transaction-based latency
+
+```json
+{
+ "size": 0,
+ "query": {
+ "bool": {
+ "filter": [{ "terms": { "processor.event": ["transaction"] } }]
+ }
+ },
+ "aggs": {
+ "latency": { "avg": { "field": "transaction.duration.us" } }
+ }
+}
+```
+
+#### Metric-based latency
+
+```json
+{
+ "size": 0,
+ "query": {
+ "bool": {
+ "filter": [
+ { "terms": { "processor.event": ["metric"] } },
+ { "term": { "metricset.name": "transaction" } }
+ ]
+ }
+ },
+ "aggs": {
+ "latency": { "avg": { "field": "transaction.duration.histogram" } }
+ }
+}
+```
+
+Please note: `metricset.name: transaction` was only recently introduced. To retain backwards compatability we still use the old filter `{ "exists": { "field": "transaction.duration.histogram" }}` when filtering for aggregated transactions.
+
+### Throughput
+
+Throughput is the number of transactions per minute. This can be calculated using transaction events or metric events (aggregated transactions).
+
+Noteworthy fields: None (based on `doc_count`)
+
+```js
+{
+ "size": 0,
+ "query": {
+ // same filters as for latency
+ },
+ "aggs": {
+ "throughput": { "rate": { "unit": "minute" } }
+ }
+}
+```
+
+### Failed transaction rate
+
+Failed transaction rate is the number of transactions with `event.outcome=failure` per minute.
+Noteworthy fields: `event.outcome`
+
+```js
+{
+ "size": 0,
+ "query": {
+ // same filters as for latency
+ },
+ "aggs": {
+ "outcomes": {
+ "terms": {
+ "field": "event.outcome",
+ "include": ["failure", "success"]
+ }
+ }
+ }
+}
+```
+
+# System metrics
+
+System metrics are captured periodically (every 60 seconds by default).
+
+### CPU
+
+
+
+Used in: [Metrics section](https://github.com/elastic/kibana/blob/00bb59713ed115343eb70d4e39059476edafbade/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts#L83)
+
+Noteworthy fields: `system.cpu.total.norm.pct`, `system.process.cpu.total.norm.pct`
+
+#### Sample document
+
+```json
+{
+ "@timestamp": "2021-09-01T10:00:00.000Z",
+ "processor.event": "metric",
+ "metricset.name": "app",
+ "system.process.cpu.total.norm.pct": 0.003,
+ "system.cpu.total.norm.pct": 0.28
+}
+```
+
+#### Query
+
+```json
+{
+ "size": 0,
+ "query": {
+ "bool": {
+ "filter": [
+ { "terms": { "processor.event": ["metric"] } },
+ { "terms": { "metricset.name": ["app"] } }
+ ]
+ }
+ },
+ "aggs": {
+ "systemCPUAverage": { "avg": { "field": "system.cpu.total.norm.pct" } },
+ "processCPUAverage": {
+ "avg": { "field": "system.process.cpu.total.norm.pct" }
+ }
+ }
+}
+```
+
+### Memory
+
+
+
+Noteworthy fields: `system.memory.actual.free`, `system.memory.total`,
+
+#### Sample document
+
+```json
+{
+ "@timestamp": "2021-09-01T10:00:00.000Z",
+ "processor.event": "metric",
+ "metricset.name": "app",
+ "system.memory.actual.free": 13182939136,
+ "system.memory.total": 15735697408
+}
+```
+
+#### Query
+
+```js
+{
+ "size": 0,
+ "query": {
+ "bool": {
+ "filter": [
+ { "terms": { "processor.event": ["metric"] }},
+ { "terms": { "metricset.name": ["app"] }}
+
+ // ensure the memory fields exists
+ { "exists": { "field": "system.memory.actual.free" }},
+ { "exists": { "field": "system.memory.total" }},
+ ]
+ }
+ },
+ "aggs": {
+ "memoryUsedAvg": {
+ "avg": {
+ "script": {
+ "lang": "expression",
+ "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']"
+ }
+ }
+ }
+ }
+}
+```
+
+Above example is overly simplified. In reality [we do a bit more](https://github.com/elastic/kibana/blob/fe9b5332e157fd456f81aecfd4ffa78d9e511a66/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts#L51-L71) to properly calculate memory usage inside containers
+
+
+
+# Transaction breakdown metrics (`transaction_breakdown`)
+
+A pre-aggregations of transaction documents where `transaction.breakdown.count` is the number of original transactions.
+
+Noteworthy fields: `transaction.name`, `transaction.type`
+
+#### Sample document
+
+```json
+{
+ "@timestamp": "2021-09-27T21:59:59.828Z",
+ "processor.event": "metric",
+ "metricset.name": "transaction_breakdown",
+ "transaction.breakdown.count": 12,
+ "transaction.name": "GET /api/products",
+ "transaction.type": "request"
+}
+}
+```
+
+# Span breakdown metrics (`span_breakdown`)
+
+A pre-aggregations of span documents where `span.self_time.count` is the number of original spans. Measures the "self-time" for a span type, and optional subtype, within a transaction group.
+
+Span breakdown metrics are used to power the "Time spent by span type" graph. Agents collect summarized metrics about the timings of spans, broken down by `span.type`.
+
+
+
+Used in: ["Time spent by span type" chart](https://github.com/elastic/kibana/blob/723370ab23573e50b3524a62c6b9998f2042423d/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts#L48-L87)
+
+Noteworthy fields: `transaction.name`, `transaction.type`, `span.type`, `span.subtype`, `span.self_time.*`
+
+#### Sample document
+
+```json
+{
+ "@timestamp": "2021-09-27T21:59:59.828Z",
+ "processor.event": "metric",
+ "metricset.name": "span_breakdown",
+ "transaction.name": "GET /api/products",
+ "transaction.type": "request",
+ "span.self_time.sum.us": 1028,
+ "span.self_time.count": 12,
+ "span.type": "db",
+ "span.subtype": "elasticsearch"
+}
+```
+
+#### Query
+
+```json
+{
+ "size": 0,
+ "query": {
+ "bool": {
+ "filter": [
+ { "terms": { "processor.event": ["metric"] } },
+ { "terms": { "metricset.name": ["span_breakdown"] } }
+ ]
+ }
+ },
+ "aggs": {
+ "total_self_time": { "sum": { "field": "span.self_time.sum.us" } },
+ "types": {
+ "terms": { "field": "span.type" },
+ "aggs": {
+ "subtypes": {
+ "terms": { "field": "span.subtype" },
+ "aggs": {
+ "self_time_per_subtype": {
+ "sum": { "field": "span.self_time.sum.us" }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+# Service destination metrics
+
+Pre-aggregations of span documents, where `span.destination.service.response_time.count` is the number of original spans.
+These metrics measure the count and total duration of requests from one service to another service.
+
+
+
+Used in: [Dependencies (latency)](https://github.com/elastic/kibana/blob/00bb59713ed115343eb70d4e39059476edafbade/x-pack/plugins/apm/server/lib/backends/get_latency_charts_for_backend.ts#L68-L79), [Dependencies (throughput)](https://github.com/elastic/kibana/blob/00bb59713ed115343eb70d4e39059476edafbade/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts#L67-L74) and [Service Map](https://github.com/elastic/kibana/blob/00bb59713ed115343eb70d4e39059476edafbade/x-pack/plugins/apm/server/lib/service_map/get_service_map_backend_node_info.ts#L57-L67)
+
+Noteworthy fields: `span.destination.service.*`
+
+#### Sample document
+
+A pre-aggregated document with 73 span requests from opbeans-ruby to elasticsearch, and a combined latency of 1554ms
+
+```json
+{
+ "@timestamp": "2021-09-01T10:00:00.000Z",
+ "processor.event": "metric",
+ "metricset.name": "service_destination",
+ "service.name": "opbeans-ruby",
+ "span.destination.service.response_time.count": 73,
+ "span.destination.service.response_time.sum.us": 1554192,
+ "span.destination.service.resource": "elasticsearch",
+ "event.outcome": "success"
+}
+```
+
+### Latency
+
+The latency between a service and an (external) endpoint
+
+```json
+{
+ "size": 0,
+ "query": {
+ "bool": {
+ "filter": [
+ { "terms": { "processor.event": ["metric"] } },
+ { "term": { "metricset.name": "service_destination" } },
+ { "term": { "span.destination.service.resource": "elasticsearch" } }
+ ]
+ }
+ },
+ "aggs": {
+ "latency_sum": {
+ "sum": { "field": "span.destination.service.response_time.sum.us" }
+ },
+ "latency_count": {
+ "sum": { "field": "span.destination.service.response_time.count" }
+ }
+ }
+}
+```
+
+### Throughput
+
+Captures the number of requests made from a service to an (external) endpoint
+
+
+#### Query
+
+```json
+{
+ "size": 0,
+ "query": {
+ "bool": {
+ "filter": [
+ { "terms": { "processor.event": ["metric"] } },
+ { "term": { "metricset.name": "service_destination" } },
+ { "term": { "span.destination.service.resource": "elasticsearch" } }
+ ]
+ }
+ },
+ "aggs": {
+ "throughput": {
+ "rate": {
+ "field": "span.destination.service.response_time.count",
+ "unit": "minute"
+ }
+ }
+ }
+}
+```
+
+## Common filters
+
+Most Elasticsearch queries will need to have one or more filters. There are a couple of reasons for adding filters:
+
+- correctness: Running an aggregation on unrelated documents will produce incorrect results
+- stability: Running an aggregation on unrelated documents could cause the entire query to fail
+- performance: limiting the number of documents will make the query faster
+
+```js
+{
+ "query": {
+ "bool": {
+ "filter": [
+ // service name
+ { "term": { "service.name": "opbeans-go" }},
+
+ // service environment
+ { "term": { "service.environment": "testing" }}
+
+ // transaction type
+ { "term": { "transaction.type": "request" }}
+
+ // event type (possible values : transaction, span, metric, error)
+ { "terms": { "processor.event": ["metric"] }},
+
+ // metric set is a subtype of `processor.event: metric`
+ { "terms": { "metricset.name": ["transaction"] }},
+
+ // time range
+ {
+ "range": {
+ "@timestamp": {
+ "gte": 1633000560000,
+ "lte": 1633001498988,
+ "format": "epoch_millis"
+ }
+ }
+ }
+ ]
+ }
+ },
+```
From 308068407d51ea5fd22eab38712599de42d1edf1 Mon Sep 17 00:00:00 2001
From: Milton Hultgren
Date: Tue, 5 Oct 2021 14:26:21 +0200
Subject: [PATCH 12/78] [Observability] Add tooltips to alert table row action
buttons (#113782)
---
.../pages/alerts/alerts_table_t_grid.tsx | 70 +++++++++++++------
1 file changed, 47 insertions(+), 23 deletions(-)
diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx
index 3b588c59260d1..ace01aa851ce8 100644
--- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx
+++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx
@@ -36,6 +36,7 @@ import {
EuiFlexItem,
EuiContextMenuPanel,
EuiPopover,
+ EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
@@ -248,40 +249,63 @@ function ObservabilityActions({
];
}, [afterCaseSelection, casePermissions, timelines, event, statusActionItems, alertPermissions]);
+ const viewDetailsTextLabel = i18n.translate(
+ 'xpack.observability.alertsTable.viewDetailsTextLabel',
+ {
+ defaultMessage: 'View details',
+ }
+ );
+ const viewInAppTextLabel = i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', {
+ defaultMessage: 'View in app',
+ });
+ const moreActionsTextLabel = i18n.translate(
+ 'xpack.observability.alertsTable.moreActionsTextLabel',
+ {
+ defaultMessage: 'More actions',
+ }
+ );
+
return (
<>
- setFlyoutAlert(alert)}
- data-test-subj="openFlyoutButton"
- />
+
+ setFlyoutAlert(alert)}
+ data-test-subj="openFlyoutButton"
+ aria-label={viewDetailsTextLabel}
+ />
+
-
+
+
+
{actionsMenuItems.length > 0 && (
toggleActionsPopover(eventId)}
- data-test-subj="alerts-table-row-action-more"
- />
+
+ toggleActionsPopover(eventId)}
+ data-test-subj="alerts-table-row-action-more"
+ />
+
}
isOpen={openActionsPopoverId === eventId}
closePopover={closeActionsPopover}
From cf6bb10bb1886cae5cd3c6e1fa4648f2adce1b54 Mon Sep 17 00:00:00 2001
From: Kevin Lacabane
Date: Tue, 5 Oct 2021 14:44:32 +0200
Subject: [PATCH 13/78] [Stack Monitoring ] Migrate kibana instances page to
react (#113874)
* kibana instances page component
* kibana instances route
* extract SetupModeProps to remove duplication
---
.../monitoring/public/application/index.tsx | 8 ++
.../application/pages/beats/instances.tsx | 8 +-
.../pages/cluster/overview_page.tsx | 7 +-
.../elasticsearch/index_advanced_page.tsx | 8 +-
.../pages/elasticsearch/index_page.tsx | 8 +-
.../pages/elasticsearch/indices_page.tsx | 8 +-
.../pages/elasticsearch/node_page.tsx | 8 +-
.../pages/elasticsearch/nodes_page.tsx | 8 +-
.../application/pages/kibana/instances.tsx | 100 ++++++++++++++++++
.../setup_mode/setup_mode_renderer.d.ts | 6 ++
10 files changed, 121 insertions(+), 48 deletions(-)
create mode 100644 x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx
diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx
index 9ecfca2f2df2e..09698b4bb56ec 100644
--- a/x-pack/plugins/monitoring/public/application/index.tsx
+++ b/x-pack/plugins/monitoring/public/application/index.tsx
@@ -25,6 +25,7 @@ import { BeatsInstancesPage } from './pages/beats/instances';
import { BeatsInstancePage } from './pages/beats/instance';
import { ApmOverviewPage, ApmInstancesPage, ApmInstancePage } from './pages/apm';
import { KibanaOverviewPage } from './pages/kibana/overview';
+import { KibanaInstancesPage } from './pages/kibana/instances';
import { ElasticsearchNodesPage } from './pages/elasticsearch/nodes_page';
import { ElasticsearchIndicesPage } from './pages/elasticsearch/indices_page';
import { ElasticsearchIndexPage } from './pages/elasticsearch/index_page';
@@ -143,6 +144,13 @@ const MonitoringApp: React.FC<{
/>
{/* Kibana Views */}
+
+
= ({ clusters }) => {
const globalState = useContext(GlobalStateContext);
const { services } = useKibana<{ data: any }>();
diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx
index 9bf0bf6d14795..3ea965f9c3bfd 100644
--- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx
@@ -14,17 +14,12 @@ import { GlobalStateContext } from '../../global_state_context';
import { TabMenuItem } from '../page_template';
import { Overview } from '../../../components/cluster/overview';
import { ExternalConfigContext } from '../../external_config_context';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants';
import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
const CODE_PATHS = [CODE_PATH_ALL];
-interface SetupModeProps {
- setupMode: any;
- flyoutComponent: any;
- bottomBarComponent: any;
-}
export const ClusterOverview: React.FC<{}> = () => {
const state = useContext(GlobalStateContext);
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
index ccaf23c7ade8e..a55e0b5df9648 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx
@@ -10,19 +10,13 @@ import { useParams } from 'react-router-dom';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { GlobalStateContext } from '../../global_state_context';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useCharts } from '../../hooks/use_charts';
import { ItemTemplate } from './item_template';
// @ts-ignore
import { AdvancedIndex } from '../../../components/elasticsearch/index/advanced';
-interface SetupModeProps {
- setupMode: any;
- flyoutComponent: any;
- bottomBarComponent: any;
-}
-
export const ElasticsearchIndexAdvancedPage: React.FC = ({ clusters }) => {
const globalState = useContext(GlobalStateContext);
const { services } = useKibana<{ data: any }>();
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
index b23f9c71a98bf..4f659f6c1354e 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx
@@ -12,7 +12,7 @@ import { GlobalStateContext } from '../../global_state_context';
// @ts-ignore
import { IndexReact } from '../../../components/elasticsearch/index/index_react';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useCharts } from '../../hooks/use_charts';
import { ItemTemplate } from './item_template';
@@ -21,12 +21,6 @@ import { indicesByNodes } from '../../../components/elasticsearch/shard_allocati
// @ts-ignore
import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels';
-interface SetupModeProps {
- setupMode: any;
- flyoutComponent: any;
- bottomBarComponent: any;
-}
-
export const ElasticsearchIndexPage: React.FC = ({ clusters }) => {
const globalState = useContext(GlobalStateContext);
const { services } = useKibana<{ data: any }>();
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx
index 9166f2090d89a..8d5f7bfebc2b3 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx
@@ -12,17 +12,11 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { GlobalStateContext } from '../../global_state_context';
import { ElasticsearchIndices } from '../../../components/elasticsearch';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useTable } from '../../hooks/use_table';
import { useLocalStorage } from '../../hooks/use_local_storage';
-interface SetupModeProps {
- setupMode: any;
- flyoutComponent: any;
- bottomBarComponent: any;
-}
-
export const ElasticsearchIndicesPage: React.FC = ({ clusters }) => {
const globalState = useContext(GlobalStateContext);
const { services } = useKibana<{ data: any }>();
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
index a8825f377eada..58acd77afc622 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx
@@ -12,7 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'
import { GlobalStateContext } from '../../global_state_context';
import { NodeReact } from '../../../components/elasticsearch';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useLocalStorage } from '../../hooks/use_local_storage';
import { useCharts } from '../../hooks/use_charts';
@@ -20,12 +20,6 @@ import { nodesByIndices } from '../../../components/elasticsearch/shard_allocati
// @ts-ignore
import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels';
-interface SetupModeProps {
- setupMode: any;
- flyoutComponent: any;
- bottomBarComponent: any;
-}
-
export const ElasticsearchNodePage: React.FC = ({ clusters }) => {
const globalState = useContext(GlobalStateContext);
const { zoomInfo, onBrush } = useCharts();
diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx
index 1fee700b4d920..d91b8b0441c59 100644
--- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx
+++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx
@@ -13,17 +13,11 @@ import { GlobalStateContext } from '../../global_state_context';
import { ExternalConfigContext } from '../../external_config_context';
import { ElasticsearchNodes } from '../../../components/elasticsearch';
import { ComponentProps } from '../../route_init';
-import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useTable } from '../../hooks/use_table';
import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
-interface SetupModeProps {
- setupMode: any;
- flyoutComponent: any;
- bottomBarComponent: any;
-}
-
export const ElasticsearchNodesPage: React.FC = ({ clusters }) => {
const globalState = useContext(GlobalStateContext);
const { showCgroupMetricsElasticsearch } = useContext(ExternalConfigContext);
diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx
new file mode 100644
index 0000000000000..12f3214b73693
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx
@@ -0,0 +1,100 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useContext, useState, useCallback, useEffect } from 'react';
+import { i18n } from '@kbn/i18n';
+import { find } from 'lodash';
+import { ComponentProps } from '../../route_init';
+import { GlobalStateContext } from '../../global_state_context';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { useTable } from '../../hooks/use_table';
+import { KibanaTemplate } from './kibana_template';
+// @ts-ignore
+import { KibanaInstances } from '../../../components/kibana/instances';
+// @ts-ignore
+import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer';
+import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
+import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
+
+export const KibanaInstancesPage: React.FC = ({ clusters }) => {
+ const { cluster_uuid: clusterUuid, ccs } = useContext(GlobalStateContext);
+ const { services } = useKibana<{ data: any }>();
+ const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context);
+ const { updateTotalItemCount, getPaginationTableProps } = useTable('kibana.instances');
+ const cluster = find(clusters, {
+ cluster_uuid: clusterUuid,
+ }) as any;
+ const [data, setData] = useState({} as any);
+
+ const title = i18n.translate('xpack.monitoring.kibana.instances.routeTitle', {
+ defaultMessage: 'Kibana - Instances',
+ });
+
+ const pageTitle = i18n.translate('xpack.monitoring.kibana.instances.pageTitle', {
+ defaultMessage: 'Kibana instances',
+ });
+
+ useEffect(() => {
+ if (cluster) {
+ generateBreadcrumbs(cluster.cluster_name, {
+ inKibana: true,
+ });
+ }
+ }, [cluster, generateBreadcrumbs]);
+
+ const getPageData = useCallback(async () => {
+ const bounds = services.data?.query.timefilter.timefilter.getBounds();
+ const url = `../api/monitoring/v1/clusters/${clusterUuid}/kibana/instances`;
+ const response = await services.http?.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify({
+ ccs,
+ timeRange: {
+ min: bounds.min.toISOString(),
+ max: bounds.max.toISOString(),
+ },
+ }),
+ });
+
+ setData(response);
+ updateTotalItemCount(response.stats.total);
+ }, [
+ ccs,
+ clusterUuid,
+ services.data?.query.timefilter.timefilter,
+ services.http,
+ updateTotalItemCount,
+ ]);
+
+ return (
+
+
+ (
+
+ {flyoutComponent}
+
+ {bottomBarComponent}
+
+ )}
+ />
+
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts
index 27462f07c07be..48e8ee13059c0 100644
--- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts
+++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts
@@ -6,3 +6,9 @@
*/
export const SetupModeRenderer: FunctionComponent;
+
+export interface SetupModeProps {
+ setupMode: any;
+ flyoutComponent: any;
+ bottomBarComponent: any;
+}
From 02ca808dc2d1e43760b30e6a57290e7e91d4523d Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Tue, 5 Oct 2021 14:02:18 +0100
Subject: [PATCH 14/78] [ML] Auto-scalable ML node integrations improvements
(#112264)
* [ML] Lazy ML node integrations improvements
* adding checks to metric and uptime
* updating callout
* adding callout to security
* cleaning up logs changes
* further clean up
* cleaning up callout code
* reverting churn
* linting
* improvements to bundle size
* adding link to cloud
* text changes
* fixing test
* translation id
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../Settings/anomaly_detection/jobs_list.tsx | 2 +
.../plugins/infra/common/infra_ml/infra_ml.ts | 5 +-
.../infra/common/log_analysis/log_analysis.ts | 5 +-
.../api/ml_get_jobs_summary_api.ts | 2 +
.../log_analysis/api/ml_setup_module_api.ts | 1 +
.../log_analysis_module_status.tsx | 14 +-
.../ml/api/ml_get_jobs_summary_api.ts | 2 +
.../containers/ml/infra_ml_module_status.tsx | 12 +-
.../page_results_content.tsx | 2 +
.../log_entry_rate/page_results_content.tsx | 2 +
.../ml/anomaly_detection/flyout_home.tsx | 21 ++-
.../jobs_awaiting_node_warning/index.ts | 1 +
.../jobs_awaiting_node_warning.tsx | 6 +-
.../new_job_awaiting_node.tsx | 6 +-
.../new_job_awaiting_node_shared/index.tsx | 8 +
.../lazy_loader.tsx | 24 +++
.../new_job_awaiting_node_shared.tsx | 164 ++++++++++++++++++
.../application/services/ml_server_info.ts | 5 +-
x-pack/plugins/ml/public/index.ts | 2 +
.../components/ml_popover/ml_popover.tsx | 8 +-
.../monitor/ml/ml_flyout_container.tsx | 9 +-
.../components/monitor/ml/translations.tsx | 8 +
.../uptime/public/state/actions/types.ts | 1 +
.../uptime/public/state/api/ml_anomaly.ts | 2 +
24 files changed, 285 insertions(+), 27 deletions(-)
create mode 100644 x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/index.tsx
create mode 100644 x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/lazy_loader.tsx
create mode 100644 x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/new_job_awaiting_node_shared.tsx
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
index 7aafb27aa18f3..13d70438ef3b0 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
@@ -25,6 +25,7 @@ import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
import { ITableColumn, ManagedTable } from '../../../shared/managed_table';
import { AnomalyDetectionApiResponse } from './index';
import { LegacyJobsCallout } from './legacy_jobs_callout';
+import { MLJobsAwaitingNodeWarning } from '../../../../../../ml/public';
type Jobs = AnomalyDetectionApiResponse['jobs'];
@@ -67,6 +68,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {
return (
<>
+ j.job_id)} />
- ['started', 'finished', 'stopped', 'failed'].includes(jobStatus);
+ ['started', 'starting', 'finished', 'stopped', 'failed'].includes(jobStatus);
export const isHealthyJobStatus = (jobStatus: JobStatus) =>
- ['started', 'finished'].includes(jobStatus);
+ ['started', 'starting', 'finished'].includes(jobStatus);
/**
* Maps a setup status to the possibility that results have already been
diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts
index 18c9f591395ee..81e1009a80585 100644
--- a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts
+++ b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts
@@ -12,6 +12,7 @@ export type JobStatus =
| 'initializing'
| 'stopped'
| 'started'
+ | 'starting'
| 'finished'
| 'failed';
@@ -35,10 +36,10 @@ export type SetupStatus =
* before this state was reached.
*/
export const isJobStatusWithResults = (jobStatus: JobStatus) =>
- ['started', 'finished', 'stopped', 'failed'].includes(jobStatus);
+ ['started', 'starting', 'finished', 'stopped', 'failed'].includes(jobStatus);
export const isHealthyJobStatus = (jobStatus: JobStatus) =>
- ['started', 'finished'].includes(jobStatus);
+ ['started', 'starting', 'finished'].includes(jobStatus);
/**
* Maps a setup status to the possibility that results have already been
diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts
index d4e1f7366dd2a..2a5f68b3c32e2 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts
@@ -41,6 +41,7 @@ export type FetchJobStatusRequestPayload = rt.TypeOf
);
const nextSetupStatus: SetupStatus = Object.values(nextJobStatus).every(
- (jobState) => jobState === 'started'
+ (jobState) => jobState === 'started' || jobState === 'starting'
)
? { type: 'succeeded' }
: {
@@ -224,9 +224,17 @@ const getJobStatus =
jobSummary.datafeedState === 'stopped'
) {
return 'stopped';
- } else if (jobSummary.jobState === 'opening') {
+ } else if (
+ jobSummary.jobState === 'opening' &&
+ jobSummary.awaitingNodeAssignment === false
+ ) {
return 'initializing';
- } else if (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') {
+ } else if (
+ (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') ||
+ (jobSummary.jobState === 'opening' &&
+ jobSummary.datafeedState === 'starting' &&
+ jobSummary.awaitingNodeAssignment === true)
+ ) {
return 'started';
}
diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts
index 585aa8786286c..c7dc1d509cf11 100644
--- a/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts
+++ b/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts
@@ -41,6 +41,7 @@ export type FetchJobStatusRequestPayload = rt.TypeOf
+
+
{
0}
- hasK8sJobs={k8sJobSummaries.length > 0}
- jobIds={jobIds}
- />
- )
+ <>
+ {tab === 'jobs' && hasJobs && (
+ <>
+ 0}
+ hasK8sJobs={k8sJobSummaries.length > 0}
+ jobIds={jobIds}
+ />
+ >
+ )}
+
+ >
}
>
{tab === 'jobs' && (
diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/index.ts b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/index.ts
index a0a79c10f3ef2..fd87a7e816f67 100644
--- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/index.ts
+++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/index.ts
@@ -7,3 +7,4 @@
export { JobsAwaitingNodeWarning } from './jobs_awaiting_node_warning';
export { NewJobAwaitingNodeWarning } from './new_job_awaiting_node';
+export { MLJobsAwaitingNodeWarning } from './new_job_awaiting_node_shared';
diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx
index 2cc36b7a2adf7..2f51fd6e32a60 100644
--- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx
+++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Fragment, FC } from 'react';
+import React, { FC } from 'react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -21,7 +21,7 @@ export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => {
}
return (
-
+ <>
= ({ jobCount }) => {
-
+ >
);
};
diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx
index ce31d1afc475b..f181497a9efc5 100644
--- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx
+++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Fragment, FC } from 'react';
+import React, { FC } from 'react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -22,7 +22,7 @@ export const NewJobAwaitingNodeWarning: FC = () => {
}
return (
-
+ <>
= () => {
-
+ >
);
};
diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/index.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/index.tsx
new file mode 100644
index 0000000000000..0457e7dd18d3c
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { MLJobsAwaitingNodeWarning } from './lazy_loader';
diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/lazy_loader.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/lazy_loader.tsx
new file mode 100644
index 0000000000000..655bde61ccc5d
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/lazy_loader.tsx
@@ -0,0 +1,24 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+
+const MLJobsAwaitingNodeWarningComponent = React.lazy(
+ () => import('./new_job_awaiting_node_shared')
+);
+
+interface Props {
+ jobIds: string[];
+}
+
+export const MLJobsAwaitingNodeWarning: FC = ({ jobIds }) => {
+ return (
+ }>
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/new_job_awaiting_node_shared.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/new_job_awaiting_node_shared.tsx
new file mode 100644
index 0000000000000..5850349ff5fd6
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared/new_job_awaiting_node_shared.tsx
@@ -0,0 +1,164 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC, useState, useEffect, useCallback, useMemo } from 'react';
+import { estypes } from '@elastic/elasticsearch';
+
+import { EuiCallOut, EuiSpacer, EuiLink } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
+import { JOB_STATE } from '../../../../../common/constants/states';
+import { mlApiServicesProvider } from '../../../services/ml_api_service';
+import { HttpService } from '../../../services/http_service';
+import { extractDeploymentId, CloudInfo } from '../../../services/ml_server_info';
+
+interface Props {
+ jobIds: string[];
+}
+
+function isJobAwaitingNodeAssignment(job: estypes.MlJobStats) {
+ return job.node === undefined && job.state === JOB_STATE.OPENING;
+}
+
+const MLJobsAwaitingNodeWarning: FC = ({ jobIds }) => {
+ const { http } = useKibana().services;
+ const ml = useMemo(() => mlApiServicesProvider(new HttpService(http!)), [http]);
+
+ const [unassignedJobCount, setUnassignedJobCount] = useState(0);
+ const [cloudInfo, setCloudInfo] = useState(null);
+
+ const checkNodes = useCallback(async () => {
+ try {
+ if (jobIds.length === 0) {
+ setUnassignedJobCount(0);
+ return;
+ }
+
+ const { lazyNodeCount } = await ml.mlNodeCount();
+ if (lazyNodeCount === 0) {
+ setUnassignedJobCount(0);
+ return;
+ }
+
+ const { jobs } = await ml.getJobStats({ jobId: jobIds.join(',') });
+ const unassignedJobs = jobs.filter(isJobAwaitingNodeAssignment);
+ setUnassignedJobCount(unassignedJobs.length);
+ } catch (error) {
+ setUnassignedJobCount(0);
+ // eslint-disable-next-line no-console
+ console.error('Could not determine ML node information', error);
+ }
+ }, [jobIds]);
+
+ const checkCloudInfo = useCallback(async () => {
+ if (unassignedJobCount === 0) {
+ return;
+ }
+
+ try {
+ const resp = await ml.mlInfo();
+ const cloudId = resp.cloudId ?? null;
+ setCloudInfo({
+ isCloud: cloudId !== null,
+ cloudId,
+ deploymentId: cloudId === null ? null : extractDeploymentId(cloudId),
+ });
+ } catch (error) {
+ setCloudInfo(null);
+ // eslint-disable-next-line no-console
+ console.error('Could not determine cloud information', error);
+ }
+ }, [unassignedJobCount]);
+
+ useEffect(() => {
+ checkCloudInfo();
+ }, [unassignedJobCount]);
+
+ useEffect(() => {
+ checkNodes();
+ }, [jobIds]);
+
+ if (unassignedJobCount === 0) {
+ return null;
+ }
+
+ return (
+ <>
+
+ }
+ color="primary"
+ iconType="iInCircle"
+ >
+
+
+
+ {cloudInfo &&
+ (cloudInfo.isCloud ? (
+ <>
+
+ {cloudInfo.deploymentId === null ? null : (
+
+
+
+ ),
+ }}
+ />
+ )}
+ >
+ ) : (
+
+
+
+ ),
+ }}
+ />
+ ))}
+
+
+
+ >
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default MLJobsAwaitingNodeWarning;
diff --git a/x-pack/plugins/ml/public/application/services/ml_server_info.ts b/x-pack/plugins/ml/public/application/services/ml_server_info.ts
index 21a1773206c43..c13ac94337490 100644
--- a/x-pack/plugins/ml/public/application/services/ml_server_info.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_server_info.ts
@@ -11,6 +11,7 @@ import { MlServerDefaults, MlServerLimits } from '../../../common/types/ml_serve
export interface CloudInfo {
cloudId: string | null;
isCloud: boolean;
+ deploymentId: string | null;
}
let defaults: MlServerDefaults = {
@@ -22,6 +23,7 @@ let limits: MlServerLimits = {};
const cloudInfo: CloudInfo = {
cloudId: null,
isCloud: false,
+ deploymentId: null,
};
export async function loadMlServerInfo() {
@@ -31,6 +33,7 @@ export async function loadMlServerInfo() {
limits = resp.limits;
cloudInfo.cloudId = resp.cloudId || null;
cloudInfo.isCloud = resp.cloudId !== undefined;
+ cloudInfo.deploymentId = !resp.cloudId ? null : extractDeploymentId(resp.cloudId);
return { defaults, limits, cloudId: cloudInfo };
} catch (error) {
return { defaults, limits, cloudId: cloudInfo };
@@ -54,7 +57,7 @@ export function isCloud(): boolean {
}
export function getCloudDeploymentId(): string | null {
- return cloudInfo.cloudId === null ? null : extractDeploymentId(cloudInfo.cloudId);
+ return cloudInfo.deploymentId;
}
export function extractDeploymentId(cloudId: string) {
diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts
index 78090c611b479..6af8b8a6c876d 100755
--- a/x-pack/plugins/ml/public/index.ts
+++ b/x-pack/plugins/ml/public/index.ts
@@ -64,3 +64,5 @@ export const getMlSharedImports = async () => {
// Helper to get Type returned by getMlSharedImports.
type AwaitReturnType = T extends PromiseLike ? U : T;
export type GetMlSharedImportsReturnType = AwaitReturnType>;
+
+export { MLJobsAwaitingNodeWarning } from './application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared';
diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx
index 6abca9cfe8853..cf5e8a5bad80a 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx
@@ -14,7 +14,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
-import React, { Dispatch, useCallback, useReducer, useState } from 'react';
+import React, { Dispatch, useCallback, useReducer, useState, useMemo } from 'react';
import styled from 'styled-components';
import { useKibana } from '../../lib/kibana';
@@ -30,6 +30,7 @@ import * as i18n from './translations';
import { JobsFilters, SecurityJob } from './types';
import { UpgradeContents } from './upgrade_contents';
import { useSecurityJobs } from './hooks/use_security_jobs';
+import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public';
const PopoverContentsDiv = styled.div`
max-width: 684px;
@@ -116,6 +117,10 @@ export const MlPopover = React.memo(() => {
});
const incompatibleJobCount = jobs.filter((j) => !j.isCompatible).length;
+ const installedJobsIds = useMemo(
+ () => jobs.filter((j) => j.isInstalled).map((j) => j.id),
+ [jobs]
+ );
if (!isLicensed) {
// If the user does not have platinum show upgrade UI
@@ -216,6 +221,7 @@ export const MlPopover = React.memo(() => {
>
)}
+
{
if (success) {
@@ -51,7 +52,9 @@ const showMLJobNotification = (
),
text: toMountPoint(
- {labels.JOB_CREATED_SUCCESS_MESSAGE}
+ {awaitingNodeAssignment
+ ? labels.JOB_CREATED_LAZY_SUCCESS_MESSAGE
+ : labels.JOB_CREATED_SUCCESS_MESSAGE}
{labels.VIEW_JOB}
@@ -107,7 +110,8 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => {
monitorId as string,
basePath,
{ to: dateRangeEnd, from: dateRangeStart },
- true
+ true,
+ hasMLJob.awaitingNodeAssignment
);
const loadMLJob = (jobId: string) =>
dispatch(getExistingMLJobAction.get({ monitorId: monitorId as string }));
@@ -123,6 +127,7 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => {
basePath,
{ to: dateRangeEnd, from: dateRangeStart },
false,
+ false,
error as Error
);
}
diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx
index 82b4006246ec7..1fc4093a67d83 100644
--- a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx
@@ -22,6 +22,14 @@ export const JOB_CREATED_SUCCESS_MESSAGE = i18n.translate(
}
);
+export const JOB_CREATED_LAZY_SUCCESS_MESSAGE = i18n.translate(
+ 'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreatedLazyNotificationText',
+ {
+ defaultMessage:
+ 'The analysis is waiting for an ML node to become available. It might take a while before results are added to the response times graph.',
+ }
+);
+
export const JOB_CREATION_FAILED = i18n.translate(
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle',
{
diff --git a/x-pack/plugins/uptime/public/state/actions/types.ts b/x-pack/plugins/uptime/public/state/actions/types.ts
index b830f81624046..754710db306e4 100644
--- a/x-pack/plugins/uptime/public/state/actions/types.ts
+++ b/x-pack/plugins/uptime/public/state/actions/types.ts
@@ -48,6 +48,7 @@ export interface MonitorDetailsActionPayload {
export interface CreateMLJobSuccess {
count: number;
jobId: string;
+ awaitingNodeAssignment: boolean;
}
export interface DeleteJobResults {
diff --git a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts
index 95784467610fb..24f2d667323d1 100644
--- a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts
+++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts
@@ -57,10 +57,12 @@ export const createMLJob = async ({
const response: DataRecognizerConfigResponse = await apiService.post(url, data);
if (response?.jobs?.[0]?.id === getMLJobId(monitorId)) {
const jobResponse = response.jobs[0];
+ const datafeedResponse = response.datafeeds[0];
if (jobResponse.success) {
return {
count: 1,
jobId: jobResponse.id,
+ awaitingNodeAssignment: datafeedResponse.awaitingMlNodeAllocation === true,
};
} else {
const { error } = jobResponse;
From 81046f171ec85efd09f2424d072193543e20517e Mon Sep 17 00:00:00 2001
From: Byron Hulcher
Date: Tue, 5 Oct 2021 09:08:03 -0400
Subject: [PATCH 15/78] [App Search] Detail Page for Automated Curations
(#113885)
---
.../components/suggestions_logic.test.tsx | 5 +-
.../components/suggestions_logic.tsx | 2 +-
.../components/curations/constants.ts | 17 +++
.../curation/automated_curation.test.tsx | 105 ++++++++++++++++++
.../curations/curation/automated_curation.tsx | 65 +++++++++++
.../curations/curation/curation.test.tsx | 73 +++---------
.../curations/curation/curation.tsx | 56 +---------
.../curations/curation/curation_logic.test.ts | 54 +++++++++
.../curations/curation/curation_logic.ts | 30 +++++
.../documents/organic_documents.test.tsx | 36 +++++-
.../curation/documents/organic_documents.tsx | 53 +++++----
.../documents/promoted_documents.test.tsx | 55 ++++++++-
.../curation/documents/promoted_documents.tsx | 71 ++++++++----
.../curation/manual_curation.test.tsx | 94 ++++++++++++++++
.../curations/curation/manual_curation.tsx | 68 ++++++++++++
.../results/add_result_button.test.tsx | 29 +++--
.../curation/results/add_result_button.tsx | 7 +-
.../app_search/components/curations/types.ts | 3 +
.../curation_suggestion_logic.test.ts | 6 +-
.../components/result/result_actions.tsx | 3 +-
.../app_search/components/result/types.ts | 1 +
.../search_relevance_suggestions.test.ts | 29 +++++
.../search_relevance_suggestions.ts | 14 +++
.../translations/translations/ja-JP.json | 2 -
.../translations/translations/zh-CN.json | 2 -
25 files changed, 702 insertions(+), 178 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx
index 5afbce3661da3..bf64101527fd2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx
@@ -16,7 +16,7 @@ import { nextTick } from '@kbn/test/jest';
import { DEFAULT_META } from '../../../../shared/constants';
-import { SuggestionsLogic } from './suggestions_logic';
+import { SuggestionsAPIResponse, SuggestionsLogic } from './suggestions_logic';
const DEFAULT_VALUES = {
dataLoading: true,
@@ -30,7 +30,7 @@ const DEFAULT_VALUES = {
},
};
-const MOCK_RESPONSE = {
+const MOCK_RESPONSE: SuggestionsAPIResponse = {
meta: {
page: {
current: 1,
@@ -44,6 +44,7 @@ const MOCK_RESPONSE = {
query: 'foo',
updated_at: '2021-07-08T14:35:50Z',
promoted: ['1', '2'],
+ status: 'applied',
},
],
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx
index 9352bdab51edd..074d2114ee8cb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.tsx
@@ -15,7 +15,7 @@ import { updateMetaPageIndex } from '../../../../shared/table_pagination';
import { EngineLogic } from '../../engine';
import { CurationSuggestion } from '../types';
-interface SuggestionsAPIResponse {
+export interface SuggestionsAPIResponse {
results: CurationSuggestion[];
meta: Meta;
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts
index f8c3e3efdbc1d..01ca80776ae85 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts
@@ -50,6 +50,13 @@ export const RESTORE_CONFIRMATION = i18n.translate(
}
);
+export const CONVERT_TO_MANUAL_CONFIRMATION = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curations.convertToManualCurationConfirmation',
+ {
+ defaultMessage: 'Are you sure you want to convert this to a manual curation?',
+ }
+);
+
export const RESULT_ACTIONS_DIRECTIONS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.resultActionsDescription',
{ defaultMessage: 'Promote results by clicking the star, hide them by clicking the eye.' }
@@ -82,3 +89,13 @@ export const SHOW_DOCUMENT_ACTION = {
iconType: 'eye',
iconColor: 'primary' as EuiButtonIconColor,
};
+
+export const AUTOMATED_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curation.automatedLabel',
+ { defaultMessage: 'Automated' }
+);
+
+export const COVERT_TO_MANUAL_BUTTON_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.curation.convertToManualCurationButtonLabel',
+ { defaultMessage: 'Convert to manual curation' }
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx
new file mode 100644
index 0000000000000..3139d62863729
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.test.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import '../../../../__mocks__/shallow_useeffect.mock';
+import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
+import { mockUseParams } from '../../../../__mocks__/react_router';
+import '../../../__mocks__/engine_logic.mock';
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { EuiBadge } from '@elastic/eui';
+
+import { getPageHeaderActions, getPageTitle } from '../../../../test_helpers';
+
+jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() }));
+
+import { AppSearchPageTemplate } from '../../layout';
+
+import { AutomatedCuration } from './automated_curation';
+import { CurationLogic } from './curation_logic';
+
+import { PromotedDocuments, OrganicDocuments } from './documents';
+
+describe('AutomatedCuration', () => {
+ const values = {
+ dataLoading: false,
+ queries: ['query A', 'query B'],
+ isFlyoutOpen: false,
+ curation: {
+ suggestion: {
+ status: 'applied',
+ },
+ },
+ activeQuery: 'query A',
+ isAutomated: true,
+ };
+
+ const actions = {
+ convertToManual: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues(values);
+ setMockActions(actions);
+ mockUseParams.mockReturnValue({ curationId: 'test' });
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.is(AppSearchPageTemplate));
+ expect(wrapper.find(PromotedDocuments)).toHaveLength(1);
+ expect(wrapper.find(OrganicDocuments)).toHaveLength(1);
+ });
+
+ it('initializes CurationLogic with a curationId prop from URL param', () => {
+ mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' });
+ shallow();
+
+ expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' });
+ });
+
+ it('displays the query in the title with a badge', () => {
+ const wrapper = shallow();
+ const pageTitle = shallow({getPageTitle(wrapper)}
);
+
+ expect(pageTitle.text()).toContain('query A');
+ expect(pageTitle.find(EuiBadge)).toHaveLength(1);
+ });
+
+ describe('convert to manual button', () => {
+ let convertToManualButton: ShallowWrapper;
+ let confirmSpy: jest.SpyInstance;
+
+ beforeAll(() => {
+ const wrapper = shallow();
+ convertToManualButton = getPageHeaderActions(wrapper).childAt(0);
+
+ confirmSpy = jest.spyOn(window, 'confirm');
+ });
+
+ afterAll(() => {
+ confirmSpy.mockRestore();
+ });
+
+ it('converts the curation upon user confirmation', () => {
+ confirmSpy.mockReturnValueOnce(true);
+ convertToManualButton.simulate('click');
+ expect(actions.convertToManual).toHaveBeenCalled();
+ });
+
+ it('does not convert the curation if the user cancels', () => {
+ confirmSpy.mockReturnValueOnce(false);
+ convertToManualButton.simulate('click');
+ expect(actions.convertToManual).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx
new file mode 100644
index 0000000000000..1415537e42d6e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useParams } from 'react-router-dom';
+
+import { useValues, useActions } from 'kea';
+
+import { EuiSpacer, EuiButton, EuiBadge } from '@elastic/eui';
+
+import { AppSearchPageTemplate } from '../../layout';
+import { AutomatedIcon } from '../components/automated_icon';
+import {
+ AUTOMATED_LABEL,
+ COVERT_TO_MANUAL_BUTTON_LABEL,
+ CONVERT_TO_MANUAL_CONFIRMATION,
+} from '../constants';
+import { getCurationsBreadcrumbs } from '../utils';
+
+import { CurationLogic } from './curation_logic';
+import { PromotedDocuments, OrganicDocuments } from './documents';
+
+export const AutomatedCuration: React.FC = () => {
+ const { curationId } = useParams<{ curationId: string }>();
+ const logic = CurationLogic({ curationId });
+ const { convertToManual } = useActions(logic);
+ const { activeQuery, dataLoading, queries } = useValues(logic);
+
+ return (
+
+ {activeQuery}{' '}
+
+ {AUTOMATED_LABEL}
+
+ >
+ ),
+ rightSideItems: [
+ {
+ if (window.confirm(CONVERT_TO_MANUAL_CONFIRMATION)) convertToManual();
+ }}
+ >
+ {COVERT_TO_MANUAL_BUTTON_LABEL}
+ ,
+ ],
+ }}
+ isLoading={dataLoading}
+ >
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx
index 2efe1f2ffe86f..62c3a6c7d4578 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx
@@ -12,26 +12,25 @@ import '../../../__mocks__/engine_logic.mock';
import React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
+import { shallow } from 'enzyme';
-import { rerender, getPageTitle, getPageHeaderActions } from '../../../../test_helpers';
+import { rerender } from '../../../../test_helpers';
jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() }));
-import { CurationLogic } from './curation_logic';
-import { AddResultFlyout } from './results';
+import { AutomatedCuration } from './automated_curation';
+
+import { ManualCuration } from './manual_curation';
import { Curation } from './';
describe('Curation', () => {
const values = {
- dataLoading: false,
- queries: ['query A', 'query B'],
- isFlyoutOpen: false,
+ isAutomated: true,
};
+
const actions = {
loadCuration: jest.fn(),
- resetCuration: jest.fn(),
};
beforeEach(() => {
@@ -40,32 +39,6 @@ describe('Curation', () => {
setMockActions(actions);
});
- it('renders', () => {
- const wrapper = shallow();
-
- expect(getPageTitle(wrapper)).toEqual('Manage curation');
- expect(wrapper.prop('pageChrome')).toEqual([
- 'Engines',
- 'some-engine',
- 'Curations',
- 'query A, query B',
- ]);
- });
-
- it('renders the add result flyout when open', () => {
- setMockValues({ ...values, isFlyoutOpen: true });
- const wrapper = shallow();
-
- expect(wrapper.find(AddResultFlyout)).toHaveLength(1);
- });
-
- it('initializes CurationLogic with a curationId prop from URL param', () => {
- mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' });
- shallow();
-
- expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' });
- });
-
it('calls loadCuration on page load & whenever the curationId URL param changes', () => {
mockUseParams.mockReturnValueOnce({ curationId: 'cur-123456789' });
const wrapper = shallow();
@@ -76,31 +49,17 @@ describe('Curation', () => {
expect(actions.loadCuration).toHaveBeenCalledTimes(2);
});
- describe('restore defaults button', () => {
- let restoreDefaultsButton: ShallowWrapper;
- let confirmSpy: jest.SpyInstance;
-
- beforeAll(() => {
- const wrapper = shallow();
- restoreDefaultsButton = getPageHeaderActions(wrapper).childAt(0);
-
- confirmSpy = jest.spyOn(window, 'confirm');
- });
+ it('renders a view for automated curations', () => {
+ setMockValues({ isAutomated: true });
+ const wrapper = shallow();
- afterAll(() => {
- confirmSpy.mockRestore();
- });
+ expect(wrapper.is(AutomatedCuration)).toBe(true);
+ });
- it('resets the curation upon user confirmation', () => {
- confirmSpy.mockReturnValueOnce(true);
- restoreDefaultsButton.simulate('click');
- expect(actions.resetCuration).toHaveBeenCalled();
- });
+ it('renders a view for manual curations', () => {
+ setMockValues({ isAutomated: false });
+ const wrapper = shallow();
- it('does not reset the curation if the user cancels', () => {
- confirmSpy.mockReturnValueOnce(false);
- restoreDefaultsButton.simulate('click');
- expect(actions.resetCuration).not.toHaveBeenCalled();
- });
+ expect(wrapper.is(ManualCuration)).toBe(true);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx
index 2a01c0db049ab..19b6542e96c4b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx
@@ -10,64 +10,18 @@ import { useParams } from 'react-router-dom';
import { useValues, useActions } from 'kea';
-import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
-
-import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../../constants';
-import { AppSearchPageTemplate } from '../../layout';
-import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants';
-import { getCurationsBreadcrumbs } from '../utils';
-
+import { AutomatedCuration } from './automated_curation';
import { CurationLogic } from './curation_logic';
-import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents';
-import { ActiveQuerySelect, ManageQueriesModal } from './queries';
-import { AddResultLogic, AddResultFlyout } from './results';
+import { ManualCuration } from './manual_curation';
export const Curation: React.FC = () => {
const { curationId } = useParams() as { curationId: string };
- const { loadCuration, resetCuration } = useActions(CurationLogic({ curationId }));
- const { dataLoading, queries } = useValues(CurationLogic({ curationId }));
- const { isFlyoutOpen } = useValues(AddResultLogic);
+ const { loadCuration } = useActions(CurationLogic({ curationId }));
+ const { isAutomated } = useValues(CurationLogic({ curationId }));
useEffect(() => {
loadCuration();
}, [curationId]);
- return (
- {
- if (window.confirm(RESTORE_CONFIRMATION)) resetCuration();
- }}
- >
- {RESTORE_DEFAULTS_BUTTON_LABEL}
-
,
- ],
- }}
- isLoading={dataLoading}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isFlyoutOpen && }
-
- );
+ return isAutomated ? : ;
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts
index 8fa57e52a26a1..941fd0bf28f96 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts
@@ -55,6 +55,7 @@ describe('CurationLogic', () => {
promotedDocumentsLoading: false,
hiddenIds: [],
hiddenDocumentsLoading: false,
+ isAutomated: false,
};
beforeEach(() => {
@@ -265,7 +266,60 @@ describe('CurationLogic', () => {
});
});
+ describe('selectors', () => {
+ describe('isAutomated', () => {
+ it('is true when suggestion status is automated', () => {
+ mount({ curation: { suggestion: { status: 'automated' } } });
+
+ expect(CurationLogic.values.isAutomated).toBe(true);
+ });
+
+ it('is false when suggestion status is not automated', () => {
+ for (status of ['pending', 'applied', 'rejected', 'disabled']) {
+ mount({ curation: { suggestion: { status } } });
+
+ expect(CurationLogic.values.isAutomated).toBe(false);
+ }
+ });
+ });
+ });
+
describe('listeners', () => {
+ describe('convertToManual', () => {
+ it('should make an API call and re-load the curation on success', async () => {
+ http.put.mockReturnValueOnce(Promise.resolve());
+ mount({ activeQuery: 'some query' });
+ jest.spyOn(CurationLogic.actions, 'loadCuration');
+
+ CurationLogic.actions.convertToManual();
+ await nextTick();
+
+ expect(http.put).toHaveBeenCalledWith(
+ '/internal/app_search/engines/some-engine/search_relevance_suggestions',
+ {
+ body: JSON.stringify([
+ {
+ query: 'some query',
+ type: 'curation',
+ status: 'applied',
+ },
+ ]),
+ }
+ );
+ expect(CurationLogic.actions.loadCuration).toHaveBeenCalled();
+ });
+
+ it('flashes any error messages', async () => {
+ http.put.mockReturnValueOnce(Promise.reject('error'));
+ mount({ activeQuery: 'some query' });
+
+ CurationLogic.actions.convertToManual();
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('error');
+ });
+ });
+
describe('loadCuration', () => {
it('should set dataLoading state', () => {
mount({ dataLoading: false }, { curationId: 'cur-123456789' });
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts
index c49fc76d06874..a9fa5ab8c1048 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.ts
@@ -27,9 +27,11 @@ interface CurationValues {
promotedDocumentsLoading: boolean;
hiddenIds: string[];
hiddenDocumentsLoading: boolean;
+ isAutomated: boolean;
}
interface CurationActions {
+ convertToManual(): void;
loadCuration(): void;
onCurationLoad(curation: Curation): { curation: Curation };
updateCuration(): void;
@@ -53,6 +55,7 @@ interface CurationProps {
export const CurationLogic = kea>({
path: ['enterprise_search', 'app_search', 'curation_logic'],
actions: () => ({
+ convertToManual: true,
loadCuration: true,
onCurationLoad: (curation) => ({ curation }),
updateCuration: true,
@@ -162,7 +165,34 @@ export const CurationLogic = kea ({
+ isAutomated: [
+ () => [selectors.curation],
+ (curation: CurationValues['curation']) => {
+ return curation.suggestion?.status === 'automated';
+ },
+ ],
+ }),
listeners: ({ actions, values, props }) => ({
+ convertToManual: async () => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
+
+ try {
+ await http.put(`/internal/app_search/engines/${engineName}/search_relevance_suggestions`, {
+ body: JSON.stringify([
+ {
+ query: values.activeQuery,
+ type: 'curation',
+ status: 'applied',
+ },
+ ]),
+ });
+ actions.loadCuration();
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
loadCuration: async () => {
const { http } = HttpLogic.values;
const { engineName } = EngineLogic.values;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx
index 0624d0063e57d..b7955cf514079 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.test.tsx
@@ -13,6 +13,8 @@ import { shallow } from 'enzyme';
import { EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui';
+import { mountWithIntl } from '../../../../../test_helpers';
+
import { DataPanel } from '../../../data_panel';
import { CurationResult } from '../results';
@@ -30,6 +32,7 @@ describe('OrganicDocuments', () => {
},
activeQuery: 'world',
organicDocumentsLoading: false,
+ isAutomated: false,
};
const actions = {
addPromotedId: jest.fn(),
@@ -56,6 +59,13 @@ describe('OrganicDocuments', () => {
expect(titleText).toEqual('Top organic documents for "world"');
});
+ it('shows a title when the curation is manual', () => {
+ setMockValues({ ...values, isAutomated: false });
+ const wrapper = shallow();
+
+ expect(wrapper.find(DataPanel).prop('subtitle')).toContain('Promote results');
+ });
+
it('renders a loading state', () => {
setMockValues({ ...values, organicDocumentsLoading: true });
const wrapper = shallow();
@@ -63,11 +73,21 @@ describe('OrganicDocuments', () => {
expect(wrapper.find(EuiLoadingContent)).toHaveLength(1);
});
- it('renders an empty state', () => {
- setMockValues({ ...values, curation: { organic: [] } });
- const wrapper = shallow();
+ describe('empty state', () => {
+ it('renders', () => {
+ setMockValues({ ...values, curation: { organic: [] } });
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ });
- expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ it('tells the user to modify the query if the curation is manual', () => {
+ setMockValues({ ...values, curation: { organic: [] }, isAutomated: false });
+ const wrapper = shallow();
+ const emptyPromptBody = mountWithIntl(<>{wrapper.find(EuiEmptyPrompt).prop('body')}>);
+
+ expect(emptyPromptBody.text()).toContain('Add or change');
+ });
});
describe('actions', () => {
@@ -86,5 +106,13 @@ describe('OrganicDocuments', () => {
expect(actions.addHiddenId).toHaveBeenCalledWith('mock-document-3');
});
+
+ it('hides actions when the curation is automated', () => {
+ setMockValues({ ...values, isAutomated: true });
+ const wrapper = shallow();
+ const result = wrapper.find(CurationResult).first();
+
+ expect(result.prop('actions')).toEqual([]);
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx
index a3a761feefcd2..7314376a4a7ab 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/organic_documents.tsx
@@ -11,6 +11,7 @@ import { useValues, useActions } from 'kea';
import { EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import { DataPanel } from '../../../data_panel';
import { Result } from '../../../result/types';
@@ -25,7 +26,7 @@ import { CurationResult } from '../results';
export const OrganicDocuments: React.FC = () => {
const { addPromotedId, addHiddenId } = useActions(CurationLogic);
- const { curation, activeQuery, organicDocumentsLoading } = useValues(CurationLogic);
+ const { curation, activeQuery, isAutomated, organicDocumentsLoading } = useValues(CurationLogic);
const documents = curation.organic;
const hasDocuments = documents.length > 0 && !organicDocumentsLoading;
@@ -46,36 +47,50 @@ export const OrganicDocuments: React.FC = () => {
)}
}
- subtitle={RESULT_ACTIONS_DIRECTIONS}
+ subtitle={!isAutomated && RESULT_ACTIONS_DIRECTIONS}
>
{hasDocuments ? (
documents.map((document: Result) => (
addHiddenId(document.id.raw),
- },
- {
- ...PROMOTE_DOCUMENT_ACTION,
- onClick: () => addPromotedId(document.id.raw),
- },
- ]}
+ actions={
+ isAutomated
+ ? []
+ : [
+ {
+ ...HIDE_DOCUMENT_ACTION,
+ onClick: () => addHiddenId(document.id.raw),
+ },
+ {
+ ...PROMOTE_DOCUMENT_ACTION,
+ onClick: () => addPromotedId(document.id.raw),
+ },
+ ]
+ }
/>
))
) : organicDocumentsLoading ? (
) : (
+ {' '}
+
+ >
+ ),
+ }}
+ />
+ }
/>
)}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx
index e0c6de973666c..a66b33a47f35c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.test.tsx
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import { setMockValues, setMockActions } from '../../../../../__mocks__/kea_logic';
import React from 'react';
@@ -13,6 +12,7 @@ import { shallow } from 'enzyme';
import { EuiDragDropContext, EuiDraggable, EuiEmptyPrompt, EuiButtonEmpty } from '@elastic/eui';
+import { mountWithIntl } from '../../../../../test_helpers';
import { DataPanel } from '../../../data_panel';
import { CurationResult } from '../results';
@@ -57,11 +57,50 @@ describe('PromotedDocuments', () => {
});
});
- it('renders an empty state & hides the panel actions when empty', () => {
+ it('informs the user documents can be re-ordered if the curation is manual', () => {
+ setMockValues({ ...values, isAutomated: false });
+ const wrapper = shallow();
+ const subtitle = mountWithIntl(wrapper.prop('subtitle'));
+
+ expect(subtitle.text()).toContain('Documents can be re-ordered');
+ });
+
+ it('informs the user the curation is managed if the curation is automated', () => {
+ setMockValues({ ...values, isAutomated: true });
+ const wrapper = shallow();
+ const subtitle = mountWithIntl(wrapper.prop('subtitle'));
+
+ expect(subtitle.text()).toContain('managed by App Search');
+ });
+
+ describe('empty state', () => {
+ it('renders', () => {
+ setMockValues({ ...values, curation: { promoted: [] } });
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ });
+
+ it('hide information about starring documents if the curation is automated', () => {
+ setMockValues({ ...values, curation: { promoted: [] }, isAutomated: true });
+ const wrapper = shallow();
+ const emptyPromptBody = mountWithIntl(<>{wrapper.find(EuiEmptyPrompt).prop('body')}>);
+
+ expect(emptyPromptBody.text()).not.toContain('Star documents');
+ });
+ });
+
+ it('hides the panel actions when empty', () => {
setMockValues({ ...values, curation: { promoted: [] } });
const wrapper = shallow();
- expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ expect(wrapper.find(DataPanel).prop('action')).toBe(false);
+ });
+
+ it('hides the panel actions when the curation is automated', () => {
+ setMockValues({ ...values, isAutomated: true });
+ const wrapper = shallow();
+
expect(wrapper.find(DataPanel).prop('action')).toBe(false);
});
@@ -81,6 +120,14 @@ describe('PromotedDocuments', () => {
expect(actions.removePromotedId).toHaveBeenCalledWith('mock-document-4');
});
+ it('hides demote button for results when the curation is automated', () => {
+ setMockValues({ ...values, isAutomated: true });
+ const wrapper = shallow();
+ const result = getDraggableChildren(wrapper.find(EuiDraggable).last());
+
+ expect(result.prop('actions')).toEqual([]);
+ });
+
it('renders a demote all button that demotes all hidden results', () => {
const wrapper = shallow();
const panelActions = shallow(wrapper.find(DataPanel).prop('action') as React.ReactElement);
@@ -89,7 +136,7 @@ describe('PromotedDocuments', () => {
expect(actions.clearPromotedIds).toHaveBeenCalled();
});
- describe('draggging', () => {
+ describe('dragging', () => {
it('calls setPromotedIds with the reordered list when users are done dragging', () => {
const wrapper = shallow();
wrapper.find(EuiDragDropContext).simulate('dragEnd', {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx
index 6b0a02aa2af58..e9d9136a45ac6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/promoted_documents.tsx
@@ -21,6 +21,7 @@ import {
euiDragDropReorder,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
import { DataPanel } from '../../../data_panel';
@@ -29,7 +30,7 @@ import { CurationLogic } from '../curation_logic';
import { AddResultButton, CurationResult, convertToResultFormat } from '../results';
export const PromotedDocuments: React.FC = () => {
- const { curation, promotedIds, promotedDocumentsLoading } = useValues(CurationLogic);
+ const { curation, isAutomated, promotedIds, promotedDocumentsLoading } = useValues(CurationLogic);
const documents = curation.promoted;
const hasDocuments = documents.length > 0;
@@ -53,21 +54,33 @@ export const PromotedDocuments: React.FC = () => {
)}
}
- subtitle={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.description',
- {
- defaultMessage:
- 'Promoted results appear before organic results. Documents can be re-ordered.',
- }
- )}
+ subtitle={
+ isAutomated ? (
+
+ ) : (
+
+ )
+ }
action={
+ !isAutomated &&
hasDocuments && (
-
+
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.removeAllButtonLabel',
{ defaultMessage: 'Demote all' }
@@ -89,17 +102,22 @@ export const PromotedDocuments: React.FC = () => {
draggableId={document.id}
customDragHandle
spacing="none"
+ isDragDisabled={isAutomated}
>
{(provided) => (
removePromotedId(document.id),
- },
- ]}
+ actions={
+ isAutomated
+ ? []
+ : [
+ {
+ ...DEMOTE_DOCUMENT_ACTION,
+ onClick: () => removePromotedId(document.id),
+ },
+ ]
+ }
dragHandleProps={provided.dragHandleProps}
/>
)}
@@ -109,13 +127,22 @@ export const PromotedDocuments: React.FC = () => {
) : (
}
/>
)}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx
new file mode 100644
index 0000000000000..ad9f3bcd64e3e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.test.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import '../../../../__mocks__/shallow_useeffect.mock';
+import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
+import { mockUseParams } from '../../../../__mocks__/react_router';
+import '../../../__mocks__/engine_logic.mock';
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { getPageTitle, getPageHeaderActions } from '../../../../test_helpers';
+
+jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() }));
+import { CurationLogic } from './curation_logic';
+
+import { ManualCuration } from './manual_curation';
+import { AddResultFlyout } from './results';
+
+describe('ManualCuration', () => {
+ const values = {
+ dataLoading: false,
+ queries: ['query A', 'query B'],
+ isFlyoutOpen: false,
+ };
+ const actions = {
+ resetCuration: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues(values);
+ setMockActions(actions);
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(getPageTitle(wrapper)).toEqual('Manage curation');
+ expect(wrapper.prop('pageChrome')).toEqual([
+ 'Engines',
+ 'some-engine',
+ 'Curations',
+ 'query A, query B',
+ ]);
+ });
+
+ it('renders the add result flyout when open', () => {
+ setMockValues({ ...values, isFlyoutOpen: true });
+ const wrapper = shallow();
+
+ expect(wrapper.find(AddResultFlyout)).toHaveLength(1);
+ });
+
+ it('initializes CurationLogic with a curationId prop from URL param', () => {
+ mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' });
+ shallow();
+
+ expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' });
+ });
+
+ describe('restore defaults button', () => {
+ let restoreDefaultsButton: ShallowWrapper;
+ let confirmSpy: jest.SpyInstance;
+
+ beforeAll(() => {
+ const wrapper = shallow();
+ restoreDefaultsButton = getPageHeaderActions(wrapper).childAt(0);
+
+ confirmSpy = jest.spyOn(window, 'confirm');
+ });
+
+ afterAll(() => {
+ confirmSpy.mockRestore();
+ });
+
+ it('resets the curation upon user confirmation', () => {
+ confirmSpy.mockReturnValueOnce(true);
+ restoreDefaultsButton.simulate('click');
+ expect(actions.resetCuration).toHaveBeenCalled();
+ });
+
+ it('does not reset the curation if the user cancels', () => {
+ confirmSpy.mockReturnValueOnce(false);
+ restoreDefaultsButton.simulate('click');
+ expect(actions.resetCuration).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx
new file mode 100644
index 0000000000000..d50575535bf21
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/manual_curation.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useParams } from 'react-router-dom';
+
+import { useValues, useActions } from 'kea';
+
+import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
+
+import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../../constants';
+import { AppSearchPageTemplate } from '../../layout';
+import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants';
+import { getCurationsBreadcrumbs } from '../utils';
+
+import { CurationLogic } from './curation_logic';
+import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents';
+import { ActiveQuerySelect, ManageQueriesModal } from './queries';
+import { AddResultLogic, AddResultFlyout } from './results';
+
+export const ManualCuration: React.FC = () => {
+ const { curationId } = useParams() as { curationId: string };
+ const { resetCuration } = useActions(CurationLogic({ curationId }));
+ const { dataLoading, queries } = useValues(CurationLogic({ curationId }));
+ const { isFlyoutOpen } = useValues(AddResultLogic);
+
+ return (
+ {
+ if (window.confirm(RESTORE_CONFIRMATION)) resetCuration();
+ }}
+ >
+ {RESTORE_DEFAULTS_BUTTON_LABEL}
+ ,
+ ],
+ }}
+ isLoading={dataLoading}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isFlyoutOpen && }
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx
index 53cefdd00c670..5b5c814a24c5b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.test.tsx
@@ -5,34 +5,43 @@
* 2.0.
*/
-import { setMockActions } from '../../../../../__mocks__/kea_logic';
+import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic';
import React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
+import { shallow } from 'enzyme';
import { EuiButton } from '@elastic/eui';
import { AddResultButton } from './';
describe('AddResultButton', () => {
+ const values = {
+ isAutomated: false,
+ };
+
const actions = {
openFlyout: jest.fn(),
};
- let wrapper: ShallowWrapper;
-
- beforeAll(() => {
- setMockActions(actions);
- wrapper = shallow();
- });
-
it('renders', () => {
- expect(wrapper.find(EuiButton)).toHaveLength(1);
+ const wrapper = shallow();
+
+ expect(wrapper.is(EuiButton)).toBe(true);
});
it('opens the add result flyout on click', () => {
+ setMockActions(actions);
+ const wrapper = shallow();
+
wrapper.find(EuiButton).simulate('click');
expect(actions.openFlyout).toHaveBeenCalled();
});
+
+ it('is disbled when the curation is automated', () => {
+ setMockValues({ ...values, isAutomated: true });
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiButton).prop('disabled')).toBe(true);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx
index 025dda65f4fb8..f2285064da307 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/add_result_button.tsx
@@ -7,18 +7,21 @@
import React from 'react';
-import { useActions } from 'kea';
+import { useActions, useValues } from 'kea';
import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { CurationLogic } from '..';
+
import { AddResultLogic } from './';
export const AddResultButton: React.FC = () => {
const { openFlyout } = useActions(AddResultLogic);
+ const { isAutomated } = useValues(CurationLogic);
return (
-
+
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.addResult.buttonLabel', {
defaultMessage: 'Add result manually',
})}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts
index 866bf6490ebe8..09c8a487b1b9b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts
@@ -12,7 +12,9 @@ export interface CurationSuggestion {
query: string;
updated_at: string;
promoted: string[];
+ status: 'pending' | 'applied' | 'automated' | 'rejected' | 'disabled';
}
+
export interface Curation {
id: string;
last_updated: string;
@@ -20,6 +22,7 @@ export interface Curation {
promoted: CurationResult[];
hidden: CurationResult[];
organic: Result[];
+ suggestion?: CurationSuggestion;
}
export interface CurationsAPIResponse {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts
index 6e616dcd9452c..9edeab4b658ef 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts
@@ -15,6 +15,8 @@ import '../../../../__mocks__/engine_logic.mock';
import { nextTick } from '@kbn/test/jest';
+import { CurationSuggestion } from '../../types';
+
import { CurationSuggestionLogic } from './curation_suggestion_logic';
const DEFAULT_VALUES = {
@@ -23,10 +25,11 @@ const DEFAULT_VALUES = {
suggestedPromotedDocuments: [],
};
-const suggestion = {
+const suggestion: CurationSuggestion = {
query: 'foo',
updated_at: '2021-07-08T14:35:50Z',
promoted: ['1', '2', '3'],
+ status: 'applied',
};
const suggestedPromotedDocuments = [
@@ -186,6 +189,7 @@ describe('CurationSuggestionLogic', () => {
query: 'foo',
updated_at: '2021-07-08T14:35:50Z',
promoted: ['1', '2', '3'],
+ status: 'applied',
},
// Note that these were re-ordered to match the 'promoted' list above, and since document
// 3 was not found it is not included in this list
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx
index 52fbee90fe31a..5eac38b88937c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_actions.tsx
@@ -18,7 +18,7 @@ interface Props {
export const ResultActions: React.FC = ({ actions }) => {
return (
- {actions.map(({ onClick, title, iconType, iconColor }) => (
+ {actions.map(({ onClick, title, iconType, iconColor, disabled }) => (
= ({ actions }) => {
color={iconColor ? iconColor : 'primary'}
aria-label={title}
title={title}
+ disabled={disabled}
/>
))}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts
index 4be3eb137177b..d9f1bb394778e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts
@@ -41,4 +41,5 @@ export interface ResultAction {
title: string;
iconType: string;
iconColor?: EuiButtonIconColor;
+ disabled?: boolean;
}
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts
index e6bfaa4a9cca2..2bdcfb9fe9d58 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.test.ts
@@ -38,6 +38,35 @@ describe('search relevance insights routes', () => {
});
});
+ describe('PUT /internal/app_search/engines/{name}/search_relevance_suggestions', () => {
+ const mockRouter = new MockRouter({
+ method: 'put',
+ path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions',
+ });
+
+ beforeEach(() => {
+ registerSearchRelevanceSuggestionsRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ mockRouter.callRoute({
+ params: { engineName: 'some-engine' },
+ body: {
+ query: 'some query',
+ type: 'curation',
+ status: 'applied',
+ },
+ });
+
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/api/as/v0/engines/:engineName/search_relevance_suggestions',
+ });
+ });
+ });
+
describe('GET /internal/app_search/engines/{name}/search_relevance_suggestions/settings', () => {
const mockRouter = new MockRouter({
method: 'get',
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts
index c6fa108a5629e..8b3b204c24d70 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_relevance_suggestions.ts
@@ -39,6 +39,20 @@ export function registerSearchRelevanceSuggestionsRoutes({
})
);
+ router.put(
+ skipBodyValidation({
+ path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions',
+ validate: {
+ params: schema.object({
+ engineName: schema.string(),
+ }),
+ },
+ }),
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/api/as/v0/engines/:engineName/search_relevance_suggestions',
+ })
+ );
+
router.get(
{
path: '/internal/app_search/engines/{engineName}/search_relevance_suggestions/settings',
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index bdccd8ad87760..6649ee32d8eb2 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9328,11 +9328,9 @@
"xpack.enterpriseSearch.appSearch.engine.curations.manageQueryButtonLabel": "クエリを管理",
"xpack.enterpriseSearch.appSearch.engine.curations.manageQueryDescription": "このキュレーションのクエリを編集、追加、削除します。",
"xpack.enterpriseSearch.appSearch.engine.curations.manageQueryTitle": "クエリを管理",
- "xpack.enterpriseSearch.appSearch.engine.curations.organicDocuments.emptyDescription": "表示するオーガニック結果はありません。上記のアクティブなクエリを追加または変更します。",
"xpack.enterpriseSearch.appSearch.engine.curations.organicDocuments.title": "\"{currentQuery}\"の上位のオーガニックドキュメント",
"xpack.enterpriseSearch.appSearch.engine.curations.overview.title": "キュレーションされた結果",
"xpack.enterpriseSearch.appSearch.engine.curations.promoteButtonLabel": "この結果を昇格",
- "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.description": "昇格された結果はオーガニック結果の前に表示されます。ドキュメントを並べ替えることができます。",
"xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.emptyDescription": "以下のオーガニック結果からドキュメントにスターを付けるか、手動で結果を検索して昇格します。",
"xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.removeAllButtonLabel": "すべて降格",
"xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.title": "昇格されたドキュメント",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 4be40151f7318..7378be3677aab 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9420,11 +9420,9 @@
"xpack.enterpriseSearch.appSearch.engine.curations.manageQueryButtonLabel": "管理查询",
"xpack.enterpriseSearch.appSearch.engine.curations.manageQueryDescription": "编辑、添加或移除此策展的查询。",
"xpack.enterpriseSearch.appSearch.engine.curations.manageQueryTitle": "管理查询",
- "xpack.enterpriseSearch.appSearch.engine.curations.organicDocuments.emptyDescription": "没有要显示的有机结果。在上面添加或更改活动查询。",
"xpack.enterpriseSearch.appSearch.engine.curations.organicDocuments.title": "“{currentQuery}”的排名靠前有机文档",
"xpack.enterpriseSearch.appSearch.engine.curations.overview.title": "已策展结果",
"xpack.enterpriseSearch.appSearch.engine.curations.promoteButtonLabel": "提升此结果",
- "xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.description": "提升结果显示在有机结果之前。可以重新排列文档。",
"xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.emptyDescription": "使用星号标记来自下面有机结果的文档或手动搜索或提升结果。",
"xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.removeAllButtonLabel": "全部降低",
"xpack.enterpriseSearch.appSearch.engine.curations.promotedDocuments.title": "提升文档",
From 433a0e3927d76a35f694ed269eaa43b77f9aa77c Mon Sep 17 00:00:00 2001
From: Nathan L Smith
Date: Tue, 5 Oct 2021 08:19:46 -0500
Subject: [PATCH 16/78] Remove APM Alerts subfeature privilege (#113469)
This was not being used (`alerts_all` and `alerts_read`) and will not be used in the future and can be safely removed.
Fixes #112274.
---
x-pack/plugins/apm/server/feature.ts | 61 ++-----------------
.../translations/translations/ja-JP.json | 3 -
.../translations/translations/zh-CN.json | 3 -
.../apis/security/privileges.ts | 2 +-
4 files changed, 7 insertions(+), 62 deletions(-)
diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts
index 0d1ed4745d00d..4c1fe784ea490 100644
--- a/x-pack/plugins/apm/server/feature.ts
+++ b/x-pack/plugins/apm/server/feature.ts
@@ -6,7 +6,6 @@
*/
import { i18n } from '@kbn/i18n';
-import { SubFeaturePrivilegeGroupType } from '../../features/common';
import { LicenseType } from '../../licensing/common/types';
import { AlertType, APM_SERVER_FEATURE_ID } from '../common/alert_types';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
@@ -39,6 +38,9 @@ export const APM_FEATURE = {
read: [],
},
alerting: {
+ alert: {
+ all: Object.values(AlertType),
+ },
rule: {
all: Object.values(AlertType),
},
@@ -57,6 +59,9 @@ export const APM_FEATURE = {
read: [],
},
alerting: {
+ alert: {
+ read: Object.values(AlertType),
+ },
rule: {
read: Object.values(AlertType),
},
@@ -67,60 +72,6 @@ export const APM_FEATURE = {
ui: ['show', 'alerting:show'],
},
},
- subFeatures: [
- {
- name: i18n.translate('xpack.apm.featureRegistry.manageAlertsName', {
- defaultMessage: 'Alerts',
- }),
- privilegeGroups: [
- {
- groupType: 'mutually_exclusive' as SubFeaturePrivilegeGroupType,
- privileges: [
- {
- id: 'alerts_all',
- name: i18n.translate(
- 'xpack.apm.featureRegistry.subfeature.alertsAllName',
- {
- defaultMessage: 'All',
- }
- ),
- includeIn: 'all' as 'all',
- alerting: {
- alert: {
- all: Object.values(AlertType),
- },
- },
- savedObject: {
- all: [],
- read: [],
- },
- ui: [],
- },
- {
- id: 'alerts_read',
- name: i18n.translate(
- 'xpack.apm.featureRegistry.subfeature.alertsReadName',
- {
- defaultMessage: 'Read',
- }
- ),
- includeIn: 'read' as 'read',
- alerting: {
- alert: {
- read: Object.values(AlertType),
- },
- },
- savedObject: {
- all: [],
- read: [],
- },
- ui: [],
- },
- ],
- },
- ],
- },
- ],
};
interface Feature {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 6649ee32d8eb2..26f021379a2a9 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -6310,9 +6310,6 @@
"xpack.apm.exactTransactionRateLabel": "{value} { unit, select, minute {tpm} other {tps} }",
"xpack.apm.failedTransactionsCorrelations.licenseCheckText": "失敗したトランザクションの相関関係機能を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。",
"xpack.apm.featureRegistry.apmFeatureName": "APMおよびユーザーエクスペリエンス",
- "xpack.apm.featureRegistry.manageAlertsName": "アラート",
- "xpack.apm.featureRegistry.subfeature.alertsAllName": "すべて",
- "xpack.apm.featureRegistry.subfeature.alertsReadName": "読み取り",
"xpack.apm.feedbackMenu.appName": "APM",
"xpack.apm.fetcher.error.status": "エラー",
"xpack.apm.fetcher.error.title": "リソースの取得中にエラーが発生しました",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 7378be3677aab..f2e5308bd6c5f 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -6359,9 +6359,6 @@
"xpack.apm.exactTransactionRateLabel": "{value} { unit, select, minute {tpm} other {tps} }",
"xpack.apm.failedTransactionsCorrelations.licenseCheckText": "要使用失败事务相关性功能,必须订阅 Elastic 白金级许可证。",
"xpack.apm.featureRegistry.apmFeatureName": "APM 和用户体验",
- "xpack.apm.featureRegistry.manageAlertsName": "告警",
- "xpack.apm.featureRegistry.subfeature.alertsAllName": "全部",
- "xpack.apm.featureRegistry.subfeature.alertsReadName": "读取",
"xpack.apm.feedbackMenu.appName": "APM",
"xpack.apm.fetcher.error.status": "错误",
"xpack.apm.fetcher.error.title": "提取资源时出错",
diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts
index 958c44df35757..762fc1642a87a 100644
--- a/x-pack/test/api_integration/apis/security/privileges.ts
+++ b/x-pack/test/api_integration/apis/security/privileges.ts
@@ -37,7 +37,7 @@ export default function ({ getService }: FtrProviderContext) {
securitySolutionCases: ['all', 'read'],
infrastructure: ['all', 'read'],
logs: ['all', 'read'],
- apm: ['all', 'read', 'minimal_all', 'minimal_read', 'alerts_all', 'alerts_read'],
+ apm: ['all', 'read'],
discover: [
'all',
'read',
From 65df961b5c45419ab490245f0e5a9809676e4f00 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 5 Oct 2021 16:31:39 +0300
Subject: [PATCH 17/78] [Dashboard] Adding timelion panel without asking for
the index pattern (#113896)
* [Dashboard] Adding timelion panel without asking for the index pattern
* Adds a functional test
---
.../public/application/top_nav/editor_menu.tsx | 5 ++++-
.../dashboard/create_and_add_embeddables.ts | 18 ++++++++++++++++++
2 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
index 0ddd0902b719f..46ae4d9456d92 100644
--- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
+++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx
@@ -128,7 +128,10 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
name: titleInWizard || title,
icon: icon as string,
onClick:
- group === VisGroups.AGGBASED ? createNewAggsBasedVis(visType) : createNewVisType(visType),
+ // not all the agg-based visualizations need to be created via the wizard
+ group === VisGroups.AGGBASED && visType.options.showIndexSelection
+ ? createNewAggsBasedVis(visType)
+ : createNewVisType(visType),
'data-test-subj': `visType-${name}`,
toolTipContent: description,
};
diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/create_and_add_embeddables.ts
index 62ce68e026f72..6af295d2cf856 100644
--- a/test/functional/apps/dashboard/create_and_add_embeddables.ts
+++ b/test/functional/apps/dashboard/create_and_add_embeddables.ts
@@ -68,6 +68,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.dashboard.waitForRenderComplete();
});
+ it('adds a new timelion visualization', async () => {
+ // adding this case, as the timelion agg-based viz doesn't need the `clickNewSearch()` step
+ const originalPanelCount = await PageObjects.dashboard.getPanelCount();
+ await dashboardAddPanel.clickEditorMenuButton();
+ await dashboardAddPanel.clickAggBasedVisualizations();
+ await PageObjects.visualize.clickTimelion();
+ await PageObjects.visualize.saveVisualizationExpectSuccess(
+ 'timelion visualization from add new link',
+ { redirectToOrigin: true }
+ );
+
+ await retry.try(async () => {
+ const panelCount = await PageObjects.dashboard.getPanelCount();
+ expect(panelCount).to.eql(originalPanelCount + 1);
+ });
+ await PageObjects.dashboard.waitForRenderComplete();
+ });
+
it('adds a markdown visualization via the quick button', async () => {
const originalPanelCount = await PageObjects.dashboard.getPanelCount();
await dashboardAddPanel.clickMarkdownQuickButton();
From a4f209e6f0cd1736a318c78165756a274e4d273b Mon Sep 17 00:00:00 2001
From: juliaElastic <90178898+juliaElastic@users.noreply.github.com>
Date: Tue, 5 Oct 2021 15:44:36 +0200
Subject: [PATCH 18/78] fix policies version overlap (#113913)
---
.../sections/epm/screens/detail/policies/package_policies.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
index 42eb68099970a..304bdd621b1b2 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
@@ -217,7 +217,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
}),
render(_version, { agentPolicy, packagePolicy }) {
return (
-
+
Date: Tue, 5 Oct 2021 15:50:44 +0200
Subject: [PATCH 19/78] [Security Solution] host isolation exceptions delete
item UI (#113541)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: David Sánchez
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../host_isolation_exceptions/service.ts | 10 ++
.../host_isolation_exceptions/store/action.ts | 18 ++-
.../store/builders.ts | 4 +
.../store/middleware.test.ts | 74 ++++++++-
.../store/middleware.ts | 48 +++++-
.../store/reducer.ts | 18 +++
.../store/selector.ts | 35 +++++
.../pages/host_isolation_exceptions/types.ts | 9 +-
.../view/components/delete_modal.test.tsx | 135 +++++++++++++++++
.../view/components/delete_modal.tsx | 141 ++++++++++++++++++
.../view/host_isolation_exceptions_list.tsx | 29 +++-
11 files changed, 512 insertions(+), 9 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts
index 85545303c7df0..79ca595fbb61b 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts
@@ -64,3 +64,13 @@ export async function getHostIsolationExceptionItems({
});
return entries;
}
+
+export async function deleteHostIsolationExceptionItems(http: HttpStart, id: string) {
+ await ensureHostIsolationExceptionsListExists(http);
+ return http.delete(EXCEPTION_LIST_ITEM_URL, {
+ query: {
+ id,
+ namespace_type: 'agnostic',
+ },
+ });
+}
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts
index 793c44ce79db2..0a9f776655371 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { Action } from 'redux';
import { HostIsolationExceptionsPageState } from '../types';
@@ -13,4 +14,19 @@ export type HostIsolationExceptionsPageDataChanged =
payload: HostIsolationExceptionsPageState['entries'];
};
-export type HostIsolationExceptionsPageAction = HostIsolationExceptionsPageDataChanged;
+export type HostIsolationExceptionsDeleteItem = Action<'hostIsolationExceptionsMarkToDelete'> & {
+ payload?: ExceptionListItemSchema;
+};
+
+export type HostIsolationExceptionsSubmitDelete = Action<'hostIsolationExceptionsSubmitDelete'>;
+
+export type HostIsolationExceptionsDeleteStatusChanged =
+ Action<'hostIsolationExceptionsDeleteStatusChanged'> & {
+ payload: HostIsolationExceptionsPageState['deletion']['status'];
+ };
+
+export type HostIsolationExceptionsPageAction =
+ | HostIsolationExceptionsPageDataChanged
+ | HostIsolationExceptionsDeleteItem
+ | HostIsolationExceptionsSubmitDelete
+ | HostIsolationExceptionsDeleteStatusChanged;
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts
index f5ea3c27bde7f..68a50f9c813f4 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts
@@ -16,4 +16,8 @@ export const initialHostIsolationExceptionsPageState = (): HostIsolationExceptio
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
filter: '',
},
+ deletion: {
+ item: undefined,
+ status: createUninitialisedResourceState(),
+ },
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts
index cde9d89443903..984794e074ebb 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts
@@ -14,8 +14,12 @@ import {
createSpyMiddleware,
MiddlewareActionSpyHelper,
} from '../../../../common/store/test_utils';
-import { isFailedResourceState, isLoadedResourceState } from '../../../state';
-import { getHostIsolationExceptionItems } from '../service';
+import {
+ isFailedResourceState,
+ isLoadedResourceState,
+ isLoadingResourceState,
+} from '../../../state';
+import { getHostIsolationExceptionItems, deleteHostIsolationExceptionItems } from '../service';
import { HostIsolationExceptionsPageState } from '../types';
import { initialHostIsolationExceptionsPageState } from './builders';
import { createHostIsolationExceptionsPageMiddleware } from './middleware';
@@ -24,6 +28,7 @@ import { getListFetchError } from './selector';
jest.mock('../service');
const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock;
+const deleteHostIsolationExceptionItemsMock = deleteHostIsolationExceptionItems as jest.Mock;
const fakeCoreStart = coreMock.createStart({ basePath: '/mock' });
@@ -139,4 +144,69 @@ describe('Host isolation exceptions middleware', () => {
});
});
});
+
+ describe('When deleting an item from host isolation exceptions', () => {
+ beforeEach(() => {
+ deleteHostIsolationExceptionItemsMock.mockClear();
+ deleteHostIsolationExceptionItemsMock.mockReturnValue(undefined);
+ getHostIsolationExceptionItemsMock.mockClear();
+ getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
+ store.dispatch({
+ type: 'hostIsolationExceptionsMarkToDelete',
+ payload: {
+ id: '1',
+ },
+ });
+ });
+
+ it('should call the delete exception API when a delete is submitted and advertise a loading status', async () => {
+ const waiter = Promise.all([
+ // delete loading action
+ spyMiddleware.waitForAction('hostIsolationExceptionsDeleteStatusChanged', {
+ validate({ payload }) {
+ return isLoadingResourceState(payload);
+ },
+ }),
+ // delete finished action
+ spyMiddleware.waitForAction('hostIsolationExceptionsDeleteStatusChanged', {
+ validate({ payload }) {
+ return isLoadedResourceState(payload);
+ },
+ }),
+ ]);
+ store.dispatch({
+ type: 'hostIsolationExceptionsSubmitDelete',
+ });
+ await waiter;
+ expect(deleteHostIsolationExceptionItemsMock).toHaveBeenLastCalledWith(
+ fakeCoreStart.http,
+ '1'
+ );
+ });
+
+ it('should dispatch a failure if the API returns an error', async () => {
+ deleteHostIsolationExceptionItemsMock.mockRejectedValue({
+ body: { message: 'error message', statusCode: 500, error: 'Internal Server Error' },
+ });
+ store.dispatch({
+ type: 'hostIsolationExceptionsSubmitDelete',
+ });
+ await spyMiddleware.waitForAction('hostIsolationExceptionsDeleteStatusChanged', {
+ validate({ payload }) {
+ return isFailedResourceState(payload);
+ },
+ });
+ });
+
+ it('should reload the host isolation exception lists after delete', async () => {
+ store.dispatch({
+ type: 'hostIsolationExceptionsSubmitDelete',
+ });
+ await spyMiddleware.waitForAction('hostIsolationExceptionsPageDataChanged', {
+ validate({ payload }) {
+ return isLoadingResourceState(payload);
+ },
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts
index 1df0ef229d2ef..4946cac488700 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts
@@ -5,8 +5,11 @@
* 2.0.
*/
-import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
-import { CoreStart, HttpStart } from 'kibana/public';
+import {
+ ExceptionListItemSchema,
+ FoundExceptionListItemSchema,
+} from '@kbn/securitysolution-io-ts-list-types';
+import { CoreStart, HttpSetup, HttpStart } from 'kibana/public';
import { matchPath } from 'react-router-dom';
import { AppLocation, Immutable } from '../../../../../common/endpoint/types';
import { ImmutableMiddleware, ImmutableMiddlewareAPI } from '../../../../common/store';
@@ -17,9 +20,9 @@ import {
createFailedResourceState,
createLoadedResourceState,
} from '../../../state/async_resource_builders';
-import { getHostIsolationExceptionItems } from '../service';
+import { deleteHostIsolationExceptionItems, getHostIsolationExceptionItems } from '../service';
import { HostIsolationExceptionsPageState } from '../types';
-import { getCurrentListPageDataState, getCurrentLocation } from './selector';
+import { getCurrentListPageDataState, getCurrentLocation, getItemToDelete } from './selector';
export const SEARCHABLE_FIELDS: Readonly = [`name`, `description`, `entries.value`];
@@ -36,6 +39,9 @@ export const createHostIsolationExceptionsPageMiddleware = (
if (action.type === 'userChangedUrl' && isHostIsolationExceptionsPage(action.payload)) {
loadHostIsolationExceptionsList(store, coreStart.http);
}
+ if (action.type === 'hostIsolationExceptionsSubmitDelete') {
+ deleteHostIsolationExceptionsItem(store, coreStart.http);
+ }
};
};
@@ -88,3 +94,37 @@ function isHostIsolationExceptionsPage(location: Immutable) {
}) !== null
);
}
+
+async function deleteHostIsolationExceptionsItem(
+ store: ImmutableMiddlewareAPI,
+ http: HttpSetup
+) {
+ const { dispatch } = store;
+ const itemToDelete = getItemToDelete(store.getState());
+ if (itemToDelete === undefined) {
+ return;
+ }
+ try {
+ dispatch({
+ type: 'hostIsolationExceptionsDeleteStatusChanged',
+ payload: {
+ type: 'LoadingResourceState',
+ // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830)
+ previousState: store.getState().deletion.status,
+ },
+ });
+
+ await deleteHostIsolationExceptionItems(http, itemToDelete.id);
+
+ dispatch({
+ type: 'hostIsolationExceptionsDeleteStatusChanged',
+ payload: createLoadedResourceState(itemToDelete),
+ });
+ loadHostIsolationExceptionsList(store, http);
+ } catch (error) {
+ dispatch({
+ type: 'hostIsolationExceptionsDeleteStatusChanged',
+ payload: createFailedResourceState(error.body ?? error),
+ });
+ }
+}
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts
index 1bce76c1bfd06..09182661a80b3 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts
@@ -16,6 +16,7 @@ import { HostIsolationExceptionsPageState } from '../types';
import { initialHostIsolationExceptionsPageState } from './builders';
import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../common/constants';
import { UserChangedUrl } from '../../../../common/store/routing/action';
+import { createUninitialisedResourceState } from '../../../state';
type StateReducer = ImmutableReducer;
type CaseReducer = (
@@ -45,6 +46,23 @@ export const hostIsolationExceptionsPageReducer: StateReducer = (
}
case 'userChangedUrl':
return userChangedUrl(state, action);
+ case 'hostIsolationExceptionsMarkToDelete': {
+ return {
+ ...state,
+ deletion: {
+ item: action.payload,
+ status: createUninitialisedResourceState(),
+ },
+ };
+ }
+ case 'hostIsolationExceptionsDeleteStatusChanged':
+ return {
+ ...state,
+ deletion: {
+ ...state.deletion,
+ status: action.payload,
+ },
+ };
}
return state;
};
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts
index 0ddfc0953263c..4462864e90702 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/selector.ts
@@ -20,6 +20,7 @@ import {
import {
getLastLoadedResourceState,
isFailedResourceState,
+ isLoadedResourceState,
isLoadingResourceState,
} from '../../../state/async_resource_state';
import { HostIsolationExceptionsPageState } from '../types';
@@ -73,3 +74,37 @@ export const getListFetchError: HostIsolationExceptionsSelector<
export const getCurrentLocation: HostIsolationExceptionsSelector = (
state
) => state.location;
+
+export const getDeletionState: HostIsolationExceptionsSelector =
+ createSelector(getCurrentListPageState, (listState) => listState.deletion);
+
+export const showDeleteModal: HostIsolationExceptionsSelector = createSelector(
+ getDeletionState,
+ ({ item }) => {
+ return Boolean(item);
+ }
+);
+
+export const getItemToDelete: HostIsolationExceptionsSelector =
+ createSelector(getDeletionState, ({ item }) => item);
+
+export const isDeletionInProgress: HostIsolationExceptionsSelector = createSelector(
+ getDeletionState,
+ ({ status }) => {
+ return isLoadingResourceState(status);
+ }
+);
+
+export const wasDeletionSuccessful: HostIsolationExceptionsSelector = createSelector(
+ getDeletionState,
+ ({ status }) => {
+ return isLoadedResourceState(status);
+ }
+);
+
+export const getDeleteError: HostIsolationExceptionsSelector =
+ createSelector(getDeletionState, ({ status }) => {
+ if (isFailedResourceState(status)) {
+ return status.error;
+ }
+ });
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts
index 44f3d2a9df764..443a86fefab83 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
-import type { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
+import type {
+ ExceptionListItemSchema,
+ FoundExceptionListItemSchema,
+} from '@kbn/securitysolution-io-ts-list-types';
import { AsyncResourceState } from '../../state/async_resource_state';
export interface HostIsolationExceptionsPageLocation {
@@ -20,4 +23,8 @@ export interface HostIsolationExceptionsPageLocation {
export interface HostIsolationExceptionsPageState {
entries: AsyncResourceState;
location: HostIsolationExceptionsPageLocation;
+ deletion: {
+ item?: ExceptionListItemSchema;
+ status: AsyncResourceState;
+ };
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx
new file mode 100644
index 0000000000000..0b09b4bfa14c4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.test.tsx
@@ -0,0 +1,135 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { act } from '@testing-library/react';
+import {
+ AppContextTestRender,
+ createAppRootMockRenderer,
+} from '../../../../../common/mock/endpoint';
+import { HostIsolationExceptionDeleteModal } from './delete_modal';
+import { isFailedResourceState, isLoadedResourceState } from '../../../../state';
+import { getHostIsolationExceptionItems, deleteHostIsolationExceptionItems } from '../../service';
+import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
+import { fireEvent } from '@testing-library/dom';
+
+jest.mock('../../service');
+const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock;
+const deleteHostIsolationExceptionItemsMock = deleteHostIsolationExceptionItems as jest.Mock;
+
+describe('When on the host isolation exceptions delete modal', () => {
+ let render: () => ReturnType;
+ let renderResult: ReturnType;
+ let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction'];
+ let coreStart: AppContextTestRender['coreStart'];
+
+ beforeEach(() => {
+ const itemToDelete = getExceptionListItemSchemaMock();
+ getHostIsolationExceptionItemsMock.mockReset();
+ deleteHostIsolationExceptionItemsMock.mockReset();
+ const mockedContext = createAppRootMockRenderer();
+ mockedContext.store.dispatch({
+ type: 'hostIsolationExceptionsMarkToDelete',
+ payload: itemToDelete,
+ });
+ render = () => (renderResult = mockedContext.render());
+ waitForAction = mockedContext.middlewareSpy.waitForAction;
+ ({ coreStart } = mockedContext);
+ });
+
+ it('should render the delete modal with the cancel and submit buttons', () => {
+ render();
+ expect(renderResult.getByTestId('hostIsolationExceptionsDeleteModalCancelButton')).toBeTruthy();
+ expect(
+ renderResult.getByTestId('hostIsolationExceptionsDeleteModalConfirmButton')
+ ).toBeTruthy();
+ });
+
+ it('should disable the buttons when confirm is pressed and show loading', async () => {
+ render();
+
+ const submitButton = renderResult.baseElement.querySelector(
+ '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]'
+ )! as HTMLButtonElement;
+
+ const cancelButton = renderResult.baseElement.querySelector(
+ '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]'
+ )! as HTMLButtonElement;
+
+ act(() => {
+ fireEvent.click(submitButton);
+ });
+
+ expect(submitButton.disabled).toBe(true);
+ expect(cancelButton.disabled).toBe(true);
+ expect(submitButton.querySelector('.euiLoadingSpinner')).not.toBeNull();
+ });
+
+ it('should clear the item marked to delete when cancel is pressed', async () => {
+ render();
+ const cancelButton = renderResult.baseElement.querySelector(
+ '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]'
+ )! as HTMLButtonElement;
+
+ const waiter = waitForAction('hostIsolationExceptionsMarkToDelete', {
+ validate: ({ payload }) => {
+ return payload === undefined;
+ },
+ });
+
+ act(() => {
+ fireEvent.click(cancelButton);
+ });
+ await waiter;
+ });
+
+ it('should show success toast after the delete is completed', async () => {
+ render();
+ const updateCompleted = waitForAction('hostIsolationExceptionsDeleteStatusChanged', {
+ validate(action) {
+ return isLoadedResourceState(action.payload);
+ },
+ });
+
+ const submitButton = renderResult.baseElement.querySelector(
+ '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]'
+ )! as HTMLButtonElement;
+
+ await act(async () => {
+ fireEvent.click(submitButton);
+ await updateCompleted;
+ });
+
+ expect(coreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith(
+ '"some name" has been removed from the Host Isolation Exceptions list.'
+ );
+ });
+
+ it('should show error toast if error is encountered', async () => {
+ deleteHostIsolationExceptionItemsMock.mockRejectedValue(
+ new Error("That's not true. That's impossible")
+ );
+ render();
+ const updateFailure = waitForAction('hostIsolationExceptionsDeleteStatusChanged', {
+ validate(action) {
+ return isFailedResourceState(action.payload);
+ },
+ });
+
+ const submitButton = renderResult.baseElement.querySelector(
+ '[data-test-subj="hostIsolationExceptionsDeleteModalConfirmButton"]'
+ )! as HTMLButtonElement;
+
+ await act(async () => {
+ fireEvent.click(submitButton);
+ await updateFailure;
+ });
+
+ expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith(
+ 'Unable to remove "some name" from the Host Isolation Exceptions list. Reason: That\'s not true. That\'s impossible'
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx
new file mode 100644
index 0000000000000..61b0bb7f930c3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/delete_modal.tsx
@@ -0,0 +1,141 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo, useCallback, useEffect } from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiText,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { useDispatch } from 'react-redux';
+import { Dispatch } from 'redux';
+import { i18n } from '@kbn/i18n';
+import { useToasts } from '../../../../../common/lib/kibana';
+import { useHostIsolationExceptionsSelector } from '../hooks';
+import {
+ getDeleteError,
+ getItemToDelete,
+ isDeletionInProgress,
+ wasDeletionSuccessful,
+} from '../../store/selector';
+import { HostIsolationExceptionsPageAction } from '../../store/action';
+
+export const HostIsolationExceptionDeleteModal = memo<{}>(() => {
+ const dispatch = useDispatch>();
+ const toasts = useToasts();
+
+ const isDeleting = useHostIsolationExceptionsSelector(isDeletionInProgress);
+ const exception = useHostIsolationExceptionsSelector(getItemToDelete);
+ const wasDeleted = useHostIsolationExceptionsSelector(wasDeletionSuccessful);
+ const deleteError = useHostIsolationExceptionsSelector(getDeleteError);
+
+ const onCancel = useCallback(() => {
+ dispatch({ type: 'hostIsolationExceptionsMarkToDelete', payload: undefined });
+ }, [dispatch]);
+
+ const onConfirm = useCallback(() => {
+ dispatch({ type: 'hostIsolationExceptionsSubmitDelete' });
+ }, [dispatch]);
+
+ // Show toast for success
+ useEffect(() => {
+ if (wasDeleted) {
+ toasts.addSuccess(
+ i18n.translate(
+ 'xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteSuccess',
+ {
+ defaultMessage: '"{name}" has been removed from the Host Isolation Exceptions list.',
+ values: { name: exception?.name },
+ }
+ )
+ );
+
+ dispatch({ type: 'hostIsolationExceptionsMarkToDelete', payload: undefined });
+ }
+ }, [dispatch, exception?.name, toasts, wasDeleted]);
+
+ // show toast for failures
+ useEffect(() => {
+ if (deleteError) {
+ toasts.addDanger(
+ i18n.translate(
+ 'xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteFailure',
+ {
+ defaultMessage:
+ 'Unable to remove "{name}" from the Host Isolation Exceptions list. Reason: {message}',
+ values: { name: exception?.name, message: deleteError.message },
+ }
+ )
+ );
+ }
+ }, [deleteError, exception?.name, toasts]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {exception?.name} }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+HostIsolationExceptionDeleteModal.displayName = 'HostIsolationExceptionDeleteModal';
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx
index f6198e4e1aa54..53fb74d5bd8f7 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx
@@ -7,12 +7,14 @@
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { i18n } from '@kbn/i18n';
-import React, { useCallback } from 'react';
+import React, { Dispatch, useCallback } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { useDispatch } from 'react-redux';
import { ExceptionItem } from '../../../../common/components/exceptions/viewer/exception_item';
import {
getCurrentLocation,
+ getItemToDelete,
getListFetchError,
getListIsLoading,
getListItems,
@@ -28,18 +30,29 @@ import { AdministrationListPage } from '../../../components/administration_list_
import { SearchExceptions } from '../../../components/search_exceptions';
import { ArtifactEntryCard, ArtifactEntryCardProps } from '../../../components/artifact_entry_card';
import { HostIsolationExceptionsEmptyState } from './components/empty';
+import { HostIsolationExceptionsPageAction } from '../store/action';
+import { HostIsolationExceptionDeleteModal } from './components/delete_modal';
type HostIsolationExceptionPaginatedContent = PaginatedContentProps<
Immutable,
typeof ExceptionItem
>;
+const DELETE_HOST_ISOLATION_EXCEPTION_LABEL = i18n.translate(
+ 'xpack.securitySolution.hostIsolationExceptions.list.actions.delete',
+ {
+ defaultMessage: 'Delete Exception',
+ }
+);
+
export const HostIsolationExceptionsList = () => {
const listItems = useHostIsolationExceptionsSelector(getListItems);
const pagination = useHostIsolationExceptionsSelector(getListPagination);
const isLoading = useHostIsolationExceptionsSelector(getListIsLoading);
const fetchError = useHostIsolationExceptionsSelector(getListFetchError);
const location = useHostIsolationExceptionsSelector(getCurrentLocation);
+ const dispatch = useDispatch>();
+ const itemToDelete = useHostIsolationExceptionsSelector(getItemToDelete);
const navigateCallback = useHostIsolationExceptionsNavigateCallback();
@@ -53,6 +66,19 @@ export const HostIsolationExceptionsList = () => {
const handleItemComponentProps = (element: ExceptionListItemSchema): ArtifactEntryCardProps => ({
item: element,
'data-test-subj': `hostIsolationExceptionsCard`,
+ actions: [
+ {
+ icon: 'trash',
+ onClick: () => {
+ dispatch({
+ type: 'hostIsolationExceptionsMarkToDelete',
+ payload: element,
+ });
+ },
+ 'data-test-subj': 'deleteHostIsolationException',
+ children: DELETE_HOST_ISOLATION_EXCEPTION_LABEL,
+ },
+ ],
});
const handlePaginatedContentChange: HostIsolationExceptionPaginatedContent['onChange'] =
@@ -87,6 +113,7 @@ export const HostIsolationExceptionsList = () => {
)}
/>
+ {itemToDelete ? : null}
items={listItems}
ItemComponent={ArtifactEntryCard}
From cc00f3f279dd624934d5cb81e6de63d8d1150bd0 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Tue, 5 Oct 2021 10:02:46 -0400
Subject: [PATCH 20/78] Update kibana plugin, platform and package overview doc
(#112894)
* udpate
* Update kibana_platform_plugin_intro.mdx
* Update kibana_platform_plugin_intro.mdx
* Update kibana_platform_plugin_intro.mdx
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
dev_docs/assets/1000_ft_arch.png | Bin 0 -> 80269 bytes
.../kibana_platform_plugin_intro.mdx | 67 +++++++++---------
2 files changed, 33 insertions(+), 34 deletions(-)
create mode 100644 dev_docs/assets/1000_ft_arch.png
diff --git a/dev_docs/assets/1000_ft_arch.png b/dev_docs/assets/1000_ft_arch.png
new file mode 100644
index 0000000000000000000000000000000000000000..715c830606d76bd8b53adf7d4f6340677c43e28a
GIT binary patch
literal 80269
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 |