diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 974afddddef6aa..43ff2c0fa9791e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -788,6 +788,9 @@ packages/core/lifecycle/core-lifecycle-browser-mocks @elastic/kibana-core packages/core/lifecycle/core-lifecycle-server @elastic/kibana-core packages/core/lifecycle/core-lifecycle-server-internal @elastic/kibana-core packages/core/lifecycle/core-lifecycle-server-mocks @elastic/kibana-core +packages/core/logging/core-logging-browser-internal @elastic/kibana-core +packages/core/logging/core-logging-browser-mocks @elastic/kibana-core +packages/core/logging/core-logging-common-internal @elastic/kibana-core packages/core/logging/core-logging-server @elastic/kibana-core packages/core/logging/core-logging-server-internal @elastic/kibana-core packages/core/logging/core-logging-server-mocks @elastic/kibana-core diff --git a/package.json b/package.json index 1bb692e36df8de..a63be94afb18f8 100644 --- a/package.json +++ b/package.json @@ -243,6 +243,9 @@ "@kbn/core-lifecycle-server": "link:bazel-bin/packages/core/lifecycle/core-lifecycle-server", "@kbn/core-lifecycle-server-internal": "link:bazel-bin/packages/core/lifecycle/core-lifecycle-server-internal", "@kbn/core-lifecycle-server-mocks": "link:bazel-bin/packages/core/lifecycle/core-lifecycle-server-mocks", + "@kbn/core-logging-browser-internal": "link:bazel-bin/packages/core/logging/core-logging-browser-internal", + "@kbn/core-logging-browser-mocks": "link:bazel-bin/packages/core/logging/core-logging-browser-mocks", + "@kbn/core-logging-common-internal": "link:bazel-bin/packages/core/logging/core-logging-common-internal", "@kbn/core-logging-server": "link:bazel-bin/packages/core/logging/core-logging-server", "@kbn/core-logging-server-internal": "link:bazel-bin/packages/core/logging/core-logging-server-internal", "@kbn/core-logging-server-mocks": "link:bazel-bin/packages/core/logging/core-logging-server-mocks", @@ -558,7 +561,7 @@ "moment": "^2.29.4", "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.34", - "monaco-editor": "^0.22.3", + "monaco-editor": "^0.24.0", "mustache": "^2.3.2", "node-fetch": "^2.6.7", "node-forge": "^1.3.1", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 3dc520d9a824b3..f01c019499c712 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -108,6 +108,9 @@ filegroup( "//packages/core/lifecycle/core-lifecycle-server:build", "//packages/core/lifecycle/core-lifecycle-server-internal:build", "//packages/core/lifecycle/core-lifecycle-server-mocks:build", + "//packages/core/logging/core-logging-browser-internal:build", + "//packages/core/logging/core-logging-browser-mocks:build", + "//packages/core/logging/core-logging-common-internal:build", "//packages/core/logging/core-logging-server:build", "//packages/core/logging/core-logging-server-internal:build", "//packages/core/logging/core-logging-server-mocks:build", @@ -463,6 +466,9 @@ filegroup( "//packages/core/lifecycle/core-lifecycle-server:build_types", "//packages/core/lifecycle/core-lifecycle-server-internal:build_types", "//packages/core/lifecycle/core-lifecycle-server-mocks:build_types", + "//packages/core/logging/core-logging-browser-internal:build_types", + "//packages/core/logging/core-logging-browser-mocks:build_types", + "//packages/core/logging/core-logging-common-internal:build_types", "//packages/core/logging/core-logging-server:build_types", "//packages/core/logging/core-logging-server-internal:build_types", "//packages/core/logging/core-logging-server-mocks:build_types", diff --git a/packages/core/base/core-base-browser-internal/src/core_context.ts b/packages/core/base/core-base-browser-internal/src/core_context.ts index cf981dd7524539..c5cd4303f5b3da 100644 --- a/packages/core/base/core-base-browser-internal/src/core_context.ts +++ b/packages/core/base/core-base-browser-internal/src/core_context.ts @@ -7,11 +7,13 @@ */ import type { EnvironmentMode, PackageInfo } from '@kbn/config'; +import type { LoggerFactory } from '@kbn/logging'; import type { CoreId } from '@kbn/core-base-common-internal'; /** @internal */ export interface CoreContext { coreId: CoreId; + logger: LoggerFactory; env: { mode: Readonly; packageInfo: Readonly; diff --git a/packages/core/base/core-base-browser-mocks/BUILD.bazel b/packages/core/base/core-base-browser-mocks/BUILD.bazel index 28088cfd13dd98..4eefc60344077e 100644 --- a/packages/core/base/core-base-browser-mocks/BUILD.bazel +++ b/packages/core/base/core-base-browser-mocks/BUILD.bazel @@ -35,12 +35,14 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ + "//packages/kbn-logging-mocks", ] TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/jest", - "//packages/core/base/core-base-browser-internal:npm_module_types", + "//packages/kbn-logging-mocks:npm_module_types", + "//packages/core/base/core-base-browser-internal:npm_module_types", ] jsts_transpiler( diff --git a/packages/core/base/core-base-browser-mocks/src/core_context.mock.ts b/packages/core/base/core-base-browser-mocks/src/core_context.mock.ts index e43efe1246ffaa..e871621b909ef1 100644 --- a/packages/core/base/core-base-browser-mocks/src/core_context.mock.ts +++ b/packages/core/base/core-base-browser-mocks/src/core_context.mock.ts @@ -6,11 +6,13 @@ * Side Public License, v 1. */ +import { loggerMock } from '@kbn/logging-mocks'; import type { CoreContext } from '@kbn/core-base-browser-internal'; function createCoreContext({ production = false }: { production?: boolean } = {}): CoreContext { return { coreId: Symbol('core context mock'), + logger: loggerMock.create(), env: { mode: { dev: !production, diff --git a/packages/core/logging/core-logging-browser-internal/BUILD.bazel b/packages/core/logging/core-logging-browser-internal/BUILD.bazel new file mode 100644 index 00000000000000..b707b68279e4bf --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/BUILD.bazel @@ -0,0 +1,114 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-logging-browser-internal" +PKG_REQUIRE_NAME = "@kbn/core-logging-browser-internal" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "//packages/core/logging/core-logging-common-internal", +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "//packages/kbn-logging:npm_module_types", + "//packages/core/logging/core-logging-common-internal:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +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, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +js_library( + name = "npm_module_types", + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "build_types", + deps = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/logging/core-logging-browser-internal/README.md b/packages/core/logging/core-logging-browser-internal/README.md new file mode 100644 index 00000000000000..7888115e20cbef --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/README.md @@ -0,0 +1,3 @@ +# @kbn/core-logging-browser-internal + +This package contains the internal types and implementation for Core's browser-side logging service. diff --git a/packages/core/logging/core-logging-browser-internal/index.ts b/packages/core/logging/core-logging-browser-internal/index.ts new file mode 100644 index 00000000000000..f757b7f6ce38e7 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { BaseLogger, BrowserLoggingSystem, type IBrowserLoggingSystem } from './src'; diff --git a/packages/core/logging/core-logging-browser-internal/jest.config.js b/packages/core/logging/core-logging-browser-internal/jest.config.js new file mode 100644 index 00000000000000..aec2a0f4d8e2dd --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/jest.config.js @@ -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 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/core/logging/core-logging-browser-internal'], +}; diff --git a/packages/core/logging/core-logging-browser-internal/kibana.jsonc b/packages/core/logging/core-logging-browser-internal/kibana.jsonc new file mode 100644 index 00000000000000..6d60078e34da8d --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/core-logging-browser-internal", + "owner": "@elastic/kibana-core", + "runtimeDeps": [], + "typeDeps": [], +} diff --git a/packages/core/logging/core-logging-browser-internal/package.json b/packages/core/logging/core-logging-browser-internal/package.json new file mode 100644 index 00000000000000..56cf9d28f32b2e --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/core-logging-browser-internal", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "author": "Kibana Core", + "license": "SSPL-1.0 OR Elastic License 2.0", + "types": "./target_types/index.d.ts" +} diff --git a/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.test.ts b/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.test.ts new file mode 100644 index 00000000000000..8b8900be8e0358 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { LogRecord, LogLevel } from '@kbn/logging'; +import { ConsoleAppender } from './console_appender'; + +test('`append()` correctly formats records and pushes them to console.', () => { + jest.spyOn(global.console, 'log').mockImplementation(() => { + // noop + }); + + const records: LogRecord[] = [ + { + context: 'context-1', + level: LogLevel.All, + message: 'message-1', + timestamp: new Date(), + pid: 5355, + }, + { + context: 'context-2', + level: LogLevel.Trace, + message: 'message-2', + timestamp: new Date(), + pid: 5355, + }, + { + context: 'context-3', + error: new Error('Error'), + level: LogLevel.Fatal, + message: 'message-3', + timestamp: new Date(), + pid: 5355, + }, + ]; + + const appender = new ConsoleAppender({ + format(record) { + return `mock-${JSON.stringify(record)}`; + }, + }); + + for (const record of records) { + appender.append(record); + // eslint-disable-next-line no-console + expect(console.log).toHaveBeenCalledWith(`mock-${JSON.stringify(record)}`); + } + + // eslint-disable-next-line no-console + expect(console.log).toHaveBeenCalledTimes(records.length); +}); diff --git a/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.ts b/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.ts new file mode 100644 index 00000000000000..4d35f3150b4212 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.ts @@ -0,0 +1,38 @@ +/* + * 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 { Layout, LogRecord, DisposableAppender } from '@kbn/logging'; + +/** + * + * Appender that formats all the `LogRecord` instances it receives and logs them via built-in `console`. + * @internal + */ +export class ConsoleAppender implements DisposableAppender { + /** + * Creates ConsoleAppender instance. + * @param layout Instance of `Layout` sub-class responsible for `LogRecord` formatting. + */ + constructor(private readonly layout: Layout) {} + + /** + * Formats specified `record` and logs it via built-in `console`. + * @param record `LogRecord` instance to be logged. + */ + public append(record: LogRecord) { + // eslint-disable-next-line no-console + console.log(this.layout.format(record)); + } + + /** + * Disposes `ConsoleAppender`. + */ + public dispose() { + // noop + } +} diff --git a/packages/core/logging/core-logging-browser-internal/src/appenders/index.ts b/packages/core/logging/core-logging-browser-internal/src/appenders/index.ts new file mode 100644 index 00000000000000..070f5fb429c877 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/appenders/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { ConsoleAppender } from './console_appender'; diff --git a/packages/core/logging/core-logging-browser-internal/src/index.ts b/packages/core/logging/core-logging-browser-internal/src/index.ts new file mode 100644 index 00000000000000..618d4dd8a7ef4e --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { BaseLogger } from './logger'; +export { BrowserLoggingSystem, type IBrowserLoggingSystem } from './logging_system'; diff --git a/packages/core/logging/core-logging-browser-internal/src/layouts/__snapshots__/pattern_layout.test.ts.snap b/packages/core/logging/core-logging-browser-internal/src/layouts/__snapshots__/pattern_layout.test.ts.snap new file mode 100644 index 00000000000000..d3f9309a4773c9 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/layouts/__snapshots__/pattern_layout.test.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`\`format()\` correctly formats record with custom pattern. 1`] = `"mock-Some error stack-context-1-Some error stack"`; + +exports[`\`format()\` correctly formats record with custom pattern. 2`] = `"mock-message-2-context-2-message-2"`; + +exports[`\`format()\` correctly formats record with custom pattern. 3`] = `"mock-message-3-context-3-message-3"`; + +exports[`\`format()\` correctly formats record with custom pattern. 4`] = `"mock-message-4-context-4-message-4"`; + +exports[`\`format()\` correctly formats record with custom pattern. 5`] = `"mock-message-5-context-5-message-5"`; + +exports[`\`format()\` correctly formats record with custom pattern. 6`] = `"mock-message-6-context-6-message-6"`; + +exports[`\`format()\` correctly formats record with full pattern. 1`] = `"[2012-02-01T09:30:22.011-05:00][FATAL][context-1] Some error stack"`; + +exports[`\`format()\` correctly formats record with full pattern. 2`] = `"[2012-02-01T09:30:22.011-05:00][ERROR][context-2] message-2"`; + +exports[`\`format()\` correctly formats record with full pattern. 3`] = `"[2012-02-01T09:30:22.011-05:00][WARN ][context-3] message-3"`; + +exports[`\`format()\` correctly formats record with full pattern. 4`] = `"[2012-02-01T09:30:22.011-05:00][DEBUG][context-4] message-4"`; + +exports[`\`format()\` correctly formats record with full pattern. 5`] = `"[2012-02-01T09:30:22.011-05:00][INFO ][context-5] message-5"`; + +exports[`\`format()\` correctly formats record with full pattern. 6`] = `"[2012-02-01T09:30:22.011-05:00][TRACE][context-6] message-6"`; + +exports[`allows specifying the PID in custom pattern 1`] = `"%pid-context-1-Some error stack"`; + +exports[`allows specifying the PID in custom pattern 2`] = `"%pid-context-2-message-2"`; + +exports[`allows specifying the PID in custom pattern 3`] = `"%pid-context-3-message-3"`; + +exports[`allows specifying the PID in custom pattern 4`] = `"%pid-context-4-message-4"`; + +exports[`allows specifying the PID in custom pattern 5`] = `"%pid-context-5-message-5"`; + +exports[`allows specifying the PID in custom pattern 6`] = `"%pid-context-6-message-6"`; diff --git a/packages/core/logging/core-logging-browser-internal/src/layouts/index.ts b/packages/core/logging/core-logging-browser-internal/src/layouts/index.ts new file mode 100644 index 00000000000000..75591053d34d7f --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/layouts/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { PatternLayout } from './pattern_layout'; diff --git a/packages/core/logging/core-logging-browser-internal/src/layouts/pattern_layout.test.ts b/packages/core/logging/core-logging-browser-internal/src/layouts/pattern_layout.test.ts new file mode 100644 index 00000000000000..eb0961960b17f6 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/layouts/pattern_layout.test.ts @@ -0,0 +1,246 @@ +/* + * 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 stripAnsi from 'strip-ansi'; +import hasAnsi from 'has-ansi'; +import { LogLevel, LogRecord } from '@kbn/logging'; +import { PatternLayout } from './pattern_layout'; + +const stripAnsiSnapshotSerializer: jest.SnapshotSerializerPlugin = { + serialize(value: string) { + return stripAnsi(value); + }, + + test(value: any) { + return typeof value === 'string' && hasAnsi(value); + }, +}; + +const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 30, 22, 11)); +const records: LogRecord[] = [ + { + context: 'context-1', + error: { + message: 'Some error message', + name: 'Some error name', + stack: 'Some error stack', + }, + level: LogLevel.Fatal, + message: 'message-1', + timestamp, + pid: 5355, + }, + { + context: 'context-2', + level: LogLevel.Error, + message: 'message-2', + timestamp, + pid: 5355, + }, + { + context: 'context-3', + level: LogLevel.Warn, + message: 'message-3', + timestamp, + pid: 5355, + }, + { + context: 'context-4', + level: LogLevel.Debug, + message: 'message-4', + timestamp, + pid: 5355, + }, + { + context: 'context-5', + level: LogLevel.Info, + message: 'message-5', + timestamp, + pid: 5355, + }, + { + context: 'context-6', + level: LogLevel.Trace, + message: 'message-6', + timestamp, + pid: 5355, + }, +]; + +expect.addSnapshotSerializer(stripAnsiSnapshotSerializer); + +test('`format()` correctly formats record with full pattern.', () => { + const layout = new PatternLayout(); + + for (const record of records) { + expect(layout.format(record)).toMatchSnapshot(); + } +}); + +test('`format()` correctly formats record with custom pattern.', () => { + const layout = new PatternLayout('mock-%message-%logger-%message'); + + for (const record of records) { + expect(layout.format(record)).toMatchSnapshot(); + } +}); + +test('`format()` correctly formats record with meta data.', () => { + const layout = new PatternLayout('[%date][%level][%logger]%meta %message'); + + expect( + layout.format({ + context: 'context-meta', + level: LogLevel.Debug, + message: 'message-meta', + timestamp, + pid: 5355, + meta: { + // @ts-expect-error not valid ECS field + from: 'v7', + to: 'v8', + }, + }) + ).toBe( + '[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta' + ); + + expect( + layout.format({ + context: 'context-meta', + level: LogLevel.Debug, + message: 'message-meta', + timestamp, + pid: 5355, + meta: {}, + }) + ).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta]{} message-meta'); + + expect( + layout.format({ + context: 'context-meta', + level: LogLevel.Debug, + message: 'message-meta', + timestamp, + pid: 5355, + }) + ).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta] message-meta'); +}); + +test('allows specifying the PID in custom pattern', () => { + const layout = new PatternLayout('%pid-%logger-%message'); + + for (const record of records) { + expect(layout.format(record)).toMatchSnapshot(); + } +}); + +test('`format()` allows specifying pattern with meta.', () => { + const layout = new PatternLayout('%logger-%meta-%message'); + const record = { + context: 'context', + level: LogLevel.Debug, + message: 'message', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }; + // @ts-expect-error not valid ECS field + expect(layout.format(record)).toBe('context-{"from":"v7","to":"v8"}-message'); +}); + +describe('format', () => { + describe('timestamp', () => { + const record = { + context: 'context', + level: LogLevel.Debug, + message: 'message', + timestamp, + pid: 5355, + }; + it('uses ISO8601_TZ as default', () => { + const layout = new PatternLayout(); + + expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context] message'); + }); + + describe('supports specifying a predefined format', () => { + it('ISO8601', () => { + const layout = new PatternLayout('[%date{ISO8601}][%logger]'); + + expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); + }); + + it('ISO8601_TZ', () => { + const layout = new PatternLayout('[%date{ISO8601_TZ}][%logger]'); + + expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][context]'); + }); + + it('ABSOLUTE', () => { + const layout = new PatternLayout('[%date{ABSOLUTE}][%logger]'); + + expect(layout.format(record)).toBe('[09:30:22.011][context]'); + }); + + it('UNIX', () => { + const layout = new PatternLayout('[%date{UNIX}][%logger]'); + + expect(layout.format(record)).toBe('[1328106622][context]'); + }); + + it('UNIX_MILLIS', () => { + const layout = new PatternLayout('[%date{UNIX_MILLIS}][%logger]'); + + expect(layout.format(record)).toBe('[1328106622011][context]'); + }); + }); + + describe('supports specifying a predefined format and timezone', () => { + it('ISO8601', () => { + const layout = new PatternLayout('[%date{ISO8601}{America/Los_Angeles}][%logger]'); + + expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); + }); + + it('ISO8601_TZ', () => { + const layout = new PatternLayout('[%date{ISO8601_TZ}{America/Los_Angeles}][%logger]'); + + expect(layout.format(record)).toBe('[2012-02-01T06:30:22.011-08:00][context]'); + }); + + it('ABSOLUTE', () => { + const layout = new PatternLayout('[%date{ABSOLUTE}{America/Los_Angeles}][%logger]'); + + expect(layout.format(record)).toBe('[06:30:22.011][context]'); + }); + + it('UNIX', () => { + const layout = new PatternLayout('[%date{UNIX}{America/Los_Angeles}][%logger]'); + + expect(layout.format(record)).toBe('[1328106622][context]'); + }); + + it('UNIX_MILLIS', () => { + const layout = new PatternLayout('[%date{UNIX_MILLIS}{America/Los_Angeles}][%logger]'); + + expect(layout.format(record)).toBe('[1328106622011][context]'); + }); + }); + it('formats several conversions patterns correctly', () => { + const layout = new PatternLayout( + '[%date{ABSOLUTE}{America/Los_Angeles}][%logger][%date{UNIX}]' + ); + + expect(layout.format(record)).toBe('[06:30:22.011][context][1328106622]'); + }); + }); +}); diff --git a/packages/core/logging/core-logging-browser-internal/src/layouts/pattern_layout.ts b/packages/core/logging/core-logging-browser-internal/src/layouts/pattern_layout.ts new file mode 100644 index 00000000000000..0efdf33afabb42 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/layouts/pattern_layout.ts @@ -0,0 +1,42 @@ +/* + * 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 { + PatternLayout as BasePatternLayout, + type Conversion, + LoggerConversion, + LevelConversion, + MetaConversion, + MessageConversion, + DateConversion, +} from '@kbn/core-logging-common-internal'; + +const DEFAULT_PATTERN = `[%date][%level][%logger] %message`; + +const conversions: Conversion[] = [ + LoggerConversion, + MessageConversion, + LevelConversion, + MetaConversion, + DateConversion, +]; + +/** + * Layout that formats `LogRecord` using the `pattern` string with optional + * color highlighting (eg. to make log messages easier to read in the terminal). + * @internal + */ +export class PatternLayout extends BasePatternLayout { + constructor(pattern: string = DEFAULT_PATTERN) { + super({ + pattern, + highlight: false, + conversions, + }); + } +} diff --git a/packages/core/logging/core-logging-browser-internal/src/logger.test.ts b/packages/core/logging/core-logging-browser-internal/src/logger.test.ts new file mode 100644 index 00000000000000..7c9606264602a4 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/logger.test.ts @@ -0,0 +1,431 @@ +/* + * 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 { LogLevel, Appender } from '@kbn/logging'; +import { getLoggerContext } from '@kbn/core-logging-common-internal'; +import { BaseLogger, BROWSER_PID } from './logger'; + +const context = getLoggerContext(['context', 'parent', 'child']); +let appenderMocks: Appender[]; +let logger: BaseLogger; +const factory = { + get: jest.fn().mockImplementation(() => logger), +}; + +const timestamp = new Date(2012, 1, 1); +beforeEach(() => { + jest.spyOn(global, 'Date').mockImplementation(() => timestamp); + + appenderMocks = [{ append: jest.fn() }, { append: jest.fn() }]; + logger = new BaseLogger(context, LogLevel.All, appenderMocks, factory); +}); + +afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); +}); + +test('`trace()` correctly forms `LogRecord` and passes it to all appenders.', () => { + logger.trace('message-1'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Trace, + message: 'message-1', + meta: undefined, + timestamp, + pid: BROWSER_PID, + }); + } + + // @ts-expect-error ECS custom meta + logger.trace('message-2', { trace: true }); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(2); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Trace, + message: 'message-2', + meta: { trace: true }, + timestamp, + pid: BROWSER_PID, + }); + } +}); + +test('`debug()` correctly forms `LogRecord` and passes it to all appenders.', () => { + logger.debug('message-1'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Debug, + message: 'message-1', + meta: undefined, + timestamp, + pid: BROWSER_PID, + }); + } + + // @ts-expect-error ECS custom meta + logger.debug('message-2', { debug: true }); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(2); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Debug, + message: 'message-2', + meta: { debug: true }, + timestamp, + pid: BROWSER_PID, + }); + } +}); + +test('`info()` correctly forms `LogRecord` and passes it to all appenders.', () => { + logger.info('message-1'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Info, + message: 'message-1', + meta: undefined, + timestamp, + pid: BROWSER_PID, + }); + } + + // @ts-expect-error ECS custom meta + logger.info('message-2', { info: true }); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(2); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Info, + message: 'message-2', + meta: { info: true }, + timestamp, + pid: BROWSER_PID, + }); + } +}); + +test('`warn()` correctly forms `LogRecord` and passes it to all appenders.', () => { + logger.warn('message-1'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Warn, + message: 'message-1', + meta: undefined, + timestamp, + pid: BROWSER_PID, + }); + } + + const error = new Error('message-2'); + logger.warn(error); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(2); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error, + level: LogLevel.Warn, + message: 'message-2', + meta: undefined, + timestamp, + pid: BROWSER_PID, + }); + } + + // @ts-expect-error ECS custom meta + logger.warn('message-3', { warn: true }); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(3); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Warn, + message: 'message-3', + meta: { warn: true }, + timestamp, + pid: BROWSER_PID, + }); + } +}); + +test('`error()` correctly forms `LogRecord` and passes it to all appenders.', () => { + logger.error('message-1'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Error, + message: 'message-1', + meta: undefined, + timestamp, + pid: BROWSER_PID, + }); + } + + const error = new Error('message-2'); + logger.error(error); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(2); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error, + level: LogLevel.Error, + message: 'message-2', + meta: undefined, + timestamp, + pid: BROWSER_PID, + }); + } + + // @ts-expect-error ECS custom meta + logger.error('message-3', { error: true }); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(3); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Error, + message: 'message-3', + meta: { error: true }, + timestamp, + pid: BROWSER_PID, + }); + } +}); + +test('`fatal()` correctly forms `LogRecord` and passes it to all appenders.', () => { + logger.fatal('message-1'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Fatal, + message: 'message-1', + meta: undefined, + timestamp, + pid: BROWSER_PID, + }); + } + + const error = new Error('message-2'); + logger.fatal(error); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(2); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error, + level: LogLevel.Fatal, + message: 'message-2', + meta: undefined, + timestamp, + pid: BROWSER_PID, + }); + } + + // @ts-expect-error ECS custom meta + logger.fatal('message-3', { fatal: true }); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(3); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + error: undefined, + level: LogLevel.Fatal, + message: 'message-3', + meta: { fatal: true }, + timestamp, + pid: BROWSER_PID, + }); + } +}); + +test('`log()` just passes the record to all appenders.', () => { + const record = { + context, + level: LogLevel.Info, + message: 'message-1', + timestamp, + pid: 5355, + }; + + logger.log(record); + + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith(record); + } +}); + +test('`get()` calls the logger factory with proper context and return the result', () => { + logger.get('sub', 'context'); + expect(factory.get).toHaveBeenCalledTimes(1); + expect(factory.get).toHaveBeenCalledWith(context, 'sub', 'context'); + + factory.get.mockClear(); + factory.get.mockImplementation(() => 'some-logger'); + + const childLogger = logger.get('other', 'sub'); + expect(factory.get).toHaveBeenCalledTimes(1); + expect(factory.get).toHaveBeenCalledWith(context, 'other', 'sub'); + expect(childLogger).toEqual('some-logger'); +}); + +test('logger with `Off` level does not pass any records to appenders.', () => { + const turnedOffLogger = new BaseLogger(context, LogLevel.Off, appenderMocks, factory); + turnedOffLogger.trace('trace-message'); + turnedOffLogger.debug('debug-message'); + turnedOffLogger.info('info-message'); + turnedOffLogger.warn('warn-message'); + turnedOffLogger.error('error-message'); + turnedOffLogger.fatal('fatal-message'); + + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).not.toHaveBeenCalled(); + } +}); + +test('logger with `All` level passes all records to appenders.', () => { + const catchAllLogger = new BaseLogger(context, LogLevel.All, appenderMocks, factory); + + catchAllLogger.trace('trace-message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + level: LogLevel.Trace, + message: 'trace-message', + timestamp, + pid: BROWSER_PID, + }); + } + + catchAllLogger.debug('debug-message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(2); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + level: LogLevel.Debug, + message: 'debug-message', + timestamp, + pid: BROWSER_PID, + }); + } + + catchAllLogger.info('info-message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(3); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + level: LogLevel.Info, + message: 'info-message', + timestamp, + pid: BROWSER_PID, + }); + } + + catchAllLogger.warn('warn-message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(4); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + level: LogLevel.Warn, + message: 'warn-message', + timestamp, + pid: BROWSER_PID, + }); + } + + catchAllLogger.error('error-message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(5); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + level: LogLevel.Error, + message: 'error-message', + timestamp, + pid: BROWSER_PID, + }); + } + + catchAllLogger.fatal('fatal-message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(6); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + level: LogLevel.Fatal, + message: 'fatal-message', + timestamp, + pid: BROWSER_PID, + }); + } +}); + +test('passes log record to appenders only if log level is supported.', () => { + const warnLogger = new BaseLogger(context, LogLevel.Warn, appenderMocks, factory); + + warnLogger.trace('trace-message'); + warnLogger.debug('debug-message'); + warnLogger.info('info-message'); + + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).not.toHaveBeenCalled(); + } + + warnLogger.warn('warn-message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + level: LogLevel.Warn, + message: 'warn-message', + timestamp, + pid: BROWSER_PID, + }); + } + + warnLogger.error('error-message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(2); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + level: LogLevel.Error, + message: 'error-message', + timestamp, + pid: BROWSER_PID, + }); + } + + warnLogger.fatal('fatal-message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(3); + expect(appenderMock.append).toHaveBeenCalledWith({ + context, + level: LogLevel.Fatal, + message: 'fatal-message', + timestamp, + pid: BROWSER_PID, + }); + } +}); diff --git a/packages/core/logging/core-logging-browser-internal/src/logger.ts b/packages/core/logging/core-logging-browser-internal/src/logger.ts new file mode 100644 index 00000000000000..31c3c9f4eccb59 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/logger.ts @@ -0,0 +1,46 @@ +/* + * 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 { LogLevel, LogRecord, LogMeta } from '@kbn/logging'; +import { AbstractLogger } from '@kbn/core-logging-common-internal'; + +function isError(x: any): x is Error { + return x instanceof Error; +} + +export const BROWSER_PID = -1; + +/** @internal */ +export class BaseLogger extends AbstractLogger { + protected createLogRecord( + level: LogLevel, + errorOrMessage: string | Error, + meta?: Meta + ): LogRecord { + if (isError(errorOrMessage)) { + return { + context: this.context, + error: errorOrMessage, + level, + message: errorOrMessage.message, + meta, + timestamp: new Date(), + pid: BROWSER_PID, + }; + } + + return { + context: this.context, + level, + message: errorOrMessage, + meta, + timestamp: new Date(), + pid: BROWSER_PID, + }; + } +} diff --git a/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts b/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts new file mode 100644 index 00000000000000..61b32002e7ae5e --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { BrowserLoggingSystem } from './logging_system'; + +describe('', () => { + const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 33, 22, 11)); + + let mockConsoleLog: jest.SpyInstance; + let loggingSystem: BrowserLoggingSystem; + + beforeEach(() => { + mockConsoleLog = jest.spyOn(global.console, 'log').mockReturnValue(undefined); + jest.spyOn(global, 'Date').mockImplementation(() => timestamp); + loggingSystem = new BrowserLoggingSystem({ logLevel: 'warn' }); + }); + + afterEach(() => { + mockConsoleLog.mockReset(); + }); + + describe('#get', () => { + it('returns the same logger for same context', () => { + const loggerA = loggingSystem.get('same.logger'); + const loggerB = loggingSystem.get('same.logger'); + expect(loggerA).toBe(loggerB); + }); + + it('returns different loggers for different contexts', () => { + const loggerA = loggingSystem.get('some.logger'); + const loggerB = loggingSystem.get('another.logger'); + expect(loggerA).not.toBe(loggerB); + }); + }); + + describe('logger configuration', () => { + it('properly configure the logger to use the correct context and pattern', () => { + const logger = loggingSystem.get('foo.bar'); + logger.warn('some message'); + + expect(mockConsoleLog.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "[2012-02-01T09:33:22.011-05:00][WARN ][foo.bar] some message", + ] + `); + }); + + it('properly configure the logger to use the correct level', () => { + const logger = loggingSystem.get('foo.bar'); + logger.trace('some trace message'); + logger.debug('some debug message'); + logger.info('some info message'); + logger.warn('some warn message'); + logger.error('some error message'); + logger.fatal('some fatal message'); + + expect(mockConsoleLog.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "[2012-02-01T04:33:22.011-05:00][WARN ][foo.bar] some warn message", + ], + Array [ + "[2012-01-31T23:33:22.011-05:00][ERROR][foo.bar] some error message", + ], + Array [ + "[2012-01-31T18:33:22.011-05:00][FATAL][foo.bar] some fatal message", + ], + ] + `); + }); + }); +}); diff --git a/packages/core/logging/core-logging-browser-internal/src/logging_system.ts b/packages/core/logging/core-logging-browser-internal/src/logging_system.ts new file mode 100644 index 00000000000000..50f7c449996ba9 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/logging_system.ts @@ -0,0 +1,74 @@ +/* + * 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 { LogLevel, Logger, LoggerFactory, LogLevelId, DisposableAppender } from '@kbn/logging'; +import { getLoggerContext } from '@kbn/core-logging-common-internal'; +import type { LoggerConfigType } from './types'; +import { BaseLogger } from './logger'; +import { PatternLayout } from './layouts'; +import { ConsoleAppender } from './appenders'; + +export interface BrowserLoggingConfig { + logLevel: LogLevelId; +} + +const CONSOLE_APPENDER_ID = 'console'; + +/** + * @internal + */ +export interface IBrowserLoggingSystem extends LoggerFactory { + asLoggerFactory(): LoggerFactory; +} + +/** + * @internal + */ +export class BrowserLoggingSystem implements IBrowserLoggingSystem { + private readonly loggers: Map = new Map(); + private readonly appenders: Map = new Map(); + + constructor(private readonly loggingConfig: BrowserLoggingConfig) { + this.setupSystem(loggingConfig); + } + + public get(...contextParts: string[]): Logger { + const context = getLoggerContext(contextParts); + if (!this.loggers.has(context)) { + this.loggers.set(context, this.createLogger(context)); + } + return this.loggers.get(context)!; + } + + private createLogger(context: string) { + const { level, appenders } = this.getLoggerConfigByContext(context); + const loggerLevel = LogLevel.fromId(level); + const loggerAppenders = appenders.map((appenderKey) => this.appenders.get(appenderKey)!); + return new BaseLogger(context, loggerLevel, loggerAppenders, this.asLoggerFactory()); + } + + private getLoggerConfigByContext(context: string): LoggerConfigType { + return { + level: this.loggingConfig.logLevel, + appenders: [CONSOLE_APPENDER_ID], + name: context, + }; + } + + private setupSystem(loggingConfig: BrowserLoggingConfig) { + const consoleAppender = new ConsoleAppender(new PatternLayout()); + this.appenders.set(CONSOLE_APPENDER_ID, consoleAppender); + } + + /** + * Safe wrapper that allows passing logging service as immutable LoggerFactory. + */ + public asLoggerFactory(): LoggerFactory { + return { get: (...contextParts: string[]) => this.get(...contextParts) }; + } +} diff --git a/packages/core/logging/core-logging-browser-internal/src/types.ts b/packages/core/logging/core-logging-browser-internal/src/types.ts new file mode 100644 index 00000000000000..29ef977f2f28ff --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/src/types.ts @@ -0,0 +1,20 @@ +/* + * 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 { LogLevelId } from '@kbn/logging'; + +/** + * Describes the configuration of a given logger. + * + * @public + */ +export interface LoggerConfigType { + appenders: string[]; + name: string; + level: LogLevelId; +} diff --git a/packages/core/logging/core-logging-browser-internal/tsconfig.json b/packages/core/logging/core-logging-browser-internal/tsconfig.json new file mode 100644 index 00000000000000..25957cd665d111 --- /dev/null +++ b/packages/core/logging/core-logging-browser-internal/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node", + ] + }, + "include": [ + "**/*.ts", + ] +} diff --git a/packages/core/logging/core-logging-browser-mocks/BUILD.bazel b/packages/core/logging/core-logging-browser-mocks/BUILD.bazel new file mode 100644 index 00000000000000..a5e2c1ac54b190 --- /dev/null +++ b/packages/core/logging/core-logging-browser-mocks/BUILD.bazel @@ -0,0 +1,115 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-logging-browser-mocks" +PKG_REQUIRE_NAME = "@kbn/core-logging-browser-mocks" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/*.config.js", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//react", + "//packages/kbn-logging-mocks", +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "//packages/kbn-logging-mocks:npm_module_types", + "//packages/core/logging/core-logging-browser-internal:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +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, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +js_library( + name = "npm_module_types", + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "build_types", + deps = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/logging/core-logging-browser-mocks/README.md b/packages/core/logging/core-logging-browser-mocks/README.md new file mode 100644 index 00000000000000..eeb456fe2379cf --- /dev/null +++ b/packages/core/logging/core-logging-browser-mocks/README.md @@ -0,0 +1,3 @@ +# @kbn/core-logging-browser-mocks + +This package contains the mocks for Core's browser-side logging service. diff --git a/packages/core/logging/core-logging-browser-mocks/index.ts b/packages/core/logging/core-logging-browser-mocks/index.ts new file mode 100644 index 00000000000000..a8b4c9ff1a2fa3 --- /dev/null +++ b/packages/core/logging/core-logging-browser-mocks/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { loggingSystemMock } from './src'; diff --git a/packages/core/logging/core-logging-browser-mocks/jest.config.js b/packages/core/logging/core-logging-browser-mocks/jest.config.js new file mode 100644 index 00000000000000..47449390afd033 --- /dev/null +++ b/packages/core/logging/core-logging-browser-mocks/jest.config.js @@ -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 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/core/logging/core-logging-browser-mocks'], +}; diff --git a/packages/core/logging/core-logging-browser-mocks/kibana.jsonc b/packages/core/logging/core-logging-browser-mocks/kibana.jsonc new file mode 100644 index 00000000000000..377320816c652e --- /dev/null +++ b/packages/core/logging/core-logging-browser-mocks/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/core-logging-browser-mocks", + "owner": "@elastic/kibana-core", + "runtimeDeps": [], + "typeDeps": [], +} diff --git a/packages/core/logging/core-logging-browser-mocks/package.json b/packages/core/logging/core-logging-browser-mocks/package.json new file mode 100644 index 00000000000000..8ab9610e35470f --- /dev/null +++ b/packages/core/logging/core-logging-browser-mocks/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/core-logging-browser-mocks", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "author": "Kibana Core", + "license": "SSPL-1.0 OR Elastic License 2.0", + "types": "./target_types/index.d.ts" +} diff --git a/packages/core/logging/core-logging-browser-mocks/src/index.ts b/packages/core/logging/core-logging-browser-mocks/src/index.ts new file mode 100644 index 00000000000000..07ff934f94dfa2 --- /dev/null +++ b/packages/core/logging/core-logging-browser-mocks/src/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { loggingSystemMock } from './logging_system.mock'; diff --git a/packages/core/logging/core-logging-browser-mocks/src/logging_system.mock.ts b/packages/core/logging/core-logging-browser-mocks/src/logging_system.mock.ts new file mode 100644 index 00000000000000..333b2cbb7bb8a9 --- /dev/null +++ b/packages/core/logging/core-logging-browser-mocks/src/logging_system.mock.ts @@ -0,0 +1,37 @@ +/* + * 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 { loggerMock, MockedLogger } from '@kbn/logging-mocks'; +import type { IBrowserLoggingSystem } from '@kbn/core-logging-browser-internal'; + +const createLoggingSystemMock = () => { + const mockLog: MockedLogger = loggerMock.create(); + + mockLog.get.mockImplementation((...context) => ({ + ...mockLog, + context, + })); + + const mocked: jest.Mocked = { + get: jest.fn(), + asLoggerFactory: jest.fn(), + }; + + mocked.get.mockImplementation((...context) => ({ + ...mockLog, + context, + })); + mocked.asLoggerFactory.mockImplementation(() => mocked); + + return mocked; +}; + +export const loggingSystemMock = { + create: createLoggingSystemMock, + createLogger: loggerMock.create, +}; diff --git a/packages/core/logging/core-logging-browser-mocks/tsconfig.json b/packages/core/logging/core-logging-browser-mocks/tsconfig.json new file mode 100644 index 00000000000000..571fbfcd8ef259 --- /dev/null +++ b/packages/core/logging/core-logging-browser-mocks/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ] +} diff --git a/packages/core/logging/core-logging-common-internal/BUILD.bazel b/packages/core/logging/core-logging-common-internal/BUILD.bazel new file mode 100644 index 00000000000000..3c392281b23a63 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/BUILD.bazel @@ -0,0 +1,116 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-logging-common-internal" +PKG_REQUIRE_NAME = "@kbn/core-logging-common-internal" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//lodash", + "@npm//moment-timezone", +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/moment-timezone", + "@npm//lodash", + "//packages/kbn-logging:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +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, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +js_library( + name = "npm_module_types", + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "build_types", + deps = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/logging/core-logging-common-internal/README.md b/packages/core/logging/core-logging-common-internal/README.md new file mode 100644 index 00000000000000..9358b0009781b9 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/README.md @@ -0,0 +1,3 @@ +# @kbn/core-logging-common-internal + +This package contains common types and the base implementation for the browser and server-side logging systems. diff --git a/packages/core/logging/core-logging-common-internal/index.ts b/packages/core/logging/core-logging-common-internal/index.ts new file mode 100644 index 00000000000000..24d5e93316789b --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/index.ts @@ -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 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 { + PatternLayout, + DateConversion, + LoggerConversion, + MessageConversion, + LevelConversion, + MetaConversion, + type Conversion, + AbstractLogger, + type CreateLogRecordFn, + getLoggerContext, + getParentLoggerContext, + CONTEXT_SEPARATOR, + ROOT_CONTEXT_NAME, + DEFAULT_APPENDER_NAME, +} from './src'; diff --git a/packages/core/logging/core-logging-common-internal/jest.config.js b/packages/core/logging/core-logging-common-internal/jest.config.js new file mode 100644 index 00000000000000..3a077c47c301b1 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/jest.config.js @@ -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 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/core/logging/core-logging-common-internal'], +}; diff --git a/packages/core/logging/core-logging-common-internal/kibana.jsonc b/packages/core/logging/core-logging-common-internal/kibana.jsonc new file mode 100644 index 00000000000000..353df47ee9dd07 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/core-logging-common-internal", + "owner": "@elastic/kibana-core", + "runtimeDeps": [], + "typeDeps": [], +} diff --git a/packages/core/logging/core-logging-common-internal/package.json b/packages/core/logging/core-logging-common-internal/package.json new file mode 100644 index 00000000000000..3c0aff6df7b0b0 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/core-logging-common-internal", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "author": "Kibana Core", + "license": "SSPL-1.0 OR Elastic License 2.0", + "types": "./target_types/index.d.ts" +} diff --git a/packages/core/logging/core-logging-common-internal/src/index.ts b/packages/core/logging/core-logging-common-internal/src/index.ts new file mode 100644 index 00000000000000..cf74f46eb6d65d --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/index.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. + */ + +export { + PatternLayout, + DateConversion, + LoggerConversion, + MessageConversion, + LevelConversion, + MetaConversion, + type Conversion, +} from './layouts'; +export { AbstractLogger, type CreateLogRecordFn } from './logger'; +export { + getLoggerContext, + getParentLoggerContext, + CONTEXT_SEPARATOR, + ROOT_CONTEXT_NAME, + DEFAULT_APPENDER_NAME, +} from './logger_context'; diff --git a/packages/core/logging/core-logging-common-internal/src/layouts/__snapshots__/pattern_layout.test.ts.snap b/packages/core/logging/core-logging-common-internal/src/layouts/__snapshots__/pattern_layout.test.ts.snap new file mode 100644 index 00000000000000..d3f9309a4773c9 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/layouts/__snapshots__/pattern_layout.test.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`\`format()\` correctly formats record with custom pattern. 1`] = `"mock-Some error stack-context-1-Some error stack"`; + +exports[`\`format()\` correctly formats record with custom pattern. 2`] = `"mock-message-2-context-2-message-2"`; + +exports[`\`format()\` correctly formats record with custom pattern. 3`] = `"mock-message-3-context-3-message-3"`; + +exports[`\`format()\` correctly formats record with custom pattern. 4`] = `"mock-message-4-context-4-message-4"`; + +exports[`\`format()\` correctly formats record with custom pattern. 5`] = `"mock-message-5-context-5-message-5"`; + +exports[`\`format()\` correctly formats record with custom pattern. 6`] = `"mock-message-6-context-6-message-6"`; + +exports[`\`format()\` correctly formats record with full pattern. 1`] = `"[2012-02-01T09:30:22.011-05:00][FATAL][context-1] Some error stack"`; + +exports[`\`format()\` correctly formats record with full pattern. 2`] = `"[2012-02-01T09:30:22.011-05:00][ERROR][context-2] message-2"`; + +exports[`\`format()\` correctly formats record with full pattern. 3`] = `"[2012-02-01T09:30:22.011-05:00][WARN ][context-3] message-3"`; + +exports[`\`format()\` correctly formats record with full pattern. 4`] = `"[2012-02-01T09:30:22.011-05:00][DEBUG][context-4] message-4"`; + +exports[`\`format()\` correctly formats record with full pattern. 5`] = `"[2012-02-01T09:30:22.011-05:00][INFO ][context-5] message-5"`; + +exports[`\`format()\` correctly formats record with full pattern. 6`] = `"[2012-02-01T09:30:22.011-05:00][TRACE][context-6] message-6"`; + +exports[`allows specifying the PID in custom pattern 1`] = `"%pid-context-1-Some error stack"`; + +exports[`allows specifying the PID in custom pattern 2`] = `"%pid-context-2-message-2"`; + +exports[`allows specifying the PID in custom pattern 3`] = `"%pid-context-3-message-3"`; + +exports[`allows specifying the PID in custom pattern 4`] = `"%pid-context-4-message-4"`; + +exports[`allows specifying the PID in custom pattern 5`] = `"%pid-context-5-message-5"`; + +exports[`allows specifying the PID in custom pattern 6`] = `"%pid-context-6-message-6"`; diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/date.ts b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/date.ts similarity index 98% rename from packages/core/logging/core-logging-server-internal/src/layouts/conversions/date.ts rename to packages/core/logging/core-logging-common-internal/src/layouts/conversions/date.ts index 66aad5b42354a1..024d3d26befb3b 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/date.ts +++ b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/date.ts @@ -9,8 +9,7 @@ import moment from 'moment-timezone'; import { last } from 'lodash'; import { LogRecord } from '@kbn/logging'; - -import { Conversion } from './type'; +import { Conversion } from './types'; const dateRegExp = /%date({(?[^}]+)})?({(?[^}]+)})?/g; diff --git a/packages/core/logging/core-logging-common-internal/src/layouts/conversions/index.ts b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/index.ts new file mode 100644 index 00000000000000..3f0abcdb8f4787 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/index.ts @@ -0,0 +1,14 @@ +/* + * 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 type { Conversion } from './types'; +export { LoggerConversion } from './logger'; +export { LevelConversion } from './level'; +export { MessageConversion } from './message'; +export { MetaConversion } from './meta'; +export { DateConversion } from './date'; diff --git a/packages/core/logging/core-logging-common-internal/src/layouts/conversions/level.ts b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/level.ts new file mode 100644 index 00000000000000..19bd60c3a1996f --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/level.ts @@ -0,0 +1,18 @@ +/* + * 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 { LogRecord } from '@kbn/logging'; +import { Conversion } from './types'; + +export const LevelConversion: Conversion = { + pattern: /%level/g, + convert(record: LogRecord) { + const message = record.level.id.toUpperCase().padEnd(5); + return message; + }, +}; diff --git a/packages/core/logging/core-logging-common-internal/src/layouts/conversions/logger.ts b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/logger.ts new file mode 100644 index 00000000000000..f7ddb98646119b --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/logger.ts @@ -0,0 +1,18 @@ +/* + * 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 { LogRecord } from '@kbn/logging'; +import { Conversion } from './types'; + +export const LoggerConversion: Conversion = { + pattern: /%logger/g, + convert(record: LogRecord) { + const message = record.context; + return message; + }, +}; diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/message.ts b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/message.ts similarity index 94% rename from packages/core/logging/core-logging-server-internal/src/layouts/conversions/message.ts rename to packages/core/logging/core-logging-common-internal/src/layouts/conversions/message.ts index 009eee4d33eae7..97fc4c60101ce7 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/message.ts +++ b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/message.ts @@ -7,7 +7,7 @@ */ import { LogRecord } from '@kbn/logging'; -import { Conversion } from './type'; +import { Conversion } from './types'; export const MessageConversion: Conversion = { pattern: /%message/g, diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/meta.ts b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/meta.ts similarity index 93% rename from packages/core/logging/core-logging-server-internal/src/layouts/conversions/meta.ts rename to packages/core/logging/core-logging-common-internal/src/layouts/conversions/meta.ts index abeb13136f13a0..49bd79729da5af 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/meta.ts +++ b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/meta.ts @@ -7,7 +7,7 @@ */ import { LogRecord } from '@kbn/logging'; -import { Conversion } from './type'; +import { Conversion } from './types'; export const MetaConversion: Conversion = { pattern: /%meta/g, diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/type.ts b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/types.ts similarity index 100% rename from packages/core/logging/core-logging-server-internal/src/layouts/conversions/type.ts rename to packages/core/logging/core-logging-common-internal/src/layouts/conversions/types.ts diff --git a/packages/core/logging/core-logging-common-internal/src/layouts/index.ts b/packages/core/logging/core-logging-common-internal/src/layouts/index.ts new file mode 100644 index 00000000000000..0d89459d9de9fe --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/layouts/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { PatternLayout } from './pattern_layout'; +export { + DateConversion, + LoggerConversion, + MessageConversion, + LevelConversion, + MetaConversion, + type Conversion, +} from './conversions'; diff --git a/packages/core/logging/core-logging-common-internal/src/layouts/pattern_layout.test.ts b/packages/core/logging/core-logging-common-internal/src/layouts/pattern_layout.test.ts new file mode 100644 index 00000000000000..b810321f83639c --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/layouts/pattern_layout.test.ts @@ -0,0 +1,256 @@ +/* + * 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 stripAnsi from 'strip-ansi'; +import hasAnsi from 'has-ansi'; +import { LogLevel, LogRecord } from '@kbn/logging'; +import { PatternLayout } from './pattern_layout'; + +const stripAnsiSnapshotSerializer: jest.SnapshotSerializerPlugin = { + serialize(value: string) { + return stripAnsi(value); + }, + + test(value: any) { + return typeof value === 'string' && hasAnsi(value); + }, +}; + +const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 30, 22, 11)); +const records: LogRecord[] = [ + { + context: 'context-1', + error: { + message: 'Some error message', + name: 'Some error name', + stack: 'Some error stack', + }, + level: LogLevel.Fatal, + message: 'message-1', + timestamp, + pid: 5355, + }, + { + context: 'context-2', + level: LogLevel.Error, + message: 'message-2', + timestamp, + pid: 5355, + }, + { + context: 'context-3', + level: LogLevel.Warn, + message: 'message-3', + timestamp, + pid: 5355, + }, + { + context: 'context-4', + level: LogLevel.Debug, + message: 'message-4', + timestamp, + pid: 5355, + }, + { + context: 'context-5', + level: LogLevel.Info, + message: 'message-5', + timestamp, + pid: 5355, + }, + { + context: 'context-6', + level: LogLevel.Trace, + message: 'message-6', + timestamp, + pid: 5355, + }, +]; + +expect.addSnapshotSerializer(stripAnsiSnapshotSerializer); + +test('`format()` correctly formats record with full pattern.', () => { + const layout = new PatternLayout(); + + for (const record of records) { + expect(layout.format(record)).toMatchSnapshot(); + } +}); + +test('`format()` correctly formats record with custom pattern.', () => { + const layout = new PatternLayout({ pattern: 'mock-%message-%logger-%message' }); + + for (const record of records) { + expect(layout.format(record)).toMatchSnapshot(); + } +}); + +test('`format()` correctly formats record with meta data.', () => { + const layout = new PatternLayout({ pattern: '[%date][%level][%logger]%meta %message' }); + + expect( + layout.format({ + context: 'context-meta', + level: LogLevel.Debug, + message: 'message-meta', + timestamp, + pid: 5355, + meta: { + // @ts-expect-error not valid ECS field + from: 'v7', + to: 'v8', + }, + }) + ).toBe( + '[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta' + ); + + expect( + layout.format({ + context: 'context-meta', + level: LogLevel.Debug, + message: 'message-meta', + timestamp, + pid: 5355, + meta: {}, + }) + ).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta]{} message-meta'); + + expect( + layout.format({ + context: 'context-meta', + level: LogLevel.Debug, + message: 'message-meta', + timestamp, + pid: 5355, + }) + ).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context-meta] message-meta'); +}); + +test('allows specifying the PID in custom pattern', () => { + const layout = new PatternLayout({ pattern: '%pid-%logger-%message' }); + + for (const record of records) { + expect(layout.format(record)).toMatchSnapshot(); + } +}); + +test('`format()` allows specifying pattern with meta.', () => { + const layout = new PatternLayout({ pattern: '%logger-%meta-%message' }); + const record = { + context: 'context', + level: LogLevel.Debug, + message: 'message', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }; + // @ts-expect-error not valid ECS field + expect(layout.format(record)).toBe('context-{"from":"v7","to":"v8"}-message'); +}); + +describe('format', () => { + describe('timestamp', () => { + const record = { + context: 'context', + level: LogLevel.Debug, + message: 'message', + timestamp, + pid: 5355, + }; + it('uses ISO8601_TZ as default', () => { + const layout = new PatternLayout(); + + expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][DEBUG][context] message'); + }); + + describe('supports specifying a predefined format', () => { + it('ISO8601', () => { + const layout = new PatternLayout({ pattern: '[%date{ISO8601}][%logger]' }); + + expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); + }); + + it('ISO8601_TZ', () => { + const layout = new PatternLayout({ pattern: '[%date{ISO8601_TZ}][%logger]' }); + + expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][context]'); + }); + + it('ABSOLUTE', () => { + const layout = new PatternLayout({ pattern: '[%date{ABSOLUTE}][%logger]' }); + + expect(layout.format(record)).toBe('[09:30:22.011][context]'); + }); + + it('UNIX', () => { + const layout = new PatternLayout({ pattern: '[%date{UNIX}][%logger]' }); + + expect(layout.format(record)).toBe('[1328106622][context]'); + }); + + it('UNIX_MILLIS', () => { + const layout = new PatternLayout({ pattern: '[%date{UNIX_MILLIS}][%logger]' }); + + expect(layout.format(record)).toBe('[1328106622011][context]'); + }); + }); + + describe('supports specifying a predefined format and timezone', () => { + it('ISO8601', () => { + const layout = new PatternLayout({ + pattern: '[%date{ISO8601}{America/Los_Angeles}][%logger]', + }); + + expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); + }); + + it('ISO8601_TZ', () => { + const layout = new PatternLayout({ + pattern: '[%date{ISO8601_TZ}{America/Los_Angeles}][%logger]', + }); + + expect(layout.format(record)).toBe('[2012-02-01T06:30:22.011-08:00][context]'); + }); + + it('ABSOLUTE', () => { + const layout = new PatternLayout({ + pattern: '[%date{ABSOLUTE}{America/Los_Angeles}][%logger]', + }); + + expect(layout.format(record)).toBe('[06:30:22.011][context]'); + }); + + it('UNIX', () => { + const layout = new PatternLayout({ + pattern: '[%date{UNIX}{America/Los_Angeles}][%logger]', + }); + + expect(layout.format(record)).toBe('[1328106622][context]'); + }); + + it('UNIX_MILLIS', () => { + const layout = new PatternLayout({ + pattern: '[%date{UNIX_MILLIS}{America/Los_Angeles}][%logger]', + }); + + expect(layout.format(record)).toBe('[1328106622011][context]'); + }); + }); + it('formats several conversions patterns correctly', () => { + const layout = new PatternLayout({ + pattern: '[%date{ABSOLUTE}{America/Los_Angeles}][%logger][%date{UNIX}]', + }); + + expect(layout.format(record)).toBe('[06:30:22.011][context][1328106622]'); + }); + }); +}); diff --git a/packages/core/logging/core-logging-common-internal/src/layouts/pattern_layout.ts b/packages/core/logging/core-logging-common-internal/src/layouts/pattern_layout.ts new file mode 100644 index 00000000000000..13a9b35fd41bb7 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/layouts/pattern_layout.ts @@ -0,0 +1,73 @@ +/* + * 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 { LogRecord, Layout } from '@kbn/logging'; +import { + Conversion, + LoggerConversion, + LevelConversion, + MetaConversion, + MessageConversion, + DateConversion, +} from './conversions'; + +/** + * Default pattern used by PatternLayout if it's not overridden in the configuration. + */ +const DEFAULT_PATTERN = `[%date][%level][%logger] %message`; + +const DEFAULT_CONVERSIONS: Conversion[] = [ + LoggerConversion, + MessageConversion, + LevelConversion, + MetaConversion, + DateConversion, +]; + +export interface PatternLayoutOptions { + pattern?: string; + highlight?: boolean; + conversions?: Conversion[]; +} + +/** + * Layout that formats `LogRecord` using the `pattern` string with optional + * color highlighting (eg. to make log messages easier to read in the terminal). + * @internal + */ +export class PatternLayout implements Layout { + private readonly pattern: string; + private readonly highlight: boolean; + private readonly conversions: Conversion[]; + + constructor({ + pattern = DEFAULT_PATTERN, + highlight = false, + conversions = DEFAULT_CONVERSIONS, + }: PatternLayoutOptions = {}) { + this.pattern = pattern; + this.highlight = highlight; + this.conversions = conversions; + } + + /** + * Formats `LogRecord` into a string based on the specified `pattern` and `highlighting` options. + * @param record Instance of `LogRecord` to format into string. + */ + public format(record: LogRecord): string { + let recordString = this.pattern; + for (const conversion of this.conversions) { + recordString = recordString.replace( + conversion.pattern, + conversion.convert.bind(null, record, this.highlight) + ); + } + + return recordString; + } +} diff --git a/packages/core/logging/core-logging-common-internal/src/logger.test.ts b/packages/core/logging/core-logging-common-internal/src/logger.test.ts new file mode 100644 index 00000000000000..adf4275a7d6cd7 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/logger.test.ts @@ -0,0 +1,241 @@ +/* + * 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 { Appender, LogLevel, LogMeta, LogRecord } from '@kbn/logging'; +import { getLoggerContext } from '@kbn/core-logging-common-internal'; +import { AbstractLogger, CreateLogRecordFn } from './logger'; + +describe('AbstractLogger', () => { + const context = getLoggerContext(['context', 'parent', 'child']); + const factory = { + get: jest.fn().mockImplementation(() => logger), + }; + + let appenderMocks: Appender[]; + + const createLogRecordSpy: jest.MockedFunction = jest.fn(); + + class TestLogger extends AbstractLogger { + createLogRecord( + level: LogLevel, + errorOrMessage: string | Error, + meta?: Meta + ) { + return createLogRecordSpy(level, errorOrMessage, meta); + } + } + + let logger: TestLogger; + + beforeEach(() => { + appenderMocks = [{ append: jest.fn() }, { append: jest.fn() }]; + logger = new TestLogger(context, LogLevel.All, appenderMocks, factory); + + createLogRecordSpy.mockImplementation((level, message, meta) => { + return { + level, + message, + meta, + } as LogRecord; + }); + }); + + afterEach(() => { + createLogRecordSpy.mockReset(); + }); + + describe('#trace', () => { + it('calls `createLogRecord` with the correct parameters', () => { + const meta = { tags: ['foo', 'bar'] }; + logger.trace('some message', meta); + + expect(createLogRecordSpy).toHaveBeenCalledTimes(1); + expect(createLogRecordSpy).toHaveBeenCalledWith(LogLevel.Trace, 'some message', meta); + }); + + it('pass the log record down to all appenders', () => { + const logRecord = { message: 'dummy', level: LogLevel.Trace } as LogRecord; + createLogRecordSpy.mockReturnValue(logRecord); + logger.trace('some message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith(logRecord); + } + }); + }); + + describe('#debug', () => { + it('calls `createLogRecord` with the correct parameters', () => { + const meta = { tags: ['foo', 'bar'] }; + logger.debug('some message', meta); + + expect(createLogRecordSpy).toHaveBeenCalledTimes(1); + expect(createLogRecordSpy).toHaveBeenCalledWith(LogLevel.Debug, 'some message', meta); + }); + + it('pass the log record down to all appenders', () => { + const logRecord = { message: 'dummy', level: LogLevel.Debug } as LogRecord; + createLogRecordSpy.mockReturnValue(logRecord); + logger.debug('some message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith(logRecord); + } + }); + }); + + describe('#info', () => { + it('calls `createLogRecord` with the correct parameters', () => { + const meta = { tags: ['foo', 'bar'] }; + logger.info('some message', meta); + + expect(createLogRecordSpy).toHaveBeenCalledTimes(1); + expect(createLogRecordSpy).toHaveBeenCalledWith(LogLevel.Info, 'some message', meta); + }); + + it('pass the log record down to all appenders', () => { + const logRecord = { message: 'dummy', level: LogLevel.Info } as LogRecord; + createLogRecordSpy.mockReturnValue(logRecord); + logger.info('some message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith(logRecord); + } + }); + }); + + describe('#warn', () => { + it('calls `createLogRecord` with the correct parameters', () => { + const meta = { tags: ['foo', 'bar'] }; + logger.warn('some message', meta); + + expect(createLogRecordSpy).toHaveBeenCalledTimes(1); + expect(createLogRecordSpy).toHaveBeenCalledWith(LogLevel.Warn, 'some message', meta); + }); + + it('pass the log record down to all appenders', () => { + const logRecord = { message: 'dummy', level: LogLevel.Warn } as LogRecord; + createLogRecordSpy.mockReturnValue(logRecord); + logger.warn('some message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith(logRecord); + } + }); + }); + + describe('#error', () => { + it('calls `createLogRecord` with the correct parameters', () => { + const meta = { tags: ['foo', 'bar'] }; + logger.error('some message', meta); + + expect(createLogRecordSpy).toHaveBeenCalledTimes(1); + expect(createLogRecordSpy).toHaveBeenCalledWith(LogLevel.Error, 'some message', meta); + }); + + it('pass the log record down to all appenders', () => { + const logRecord = { message: 'dummy', level: LogLevel.Error } as LogRecord; + createLogRecordSpy.mockReturnValue(logRecord); + logger.error('some message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith(logRecord); + } + }); + }); + + describe('#fatal', () => { + it('calls `createLogRecord` with the correct parameters', () => { + const meta = { tags: ['foo', 'bar'] }; + logger.fatal('some message', meta); + + expect(createLogRecordSpy).toHaveBeenCalledTimes(1); + expect(createLogRecordSpy).toHaveBeenCalledWith(LogLevel.Fatal, 'some message', meta); + }); + + it('pass the log record down to all appenders', () => { + const logRecord = { message: 'dummy', level: LogLevel.Fatal } as LogRecord; + createLogRecordSpy.mockReturnValue(logRecord); + logger.fatal('some message'); + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(1); + expect(appenderMock.append).toHaveBeenCalledWith(logRecord); + } + }); + }); + + describe('#get', () => { + it('calls the logger factory with proper context and return the result', () => { + logger.get('sub', 'context'); + expect(factory.get).toHaveBeenCalledTimes(1); + expect(factory.get).toHaveBeenCalledWith(context, 'sub', 'context'); + + factory.get.mockClear(); + factory.get.mockImplementation(() => 'some-logger'); + + const childLogger = logger.get('other', 'sub'); + expect(factory.get).toHaveBeenCalledTimes(1); + expect(factory.get).toHaveBeenCalledWith(context, 'other', 'sub'); + expect(childLogger).toEqual('some-logger'); + }); + }); + + describe('log level', () => { + it('does not calls appenders for records with unsupported levels', () => { + logger = new TestLogger(context, LogLevel.Warn, appenderMocks, factory); + + logger.trace('some trace message'); + logger.debug('some debug message'); + logger.info('some info message'); + logger.warn('some warn message'); + logger.error('some error message'); + logger.fatal('some fatal message'); + + for (const appenderMock of appenderMocks) { + expect(appenderMock.append).toHaveBeenCalledTimes(3); + expect(appenderMock.append).toHaveBeenCalledWith( + expect.objectContaining({ + level: LogLevel.Warn, + }) + ); + expect(appenderMock.append).toHaveBeenCalledWith( + expect.objectContaining({ + level: LogLevel.Error, + }) + ); + expect(appenderMock.append).toHaveBeenCalledWith( + expect.objectContaining({ + level: LogLevel.Fatal, + }) + ); + } + }); + }); + + describe('isLevelEnabled', () => { + const orderedLogLevels = [ + LogLevel.Fatal, + LogLevel.Error, + LogLevel.Warn, + LogLevel.Info, + LogLevel.Debug, + LogLevel.Trace, + LogLevel.All, + ]; + + for (const logLevel of orderedLogLevels) { + it(`returns the correct value for a '${logLevel.id}' level logger`, () => { + const levelLogger = new TestLogger(context, logLevel, appenderMocks, factory); + for (const level of orderedLogLevels) { + const levelEnabled = logLevel.supports(level); + expect(levelLogger.isLevelEnabled(level.id)).toEqual(levelEnabled); + } + }); + } + }); +}); diff --git a/packages/core/logging/core-logging-common-internal/src/logger.ts b/packages/core/logging/core-logging-common-internal/src/logger.ts new file mode 100644 index 00000000000000..69d00ed57f39f8 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/logger.ts @@ -0,0 +1,86 @@ +/* + * 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 { + Appender, + LogLevel, + LogRecord, + LoggerFactory, + LogMeta, + Logger, + LogLevelId, +} from '@kbn/logging'; + +/** + * @internal + */ +export type CreateLogRecordFn = ( + level: LogLevel, + errorOrMessage: string | Error, + meta?: Meta +) => LogRecord; + +/** + * A basic, abstract logger implementation that delegates the create of log records to the child's createLogRecord function. + * @internal + */ +export abstract class AbstractLogger implements Logger { + constructor( + protected readonly context: string, + protected readonly level: LogLevel, + protected readonly appenders: Appender[], + protected readonly factory: LoggerFactory + ) {} + + protected abstract createLogRecord( + level: LogLevel, + errorOrMessage: string | Error, + meta?: Meta + ): LogRecord; + + public trace(message: string, meta?: Meta): void { + this.log(this.createLogRecord(LogLevel.Trace, message, meta)); + } + + public debug(message: string, meta?: Meta): void { + this.log(this.createLogRecord(LogLevel.Debug, message, meta)); + } + + public info(message: string, meta?: Meta): void { + this.log(this.createLogRecord(LogLevel.Info, message, meta)); + } + + public warn(errorOrMessage: string | Error, meta?: Meta): void { + this.log(this.createLogRecord(LogLevel.Warn, errorOrMessage, meta)); + } + + public error(errorOrMessage: string | Error, meta?: Meta): void { + this.log(this.createLogRecord(LogLevel.Error, errorOrMessage, meta)); + } + + public fatal(errorOrMessage: string | Error, meta?: Meta): void { + this.log(this.createLogRecord(LogLevel.Fatal, errorOrMessage, meta)); + } + + public isLevelEnabled(levelId: LogLevelId): boolean { + return this.level.supports(LogLevel.fromId(levelId)); + } + + public log(record: LogRecord) { + if (!this.level.supports(record.level)) { + return; + } + for (const appender of this.appenders) { + appender.append(record); + } + } + + public get(...childContextPaths: string[]): Logger { + return this.factory.get(...[this.context, ...childContextPaths]); + } +} diff --git a/packages/core/logging/core-logging-common-internal/src/logger_context.test.ts b/packages/core/logging/core-logging-common-internal/src/logger_context.test.ts new file mode 100644 index 00000000000000..664b9840fd077a --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/logger_context.test.ts @@ -0,0 +1,26 @@ +/* + * 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 { getLoggerContext, getParentLoggerContext } from './logger_context'; + +describe('getLoggerContext', () => { + it('returns correct joined context name.', () => { + expect(getLoggerContext(['a', 'b', 'c'])).toEqual('a.b.c'); + expect(getLoggerContext(['a', 'b'])).toEqual('a.b'); + expect(getLoggerContext(['a'])).toEqual('a'); + expect(getLoggerContext([])).toEqual('root'); + }); +}); + +describe('getParentLoggerContext', () => { + it('returns correct parent context name.', () => { + expect(getParentLoggerContext('a.b.c')).toEqual('a.b'); + expect(getParentLoggerContext('a.b')).toEqual('a'); + expect(getParentLoggerContext('a')).toEqual('root'); + }); +}); diff --git a/packages/core/logging/core-logging-common-internal/src/logger_context.ts b/packages/core/logging/core-logging-common-internal/src/logger_context.ts new file mode 100644 index 00000000000000..d62a3fd5bea8a6 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/logger_context.ts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +/** + * Separator string that used within nested context name (eg. plugins.pid). + */ +export const CONTEXT_SEPARATOR = '.'; + +/** + * Name of the `root` context that always exists and sits at the top of logger hierarchy. + */ +export const ROOT_CONTEXT_NAME = 'root'; + +/** + * Name of the appender that is always presented and used by `root` logger by default. + */ +export const DEFAULT_APPENDER_NAME = 'default'; + +/** + * Helper method that joins separate string context parts into single context string. + * In case joined context is an empty string, `root` context name is returned. + * @param contextParts List of the context parts (e.g. ['parent', 'child']. + * @returns {string} Joined context string (e.g. 'parent.child'). + */ +export const getLoggerContext = (contextParts: string[]): string => { + return contextParts.join(CONTEXT_SEPARATOR) || ROOT_CONTEXT_NAME; +}; + +/** + * Helper method that returns parent context for the specified one. + * @param context Context to find parent for. + * @returns Name of the parent context or `root` if the context is the top level one. + */ +export const getParentLoggerContext = (context: string): string => { + const lastIndexOfSeparator = context.lastIndexOf(CONTEXT_SEPARATOR); + if (lastIndexOfSeparator === -1) { + return ROOT_CONTEXT_NAME; + } + + return context.slice(0, lastIndexOfSeparator); +}; diff --git a/packages/core/logging/core-logging-common-internal/tsconfig.json b/packages/core/logging/core-logging-common-internal/tsconfig.json new file mode 100644 index 00000000000000..25957cd665d111 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node", + ] + }, + "include": [ + "**/*.ts", + ] +} diff --git a/packages/core/logging/core-logging-server-internal/BUILD.bazel b/packages/core/logging/core-logging-server-internal/BUILD.bazel index 6fe13febb2fb06..1fc923fb7cae61 100644 --- a/packages/core/logging/core-logging-server-internal/BUILD.bazel +++ b/packages/core/logging/core-logging-server-internal/BUILD.bazel @@ -37,10 +37,12 @@ NPM_MODULE_EXTRA_FILES = [ RUNTIME_DEPS = [ "@npm//lodash", "@npm//moment-timezone", + "@npm//chalk", "@npm//elastic-apm-node", "//packages/kbn-safer-lodash-set", "//packages/kbn-config-schema", "//packages/kbn-std", + "//packages/core/logging/core-logging-common-internal", ] TYPES_DEPS = [ @@ -50,10 +52,12 @@ TYPES_DEPS = [ "@npm//rxjs", "@npm//@types/moment-timezone", "@npm//elastic-apm-node", + "@npm//chalk", "//packages/kbn-safer-lodash-set:npm_module_types", "//packages/kbn-logging:npm_module_types", "//packages/kbn-config-schema:npm_module_types", "//packages/core/base/core-base-server-internal:npm_module_types", + "//packages/core/logging/core-logging-common-internal:npm_module_types", "//packages/core/logging/core-logging-server:npm_module_types", ] diff --git a/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.ts b/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.ts index b1fe6943c704f7..0602ea81289afa 100644 --- a/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.ts +++ b/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.ts @@ -7,7 +7,7 @@ */ import { schema } from '@kbn/config-schema'; -import { Layout, LogRecord, DisposableAppender } from '@kbn/logging'; +import type { Layout, LogRecord, DisposableAppender } from '@kbn/logging'; import { Layouts } from '../../layouts/layouts'; const { literal, object } = schema; diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/index.ts b/packages/core/logging/core-logging-server-internal/src/layouts/conversions/index.ts index 9203fdd02278c8..dad8fd42c5c05e 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/index.ts +++ b/packages/core/logging/core-logging-server-internal/src/layouts/conversions/index.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -export type { Conversion } from './type'; - -export { LoggerConversion } from './logger'; -export { LevelConversion } from './level'; -export { MessageConversion } from './message'; -export { MetaConversion } from './meta'; export { PidConversion } from './pid'; -export { DateConversion } from './date'; +export { LevelConversion } from './level'; +export { LoggerConversion } from './logger'; +export { + DateConversion, + MessageConversion, + MetaConversion, +} from '@kbn/core-logging-common-internal'; diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/level.ts b/packages/core/logging/core-logging-server-internal/src/layouts/conversions/level.ts index 17e45555aa8731..38ae927b790a0e 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/level.ts +++ b/packages/core/logging/core-logging-server-internal/src/layouts/conversions/level.ts @@ -8,8 +8,7 @@ import chalk from 'chalk'; import { LogRecord, LogLevel } from '@kbn/logging'; - -import { Conversion } from './type'; +import type { Conversion } from '@kbn/core-logging-common-internal'; const LEVEL_COLORS = new Map([ [LogLevel.Fatal, chalk.red], diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/logger.ts b/packages/core/logging/core-logging-server-internal/src/layouts/conversions/logger.ts index 1cb6f06b4e2c6b..71be5e6d063654 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/logger.ts +++ b/packages/core/logging/core-logging-server-internal/src/layouts/conversions/logger.ts @@ -8,8 +8,7 @@ import chalk from 'chalk'; import { LogRecord } from '@kbn/logging'; - -import { Conversion } from './type'; +import type { Conversion } from '@kbn/core-logging-common-internal'; export const LoggerConversion: Conversion = { pattern: /%logger/g, diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/pid.ts b/packages/core/logging/core-logging-server-internal/src/layouts/conversions/pid.ts index cc43f4e874adc8..0d6237554b778b 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/conversions/pid.ts +++ b/packages/core/logging/core-logging-server-internal/src/layouts/conversions/pid.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { LogRecord } from '@kbn/logging'; -import { Conversion } from './type'; +import type { LogRecord } from '@kbn/logging'; +import type { Conversion } from '@kbn/core-logging-common-internal'; export const PidConversion: Conversion = { pattern: /%pid/g, diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.ts b/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.ts index 8206537ef7de74..58ddf5fd684f88 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.ts +++ b/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.ts @@ -7,10 +7,11 @@ */ import { schema } from '@kbn/config-schema'; -import { LogRecord, Layout } from '@kbn/logging'; - import { - Conversion, + PatternLayout as BasePatternLayout, + type Conversion, +} from '@kbn/core-logging-common-internal'; +import { LoggerConversion, LevelConversion, MetaConversion, @@ -19,9 +20,6 @@ import { DateConversion, } from './conversions'; -/** - * Default pattern used by PatternLayout if it's not overridden in the configuration. - */ const DEFAULT_PATTERN = `[%date][%level][%logger] %message`; export const patternSchema = schema.string({ @@ -50,23 +48,14 @@ const conversions: Conversion[] = [ * color highlighting (eg. to make log messages easier to read in the terminal). * @internal */ -export class PatternLayout implements Layout { +export class PatternLayout extends BasePatternLayout { public static configSchema = patternLayoutSchema; - constructor(private readonly pattern = DEFAULT_PATTERN, private readonly highlight = false) {} - - /** - * Formats `LogRecord` into a string based on the specified `pattern` and `highlighting` options. - * @param record Instance of `LogRecord` to format into string. - */ - public format(record: LogRecord): string { - let recordString = this.pattern; - for (const conversion of conversions) { - recordString = recordString.replace( - conversion.pattern, - conversion.convert.bind(null, record, this.highlight) - ); - } - return recordString; + constructor(pattern: string = DEFAULT_PATTERN, highlight: boolean = false) { + super({ + pattern, + highlight, + conversions, + }); } } diff --git a/packages/core/logging/core-logging-server-internal/src/logger.ts b/packages/core/logging/core-logging-server-internal/src/logger.ts index 7a18d9a74ebaa8..23718ca278a2e1 100644 --- a/packages/core/logging/core-logging-server-internal/src/logger.ts +++ b/packages/core/logging/core-logging-server-internal/src/logger.ts @@ -6,72 +6,16 @@ * Side Public License, v 1. */ import apmAgent from 'elastic-apm-node'; -import { - Appender, - LogLevel, - LogLevelId, - LogRecord, - LoggerFactory, - LogMeta, - Logger, -} from '@kbn/logging'; +import { LogLevel, LogRecord, LogMeta } from '@kbn/logging'; +import { AbstractLogger } from '@kbn/core-logging-common-internal'; function isError(x: any): x is Error { return x instanceof Error; } /** @internal */ -export class BaseLogger implements Logger { - constructor( - private readonly context: string, - private readonly level: LogLevel, - private readonly appenders: Appender[], - private readonly factory: LoggerFactory - ) {} - - public trace(message: string, meta?: Meta): void { - this.log(this.createLogRecord(LogLevel.Trace, message, meta)); - } - - public debug(message: string, meta?: Meta): void { - this.log(this.createLogRecord(LogLevel.Debug, message, meta)); - } - - public info(message: string, meta?: Meta): void { - this.log(this.createLogRecord(LogLevel.Info, message, meta)); - } - - public warn(errorOrMessage: string | Error, meta?: Meta): void { - this.log(this.createLogRecord(LogLevel.Warn, errorOrMessage, meta)); - } - - public error(errorOrMessage: string | Error, meta?: Meta): void { - this.log(this.createLogRecord(LogLevel.Error, errorOrMessage, meta)); - } - - public fatal(errorOrMessage: string | Error, meta?: Meta): void { - this.log(this.createLogRecord(LogLevel.Fatal, errorOrMessage, meta)); - } - - public isLevelEnabled(levelId: LogLevelId): boolean { - return this.level.supports(LogLevel.fromId(levelId)); - } - - public log(record: LogRecord) { - if (!this.level.supports(record.level)) { - return; - } - - for (const appender of this.appenders) { - appender.append(record); - } - } - - public get(...childContextPaths: string[]): Logger { - return this.factory.get(...[this.context, ...childContextPaths]); - } - - private createLogRecord( +export class BaseLogger extends AbstractLogger { + protected createLogRecord( level: LogLevel, errorOrMessage: string | Error, meta?: Meta diff --git a/packages/core/logging/core-logging-server-internal/src/logging_config.ts b/packages/core/logging/core-logging-server-internal/src/logging_config.ts index 4d79c593fb6f23..00eb1450f0abef 100644 --- a/packages/core/logging/core-logging-server-internal/src/logging_config.ts +++ b/packages/core/logging/core-logging-server-internal/src/logging_config.ts @@ -7,6 +7,12 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { + ROOT_CONTEXT_NAME, + DEFAULT_APPENDER_NAME, + getLoggerContext, + getParentLoggerContext, +} from '@kbn/core-logging-common-internal'; import type { AppenderConfigType, LoggerConfigType } from '@kbn/core-logging-server'; import { Appenders } from './appenders/appenders'; @@ -14,21 +20,6 @@ import { Appenders } from './appenders/appenders'; // (otherwise it assumes an array of A|B instead of a tuple [A,B]) const toTuple = (a: A, b: B): [A, B] => [a, b]; -/** - * Separator string that used within nested context name (eg. plugins.pid). - */ -const CONTEXT_SEPARATOR = '.'; - -/** - * Name of the `root` context that always exists and sits at the top of logger hierarchy. - */ -const ROOT_CONTEXT_NAME = 'root'; - -/** - * Name of the appender that is always presented and used by `root` logger by default. - */ -const DEFAULT_APPENDER_NAME = 'default'; - const levelSchema = schema.oneOf( [ schema.literal('all'), @@ -109,7 +100,7 @@ export class LoggingConfig { * @returns {string} Joined context string (e.g. 'parent.child'). */ public static getLoggerContext(contextParts: string[]) { - return contextParts.join(CONTEXT_SEPARATOR) || ROOT_CONTEXT_NAME; + return getLoggerContext(contextParts); } /** @@ -118,12 +109,7 @@ export class LoggingConfig { * @returns Name of the parent context or `root` if the context is the top level one. */ public static getParentLoggerContext(context: string) { - const lastIndexOfSeparator = context.lastIndexOf(CONTEXT_SEPARATOR); - if (lastIndexOfSeparator === -1) { - return ROOT_CONTEXT_NAME; - } - - return context.slice(0, lastIndexOfSeparator); + return getParentLoggerContext(context); } /** diff --git a/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.test.ts b/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.test.ts new file mode 100644 index 00000000000000..1b1bea2279b60f --- /dev/null +++ b/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { DiscoveredPlugin, PluginOpaqueId, PluginType } from '@kbn/core-base-common'; +import { type MockedLogger, loggerMock } from '@kbn/logging-mocks'; +import type { PluginInitializerContext } from '@kbn/core-plugins-browser'; +import { coreContextMock } from '@kbn/core-base-browser-mocks'; +import { createPluginInitializerContext } from './plugin_context'; + +const createPluginManifest = (pluginName: string): DiscoveredPlugin => { + return { + id: pluginName, + configPath: [pluginName], + type: PluginType.standard, + requiredPlugins: [], + optionalPlugins: [], + requiredBundles: [], + }; +}; + +const testPluginId = 'testPluginId'; + +describe('createPluginInitializerContext', () => { + let pluginId: PluginOpaqueId; + let pluginManifest: DiscoveredPlugin; + let pluginConfig: Record; + let coreContext: ReturnType; + let logger: MockedLogger; + let initContext: PluginInitializerContext; + + beforeEach(() => { + pluginId = Symbol(testPluginId); + pluginManifest = createPluginManifest(testPluginId); + pluginConfig = {}; + coreContext = coreContextMock.create(); + logger = coreContext.logger as MockedLogger; + + initContext = createPluginInitializerContext( + coreContext, + pluginId, + pluginManifest, + pluginConfig + ); + }); + + describe('logger.get', () => { + it('calls the underlying logger factory with the correct parameters', () => { + initContext.logger.get('service.sub'); + expect(logger.get).toHaveBeenCalledTimes(1); + expect(logger.get).toHaveBeenCalledWith('plugins', testPluginId, 'service.sub'); + }); + + it('returns the logger from the underlying factory', () => { + const underlyingLogger = loggerMock.create(); + logger.get.mockReturnValue(underlyingLogger); + expect(initContext.logger.get('anything')).toEqual(underlyingLogger); + }); + }); +}); diff --git a/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts index 41643b0e0250c3..351dd581b5f83c 100644 --- a/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts @@ -35,6 +35,11 @@ export function createPluginInitializerContext( return { opaqueId, env: coreContext.env, + logger: { + get(...contextParts) { + return coreContext.logger.get('plugins', pluginManifest.id, ...contextParts); + }, + }, config: { get() { return pluginConfig as unknown as T; diff --git a/packages/core/plugins/core-plugins-browser-internal/src/test_helpers/mocks.ts b/packages/core/plugins/core-plugins-browser-internal/src/test_helpers/mocks.ts index fcd4e80c02def1..7c013b17ec4cf9 100644 --- a/packages/core/plugins/core-plugins-browser-internal/src/test_helpers/mocks.ts +++ b/packages/core/plugins/core-plugins-browser-internal/src/test_helpers/mocks.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { loggerMock } from '@kbn/logging-mocks'; import type { PluginInitializerContext } from '@kbn/core-plugins-browser'; export const createPluginInitializerContextMock = (config: unknown = {}) => { @@ -25,6 +26,7 @@ export const createPluginInitializerContextMock = (config: unknown = {}) => { dist: false, }, }, + logger: loggerMock.create(), config: { get: () => config as T, }, diff --git a/packages/core/plugins/core-plugins-browser-mocks/BUILD.bazel b/packages/core/plugins/core-plugins-browser-mocks/BUILD.bazel index a6c47b536d2ef0..dbe94e7ba96498 100644 --- a/packages/core/plugins/core-plugins-browser-mocks/BUILD.bazel +++ b/packages/core/plugins/core-plugins-browser-mocks/BUILD.bazel @@ -36,12 +36,14 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ + "//packages/kbn-logging-mocks", ] TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/jest", "//packages/kbn-utility-types:npm_module_types", + "//packages/kbn-logging-mocks:npm_module_types", "//packages/core/plugins/core-plugins-browser-internal:npm_module_types", ] diff --git a/packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts b/packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts index c1539e78736835..75ec4e4570688d 100644 --- a/packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts +++ b/packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts @@ -7,6 +7,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; +import { loggerMock } from '@kbn/logging-mocks'; import type { PluginInitializerContext } from '@kbn/core-plugins-browser'; import type { PluginsService, PluginsServiceSetup } from '@kbn/core-plugins-browser-internal'; @@ -43,6 +44,7 @@ const createPluginInitializerContextMock = (config: unknown = {}) => { dist: false, }, }, + logger: loggerMock.create(), config: { get: () => config as T, }, diff --git a/packages/core/plugins/core-plugins-browser/src/plugin_initializer.ts b/packages/core/plugins/core-plugins-browser/src/plugin_initializer.ts index 14aaaff31e946c..c33abbfc386f9a 100644 --- a/packages/core/plugins/core-plugins-browser/src/plugin_initializer.ts +++ b/packages/core/plugins/core-plugins-browser/src/plugin_initializer.ts @@ -7,6 +7,7 @@ */ import type { PluginOpaqueId } from '@kbn/core-base-common'; +import type { LoggerFactory } from '@kbn/logging'; import type { PackageInfo, EnvironmentMode } from '@kbn/config'; import type { Plugin } from './plugin'; @@ -37,6 +38,7 @@ export interface PluginInitializerContext mode: Readonly; packageInfo: Readonly; }; + readonly logger: LoggerFactory; readonly config: { get: () => T; }; diff --git a/packages/core/root/core-root-browser-internal/BUILD.bazel b/packages/core/root/core-root-browser-internal/BUILD.bazel index a95b5d6d1c409d..05f41123181e3d 100644 --- a/packages/core/root/core-root-browser-internal/BUILD.bazel +++ b/packages/core/root/core-root-browser-internal/BUILD.bazel @@ -44,6 +44,7 @@ RUNTIME_DEPS = [ "//packages/kbn-i18n", "//packages/kbn-ebt-tools", "//packages/core/application/core-application-browser-internal", + "//packages/core/logging/core-logging-browser-internal", "//packages/core/injected-metadata/core-injected-metadata-browser-internal", "//packages/core/doc-links/core-doc-links-browser-internal", "//packages/core/theme/core-theme-browser-internal", @@ -76,6 +77,7 @@ TYPES_DEPS = [ "//packages/core/execution-context/core-execution-context-browser:npm_module_types", "//packages/core/application/core-application-browser-internal:npm_module_types", "//packages/core/base/core-base-browser-internal:npm_module_types", + "//packages/core/logging/core-logging-browser-internal:npm_module_types", "//packages/core/injected-metadata/core-injected-metadata-browser-internal:npm_module_types", "//packages/core/doc-links/core-doc-links-browser-internal:npm_module_types", "//packages/core/theme/core-theme-browser-internal:npm_module_types", diff --git a/packages/core/root/core-root-browser-internal/src/core_system.test.mocks.ts b/packages/core/root/core-root-browser-internal/src/core_system.test.mocks.ts index 69560623e1636e..36cdcb4ae75201 100644 --- a/packages/core/root/core-root-browser-internal/src/core_system.test.mocks.ts +++ b/packages/core/root/core-root-browser-internal/src/core_system.test.mocks.ts @@ -22,6 +22,7 @@ import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; import { renderingServiceMock } from '@kbn/core-rendering-browser-mocks'; import { integrationsServiceMock } from '@kbn/core-integrations-browser-mocks'; import { coreAppsMock } from '@kbn/core-apps-browser-mocks'; +import { loggingSystemMock } from '@kbn/core-logging-browser-mocks'; export const analyticsServiceStartMock = analyticsServiceMock.createAnalyticsServiceStart(); export const MockAnalyticsService = analyticsServiceMock.create(); @@ -137,3 +138,9 @@ export const ThemeServiceConstructor = jest.fn().mockImplementation(() => MockTh jest.doMock('@kbn/core-theme-browser-internal', () => ({ ThemeService: ThemeServiceConstructor, })); + +export const MockLoggingSystem = loggingSystemMock.create(); +export const LoggingSystemConstructor = jest.fn().mockImplementation(() => MockLoggingSystem); +jest.doMock('@kbn/core-logging-browser-internal', () => ({ + BrowserLoggingSystem: LoggingSystemConstructor, +})); diff --git a/packages/core/root/core-root-browser-internal/src/core_system.test.ts b/packages/core/root/core-root-browser-internal/src/core_system.test.ts index 8e2e980e5ea945..cb9618ce6034c3 100644 --- a/packages/core/root/core-root-browser-internal/src/core_system.test.ts +++ b/packages/core/root/core-root-browser-internal/src/core_system.test.ts @@ -38,8 +38,10 @@ import { MockAnalyticsService, analyticsServiceStartMock, fetchOptionalMemoryInfoMock, + MockLoggingSystem, + LoggingSystemConstructor, } from './core_system.test.mocks'; - +import type { EnvironmentMode } from '@kbn/config'; import { CoreSystem } from './core_system'; import { KIBANA_LOADED_EVENT, @@ -136,6 +138,7 @@ describe('constructor', () => { expect(CoreAppConstructor).toHaveBeenCalledTimes(1); expect(ThemeServiceConstructor).toHaveBeenCalledTimes(1); expect(AnalyticsServiceConstructor).toHaveBeenCalledTimes(1); + expect(LoggingSystemConstructor).toHaveBeenCalledTimes(1); }); it('passes injectedMetadata param to InjectedMetadataService', () => { @@ -180,6 +183,47 @@ describe('constructor', () => { stopCoreSystem(); expect(coreSystem.stop).toHaveBeenCalled(); }); + + describe('logging system', () => { + it('instantiate the logging system with the correct level when in dev mode', () => { + const envMode: EnvironmentMode = { + name: 'development', + dev: true, + prod: false, + }; + const injectedMetadata = { env: { mode: envMode } } as any; + + createCoreSystem({ + injectedMetadata, + }); + + expect(LoggingSystemConstructor).toHaveBeenCalledTimes(1); + expect(LoggingSystemConstructor).toHaveBeenCalledWith({ + logLevel: 'all', + }); + }); + it('instantiate the logging system with the correct level when in production mode', () => { + const envMode: EnvironmentMode = { + name: 'production', + dev: false, + prod: true, + }; + const injectedMetadata = { env: { mode: envMode } } as any; + + createCoreSystem({ + injectedMetadata, + }); + + expect(LoggingSystemConstructor).toHaveBeenCalledTimes(1); + expect(LoggingSystemConstructor).toHaveBeenCalledWith({ + logLevel: 'warn', + }); + }); + it('retrieves the logger factory from the logging system', () => { + createCoreSystem({}); + expect(MockLoggingSystem.asLoggerFactory).toHaveBeenCalledTimes(1); + }); + }); }); describe('#setup()', () => { diff --git a/packages/core/root/core-root-browser-internal/src/core_system.ts b/packages/core/root/core-root-browser-internal/src/core_system.ts index b3eae041b785de..eb61e0547279db 100644 --- a/packages/core/root/core-root-browser-internal/src/core_system.ts +++ b/packages/core/root/core-root-browser-internal/src/core_system.ts @@ -7,11 +7,13 @@ */ import { filter, firstValueFrom } from 'rxjs'; +import type { LogLevelId } from '@kbn/logging'; import type { CoreContext } from '@kbn/core-base-browser-internal'; import { InjectedMetadataService, type InjectedMetadataParams, } from '@kbn/core-injected-metadata-browser-internal'; +import { BrowserLoggingSystem } from '@kbn/core-logging-browser-internal'; import { DocLinksService } from '@kbn/core-doc-links-browser-internal'; import { ThemeService } from '@kbn/core-theme-browser-internal'; import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-browser'; @@ -78,6 +80,7 @@ interface ExtendedNavigator { * @internal */ export class CoreSystem { + private readonly loggingSystem: BrowserLoggingSystem; private readonly analytics: AnalyticsService; private readonly fatalErrors: FatalErrorsService; private readonly injectedMetadata: InjectedMetadataService; @@ -106,20 +109,24 @@ export class CoreSystem { this.rootDomElement = rootDomElement; - this.i18n = new I18nService(); + const logLevel: LogLevelId = injectedMetadata.env.mode.dev ? 'all' : 'warn'; + this.loggingSystem = new BrowserLoggingSystem({ logLevel }); this.injectedMetadata = new InjectedMetadataService({ injectedMetadata, }); - this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; + this.coreContext = { + coreId: Symbol('core'), + env: injectedMetadata.env, + logger: this.loggingSystem.asLoggerFactory(), + }; + this.i18n = new I18nService(); this.analytics = new AnalyticsService(this.coreContext); - this.fatalErrors = new FatalErrorsService(rootDomElement, () => { // Stop Core before rendering any fatal errors into the DOM this.stop(); }); - this.theme = new ThemeService(); this.notifications = new NotificationsService(); this.http = new HttpService(); @@ -136,7 +143,6 @@ export class CoreSystem { this.integrations = new IntegrationsService(); this.deprecations = new DeprecationsService(); this.executionContext = new ExecutionContextService(); - this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreAppsService(this.coreContext); diff --git a/packages/kbn-journeys/journey/journey_ftr_harness.ts b/packages/kbn-journeys/journey/journey_ftr_harness.ts index c8e876ce2873ef..7b9d8cc0266fc3 100644 --- a/packages/kbn-journeys/journey/journey_ftr_harness.ts +++ b/packages/kbn-journeys/journey/journey_ftr_harness.ts @@ -118,7 +118,6 @@ export class JourneyFtrHarness { private async onSetup() { await Promise.all([ - this.setupApm(), this.setupBrowserAndPage(), asyncForEach(this.journeyConfig.getEsArchives(), async (esArchive) => { await this.esArchiver.load(esArchive); @@ -127,6 +126,10 @@ export class JourneyFtrHarness { await this.kibanaServer.importExport.load(kbnArchive); }), ]); + + // It is important that we start the APM transaction after we open the browser and all the test data is loaded + // so that the scalability data extractor can focus on just the APM data produced by Kibana running under test. + await this.setupApm(); } private async tearDownBrowserAndPage() { @@ -181,9 +184,12 @@ export class JourneyFtrHarness { } private async onTeardown() { + await this.tearDownBrowserAndPage(); + // It is important that we complete the APM transaction after we close the browser and before we start + // unloading the test data so that the scalability data extractor can focus on just the APM data produced + // by Kibana running under test. + await this.teardownApm(); await Promise.all([ - this.tearDownBrowserAndPage(), - this.teardownApm(), asyncForEach(this.journeyConfig.getEsArchives(), async (esArchive) => { await this.esArchiver.unload(esArchive); }), diff --git a/packages/kbn-monaco/src/helpers.ts b/packages/kbn-monaco/src/helpers.ts index 25040bc1a55f16..defdd00d6fdc13 100644 --- a/packages/kbn-monaco/src/helpers.ts +++ b/packages/kbn-monaco/src/helpers.ts @@ -12,10 +12,12 @@ function registerLanguage(language: LangModuleType) { const { ID, lexerRules, languageConfiguration } = language; monaco.languages.register({ id: ID }); - monaco.languages.setMonarchTokensProvider(ID, lexerRules); - if (languageConfiguration) { - monaco.languages.setLanguageConfiguration(ID, languageConfiguration); - } + monaco.languages.onLanguage(ID, () => { + monaco.languages.setMonarchTokensProvider(ID, lexerRules); + if (languageConfiguration) { + monaco.languages.setLanguageConfiguration(ID, languageConfiguration); + } + }); } export { registerLanguage }; diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts index 6a08c25b6347a8..07a24c8c8bd2ea 100644 --- a/packages/kbn-monaco/src/monaco_imports.ts +++ b/packages/kbn-monaco/src/monaco_imports.ts @@ -23,6 +23,7 @@ import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js'; // Needed for brackets matching highlight +import 'monaco-editor/esm/vs/language/json/monaco.contribution.js'; import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; // Needed for basic javascript support import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution.js'; // Needed for basic xml support diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index 54fc26cbf76dd3..8a69b05b9425fc 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -12,11 +12,9 @@ import { EsqlLang } from './esql'; import { monaco } from './monaco_imports'; import { registerLanguage } from './helpers'; -// @ts-ignore +import jsonWorkerSrc from '!!raw-loader!../../target_workers/json.editor.worker.js'; import xJsonWorkerSrc from '!!raw-loader!../../target_workers/xjson.editor.worker.js'; -// @ts-ignore import defaultWorkerSrc from '!!raw-loader!../../target_workers/default.editor.worker.js'; -// @ts-ignore import painlessWorkerSrc from '!!raw-loader!../../target_workers/painless.editor.worker.js'; /** @@ -32,6 +30,7 @@ registerLanguage(EsqlLang); const mapLanguageIdToWorker: { [key: string]: any } = { [XJsonLang.ID]: xJsonWorkerSrc, [PainlessLang.ID]: painlessWorkerSrc, + [monaco.languages.json.jsonDefaults.languageId]: jsonWorkerSrc, }; // @ts-ignore diff --git a/packages/kbn-monaco/src/worker.d.ts b/packages/kbn-monaco/src/worker.d.ts new file mode 100644 index 00000000000000..6544070c684d85 --- /dev/null +++ b/packages/kbn-monaco/src/worker.d.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +declare module '!!raw-loader!*.editor.worker.js' { + const contents: string; + + // eslint-disable-next-line import/no-default-export + export default contents; +} diff --git a/packages/kbn-monaco/src/xjson/language.ts b/packages/kbn-monaco/src/xjson/language.ts index 017ae0a8c590b0..6ec9b1149c7079 100644 --- a/packages/kbn-monaco/src/xjson/language.ts +++ b/packages/kbn-monaco/src/xjson/language.ts @@ -12,16 +12,12 @@ import { monaco } from '../monaco_imports'; import { WorkerProxyService } from './worker_proxy_service'; import { ID } from './constants'; -const wps = new WorkerProxyService(); +const OWNER = 'XJSON_GRAMMAR_CHECKER'; monaco.languages.onLanguage(ID, async () => { - return wps.setup(); -}); - -const OWNER = 'XJSON_GRAMMAR_CHECKER'; + const wps = new WorkerProxyService(); -export const registerGrammarChecker = () => { - const allDisposables: monaco.IDisposable[] = []; + wps.setup(); const updateAnnotations = async (model: monaco.editor.IModel): Promise => { if (model.isDisposed()) { @@ -50,21 +46,20 @@ export const registerGrammarChecker = () => { }; const onModelAdd = (model: monaco.editor.IModel) => { - if (model.getModeId() === ID) { - allDisposables.push( - model.onDidChangeContent(async () => { - updateAnnotations(model); - }) - ); + if (model.getModeId() !== ID) { + return; + } + const { dispose } = model.onDidChangeContent(async () => { updateAnnotations(model); - } - }; - allDisposables.push(monaco.editor.onDidCreateModel(onModelAdd)); - return () => { - wps.stop(); - allDisposables.forEach((d) => d.dispose()); + }); + + model.onWillDispose(() => { + dispose(); + }); + + updateAnnotations(model); }; -}; -registerGrammarChecker(); + monaco.editor.onDidCreateModel(onModelAdd); +}); diff --git a/packages/kbn-monaco/webpack.config.js b/packages/kbn-monaco/webpack.config.js index d35c60b155475e..8c6f82cdb21f54 100644 --- a/packages/kbn-monaco/webpack.config.js +++ b/packages/kbn-monaco/webpack.config.js @@ -8,43 +8,43 @@ const path = require('path'); -const createLangWorkerConfig = (lang) => { - const entry = - lang === 'default' - ? 'monaco-editor/esm/vs/editor/editor.worker.js' - : path.resolve(__dirname, 'src', lang, 'worker', `${lang}.worker.ts`); +const getWorkerEntry = (language) => { + switch (language) { + case 'default': + return 'monaco-editor/esm/vs/editor/editor.worker.js'; + case 'json': + return 'monaco-editor/esm/vs/language/json/json.worker.js'; + default: + return path.resolve(__dirname, 'src', language, 'worker', `${language}.worker.ts`); + } +}; - return { - mode: 'production', - entry, - output: { - path: path.resolve(__dirname, 'target_workers'), - filename: `${lang}.editor.worker.js`, - }, - resolve: { - extensions: ['.js', '.ts', '.tsx'], - }, - stats: 'errors-only', - module: { - rules: [ - { - test: /\.(js|ts)$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - options: { - babelrc: false, - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, +const getWorkerConfig = (language) => ({ + mode: 'production', + entry: getWorkerEntry(language), + output: { + path: path.resolve(__dirname, 'target_workers'), + filename: `${language}.editor.worker.js`, + }, + resolve: { + extensions: ['.js', '.ts', '.tsx'], + }, + stats: 'errors-only', + module: { + rules: [ + { + test: /\.(js|ts)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], }, }, - ], - }, - }; -}; + }, + ], + }, +}); -module.exports = [ - createLangWorkerConfig('xjson'), - createLangWorkerConfig('painless'), - createLangWorkerConfig('default'), -]; +module.exports = ['default', 'json', 'painless', 'xjson'].map(getWorkerConfig); diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 790b8ccf028cf4..c4f04ec3c33129 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -32,6 +32,7 @@ export { } from '@kbn/core-saved-objects-browser-mocks'; export { applicationServiceMock, scopedHistoryMock } from '@kbn/core-application-browser-mocks'; export { deprecationsServiceMock } from '@kbn/core-deprecations-browser-mocks'; +export { loggingSystemMock } from '@kbn/core-logging-browser-mocks'; function createStorageMock() { const storageMock: jest.Mocked = { diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index 7dce76528a5a05..b1aa1e5df92319 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -85,7 +85,7 @@ describe('checking migration metadata changes on all registered SO types', () => "event_loop_delays_daily": "d2ed39cf669577d90921c176499908b4943fb7bd", "exception-list": "fe8cc004fd2742177cdb9300f4a67689463faf9c", "exception-list-agnostic": "49fae8fcd1967cc4be45ba2a2c66c4afbc1e341b", - "file": "280f28bd48b3ad1f1a9f84c6c0ae6dd5ed1179da", + "file": "70c2a768473057157f6ee5d29a436e5288d22ff4", "file-upload-usage-collection-telemetry": "8478924cf0057bd90df737155b364f98d05420a5", "fileShare": "3f88784b041bb8728a7f40763a08981828799a75", "fleet-fleet-server-host": "f00ca963f1bee868806319789cdc33f1f53a97e2", diff --git a/src/plugins/files/server/saved_objects/file.ts b/src/plugins/files/server/saved_objects/file.ts index af8f7ef9ef0890..54d7fb57a3a075 100644 --- a/src/plugins/files/server/saved_objects/file.ts +++ b/src/plugins/files/server/saved_objects/file.ts @@ -48,7 +48,7 @@ const properties: Properties = { export const fileObjectType: SavedObjectsType = { name: FILE_SO_TYPE, hidden: true, - namespaceType: 'multiple-isolated', + namespaceType: 'agnostic', management: { importableAndExportable: false, }, diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx index c85e03c433d673..b6edbbd1da9769 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx @@ -238,4 +238,39 @@ storiesOf('CodeEditor', module) text: 'Hover dialog example can be triggered by hovering over a word', }, } + ) + .add( + 'json support', + () => ( +
+ { + monacoEditor.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + schemas: [ + { + uri: editor.getModel()?.uri.toString() ?? '', + fileMatch: ['*'], + schema: { + type: 'object', + properties: { + version: { + enum: ['v1', 'v2'], + }, + }, + }, + }, + ], + }); + }} + height={250} + value="{}" + onChange={action('onChange')} + /> +
+ ), + { + info: { text: 'JSON language support' }, + } ); diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx index 97a8d8a8490837..6d9b4f4ce384f9 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx @@ -34,23 +34,36 @@ const simpleLogLang: monaco.languages.IMonarchLanguage = { }, }; -monaco.languages.register({ id: 'loglang' }); -monaco.languages.setMonarchTokensProvider('loglang', simpleLogLang); - const logs = ` [Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice! [Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed [Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome `; -class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -} - describe('', () => { - window.ResizeObserver = ResizeObserver; + beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + window.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + }; + + monaco.languages.register({ id: 'loglang' }); + monaco.languages.setMonarchTokensProvider('loglang', simpleLogLang); + }); test('is rendered', () => { const component = mountWithIntl( diff --git a/src/plugins/presentation_util/public/components/expression_input/language.ts b/src/plugins/presentation_util/public/components/expression_input/language.ts index 03b868af42aaa5..8f50ae97fa83d6 100644 --- a/src/plugins/presentation_util/public/components/expression_input/language.ts +++ b/src/plugins/presentation_util/public/components/expression_input/language.ts @@ -115,6 +115,8 @@ const expressionsLanguage: ExpressionsLanguage = { export function registerExpressionsLanguage(functions: ExpressionFunction[]) { expressionsLanguage.keywords = functions.map((fn) => fn.name); expressionsLanguage.deprecated = functions.filter((fn) => fn.deprecated).map((fn) => fn.name); - monaco.languages.register({ id: EXPRESSIONS_LANGUAGE_ID }); - monaco.languages.setMonarchTokensProvider(EXPRESSIONS_LANGUAGE_ID, expressionsLanguage); + monaco.languages.onLanguage(EXPRESSIONS_LANGUAGE_ID, () => { + monaco.languages.register({ id: EXPRESSIONS_LANGUAGE_ID }); + monaco.languages.setMonarchTokensProvider(EXPRESSIONS_LANGUAGE_ID, expressionsLanguage); + }); } diff --git a/tsconfig.base.json b/tsconfig.base.json index 5c15b00b23d126..1b5fba04354e18 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -204,6 +204,12 @@ "@kbn/core-lifecycle-server-internal/*": ["packages/core/lifecycle/core-lifecycle-server-internal/*"], "@kbn/core-lifecycle-server-mocks": ["packages/core/lifecycle/core-lifecycle-server-mocks"], "@kbn/core-lifecycle-server-mocks/*": ["packages/core/lifecycle/core-lifecycle-server-mocks/*"], + "@kbn/core-logging-browser-internal": ["packages/core/logging/core-logging-browser-internal"], + "@kbn/core-logging-browser-internal/*": ["packages/core/logging/core-logging-browser-internal/*"], + "@kbn/core-logging-browser-mocks": ["packages/core/logging/core-logging-browser-mocks"], + "@kbn/core-logging-browser-mocks/*": ["packages/core/logging/core-logging-browser-mocks/*"], + "@kbn/core-logging-common-internal": ["packages/core/logging/core-logging-common-internal"], + "@kbn/core-logging-common-internal/*": ["packages/core/logging/core-logging-common-internal/*"], "@kbn/core-logging-server": ["packages/core/logging/core-logging-server"], "@kbn/core-logging-server/*": ["packages/core/logging/core-logging-server/*"], "@kbn/core-logging-server-internal": ["packages/core/logging/core-logging-server-internal"], diff --git a/x-pack/plugins/lens/common/expressions/collapse/index.ts b/x-pack/plugins/lens/common/expressions/collapse/index.ts index 43874859411fc7..2b1e89af08bd47 100644 --- a/x-pack/plugins/lens/common/expressions/collapse/index.ts +++ b/x-pack/plugins/lens/common/expressions/collapse/index.ts @@ -16,6 +16,8 @@ export interface CollapseArgs { fn: CollapseFunction[]; } +export type { CollapseExpressionFunction }; + /** * Collapses multiple rows into a single row using the specified function. * diff --git a/x-pack/plugins/lens/common/expressions/datatable/datatable_column.ts b/x-pack/plugins/lens/common/expressions/datatable/datatable_column.ts index f955cc1dfa2cbc..16e76d3baf2e4a 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/datatable_column.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/datatable_column.ts @@ -48,13 +48,14 @@ export interface ColumnState { } export type DatatableColumnResult = ColumnState & { type: 'lens_datatable_column' }; - -export const datatableColumn: ExpressionFunctionDefinition< +export type DatatableColumnFunction = ExpressionFunctionDefinition< 'lens_datatable_column', null, ColumnState & { sortingHint?: SortingHint }, DatatableColumnResult -> = { +>; + +export const datatableColumn: DatatableColumnFunction = { name: 'lens_datatable_column', aliases: [], type: 'lens_datatable_column', diff --git a/x-pack/plugins/lens/common/expressions/datatable/index.ts b/x-pack/plugins/lens/common/expressions/datatable/index.ts index 2fa0312360297b..7003fd8d486b8f 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/index.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/index.ts @@ -8,4 +8,4 @@ export * from './datatable_column'; export * from './datatable'; -export type { DatatableProps } from './types'; +export type { DatatableProps, DatatableExpressionFunction } from './types'; diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_tokenization.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_tokenization.tsx index 17394560f8031c..275a540b3c4d82 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_tokenization.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_tokenization.tsx @@ -62,5 +62,7 @@ export const lexerRules = { }, } as monaco.languages.IMonarchLanguage; -monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, lexerRules); -monaco.languages.setLanguageConfiguration(LANGUAGE_ID, languageConfiguration); +monaco.languages.onLanguage(LANGUAGE_ID, () => { + monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, lexerRules); + monaco.languages.setLanguageConfiguration(LANGUAGE_ID, languageConfiguration); +}); diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx index e6683aee134999..a38d669d73cd5a 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx @@ -550,22 +550,16 @@ describe('Datatable Visualization', () => { expect(columnArgs[0].arguments).toEqual( expect.objectContaining({ columnId: ['c'], - hidden: [], - width: [], - isTransposed: [], + palette: [expect.any(Object)], transposable: [true], - alignment: [], colorMode: ['none'], }) ); expect(columnArgs[1].arguments).toEqual( expect.objectContaining({ columnId: ['b'], - hidden: [], - width: [], - isTransposed: [], + palette: [expect.objectContaining({})], transposable: [true], - alignment: [], colorMode: ['none'], }) ); @@ -592,14 +586,16 @@ describe('Datatable Visualization', () => { }); it('sets pagination based on state', () => { - expect(getDatatableExpressionArgs({ ...defaultExpressionTableState }).pageSize).toEqual([]); + expect(getDatatableExpressionArgs({ ...defaultExpressionTableState }).pageSize).toEqual( + undefined + ); expect( getDatatableExpressionArgs({ ...defaultExpressionTableState, paging: { size: 20, enabled: false }, }).pageSize - ).toEqual([]); + ).toEqual(undefined); expect( getDatatableExpressionArgs({ diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx index 60de4d4d1148c9..ccfaff17a8ecbf 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render } from 'react-dom'; -import { Ast, AstFunction } from '@kbn/interpreter'; +import { Ast } from '@kbn/interpreter'; import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { PaletteRegistry, CUSTOM_PALETTE } from '@kbn/coloring'; @@ -16,6 +16,7 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; import { IconChartDatatable } from '@kbn/chart-icons'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; +import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common'; import type { FormBasedPersistedState } from '../../datasources/form_based/types'; import type { SuggestionRequest, @@ -28,7 +29,14 @@ import { TableDimensionEditor } from './components/dimension_editor'; import { TableDimensionEditorAdditionalSection } from './components/dimension_editor_addtional_section'; import type { LayerType } from '../../../common'; import { getDefaultSummaryLabel } from '../../../common/expressions/datatable/summary'; -import type { ColumnState, SortingState, PagingState } from '../../../common/expressions'; +import type { + ColumnState, + SortingState, + PagingState, + CollapseExpressionFunction, + DatatableColumnFunction, + DatatableExpressionFunction, +} from '../../../common/expressions'; import { DataTableToolbar } from './components/toolbar'; export interface DatatableVisualizationState { @@ -398,108 +406,84 @@ export const getDatatableVisualization = ({ const datasourceExpression = datasourceExpressionsByLayers[state.layerId]; + const lensCollapseFnAsts = columns + .filter((c) => c.collapseFn) + .map((c) => + buildExpressionFunction('lens_collapse', { + by: columns + .filter( + (col) => + col.columnId !== c.columnId && + datasource!.getOperationForColumnId(col.columnId)?.isBucketed + ) + .map((col) => col.columnId), + metric: columns + .filter((col) => !datasource!.getOperationForColumnId(col.columnId)?.isBucketed) + .map((col) => col.columnId), + fn: [c.collapseFn!], + }).toAst() + ); + + const datatableFnAst = buildExpressionFunction('lens_datatable', { + title: title || '', + description: description || '', + columns: columns + .filter((c) => !c.collapseFn) + .map((column) => { + const paletteParams = { + ...column.palette?.params, + // rewrite colors and stops as two distinct arguments + colors: (column.palette?.params?.stops || []).map(({ color }) => color), + stops: + column.palette?.params?.name === 'custom' + ? (column.palette?.params?.stops || []).map(({ stop }) => stop) + : [], + reverse: false, // managed at UI level + }; + const sortingHint = datasource!.getOperationForColumnId(column.columnId)!.sortingHint; + + const hasNoSummaryRow = column.summaryRow == null || column.summaryRow === 'none'; + + const canColor = + datasource!.getOperationForColumnId(column.columnId)?.dataType === 'number'; + + const datatableColumnFn = buildExpressionFunction( + 'lens_datatable_column', + { + columnId: column.columnId, + hidden: column.hidden, + oneClickFilter: column.oneClickFilter, + width: column.width, + isTransposed: column.isTransposed, + transposable: !datasource!.getOperationForColumnId(column.columnId)?.isBucketed, + alignment: column.alignment, + colorMode: canColor && column.colorMode ? column.colorMode : 'none', + palette: paletteService.get(CUSTOM_PALETTE).toExpression(paletteParams), + summaryRow: hasNoSummaryRow ? undefined : column.summaryRow!, + summaryLabel: hasNoSummaryRow + ? undefined + : column.summaryLabel ?? getDefaultSummaryLabel(column.summaryRow!), + sortingHint, + } + ); + return buildExpression([datatableColumnFn]).toAst(); + }), + sortingColumnId: state.sorting?.columnId || '', + sortingDirection: state.sorting?.direction || 'none', + fitRowToContent: state.rowHeight === 'auto', + headerRowHeight: state.headerRowHeight ?? 'single', + rowHeightLines: + !state.rowHeight || state.rowHeight === 'single' ? 1 : state.rowHeightLines ?? 2, + headerRowHeightLines: + !state.headerRowHeight || state.headerRowHeight === 'single' + ? 1 + : state.headerRowHeightLines ?? 2, + pageSize: state.paging?.enabled ? state.paging.size : undefined, + }).toAst(); + return { type: 'expression', - chain: [ - ...(datasourceExpression?.chain ?? []), - ...columns - .filter((c) => c.collapseFn) - .map((c) => { - return { - type: 'function', - function: 'lens_collapse', - arguments: { - by: columns - .filter( - (col) => - col.columnId !== c.columnId && - datasource!.getOperationForColumnId(col.columnId)?.isBucketed - ) - .map((col) => col.columnId), - metric: columns - .filter((col) => !datasource!.getOperationForColumnId(col.columnId)?.isBucketed) - .map((col) => col.columnId), - fn: [c.collapseFn!], - }, - } as AstFunction; - }), - { - type: 'function', - function: 'lens_datatable', - arguments: { - title: [title || ''], - description: [description || ''], - columns: columns - .filter((c) => !c.collapseFn) - .map((column) => { - const paletteParams = { - ...column.palette?.params, - // rewrite colors and stops as two distinct arguments - colors: (column.palette?.params?.stops || []).map(({ color }) => color), - stops: - column.palette?.params?.name === 'custom' - ? (column.palette?.params?.stops || []).map(({ stop }) => stop) - : [], - reverse: false, // managed at UI level - }; - const sortingHint = datasource!.getOperationForColumnId( - column.columnId - )!.sortingHint; - - const hasNoSummaryRow = column.summaryRow == null || column.summaryRow === 'none'; - - const canColor = - datasource!.getOperationForColumnId(column.columnId)?.dataType === 'number'; - - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_datatable_column', - arguments: { - columnId: [column.columnId], - hidden: typeof column.hidden === 'undefined' ? [] : [column.hidden], - oneClickFilter: - typeof column.oneClickFilter === 'undefined' - ? [] - : [column.oneClickFilter], - width: typeof column.width === 'undefined' ? [] : [column.width], - isTransposed: - typeof column.isTransposed === 'undefined' ? [] : [column.isTransposed], - transposable: [ - !datasource!.getOperationForColumnId(column.columnId)?.isBucketed, - ], - alignment: - typeof column.alignment === 'undefined' ? [] : [column.alignment], - colorMode: [canColor && column.colorMode ? column.colorMode : 'none'], - palette: [paletteService.get(CUSTOM_PALETTE).toExpression(paletteParams)], - summaryRow: hasNoSummaryRow ? [] : [column.summaryRow!], - summaryLabel: hasNoSummaryRow - ? [] - : [column.summaryLabel ?? getDefaultSummaryLabel(column.summaryRow!)], - sortingHint: sortingHint ? [sortingHint] : [], - }, - }, - ], - }; - }), - sortingColumnId: [state.sorting?.columnId || ''], - sortingDirection: [state.sorting?.direction || 'none'], - fitRowToContent: [state.rowHeight === 'auto'], - headerRowHeight: [state.headerRowHeight ?? 'single'], - rowHeightLines: [ - !state.rowHeight || state.rowHeight === 'single' ? 1 : state.rowHeightLines ?? 2, - ], - headerRowHeightLines: [ - !state.headerRowHeight || state.headerRowHeight === 'single' - ? 1 - : state.headerRowHeightLines ?? 2, - ], - pageSize: state.paging?.enabled ? [state.paging.size] : [], - }, - }, - ], + chain: [...(datasourceExpression?.chain ?? []), ...lensCollapseFnAsts, datatableFnAst], }; }, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.test.ts index 7fac9550996d24..c00facef1df78e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.test.ts @@ -7,6 +7,7 @@ import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { fetchIndexShardSize } from './fetch_index_shard_size'; +import { estypes } from '@elastic/elasticsearch'; jest.mock('../../static_globals', () => ({ Globals: { @@ -24,7 +25,6 @@ import { Globals } from '../../static_globals'; describe('fetchIndexShardSize', () => { const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; - const clusters = [ { clusterUuid: 'cluster123', @@ -34,7 +34,21 @@ describe('fetchIndexShardSize', () => { const size = 10; const shardIndexPatterns = '*'; const threshold = 0.00000001; - const esRes = { + + const esRes: estypes.SearchResponse = { + took: 1, + timed_out: false, + _shards: { + total: 0, + successful: 0, + failed: 0, + skipped: 0, + }, + hits: { + total: 0, + max_score: 0, + hits: [], + }, aggregations: { clusters: { buckets: [ @@ -48,6 +62,39 @@ describe('fetchIndexShardSize', () => { { key: '.monitoring-es-7-2022.01.27', doc_count: 30, + hits: { + hits: { + total: { + value: 30, + relation: 'eq', + }, + max_score: null, + hits: [ + { + _index: '.monitoring-es-7-2022.01.27', + _id: 'JVkunX4BfK-FILsH9Wr_', + _score: null, + _source: { + index_stats: { + shards: { + primaries: 2, + }, + primaries: { + store: { + size_in_bytes: 2171105970, + }, + }, + }, + }, + sort: [1643314607570], + }, + ], + }, + }, + }, + { + key: '.monitoring-es-7-2022.01.28', + doc_count: 30, hits: { hits: { total: { @@ -67,7 +114,7 @@ describe('fetchIndexShardSize', () => { }, primaries: { store: { - size_in_bytes: 3537949, + size_in_bytes: 1073741823, }, }, }, @@ -118,12 +165,9 @@ describe('fetchIndexShardSize', () => { }, }, }; - it('fetch as expected', async () => { - esClient.search.mockResponse( - // @ts-expect-error not full response interface - esRes - ); + it('fetch as expected', async () => { + esClient.search.mockResponse(esRes); const result = await fetchIndexShardSize( esClient, clusters, @@ -135,7 +179,13 @@ describe('fetchIndexShardSize', () => { { ccs: undefined, shardIndex: '.monitoring-es-7-2022.01.27', - shardSize: 0, + shardSize: 1.01, + clusterUuid: 'NG2d5jHiSBGPE6HLlUN2Bg', + }, + { + ccs: undefined, + shardIndex: '.monitoring-es-7-2022.01.28', + shardSize: 1, clusterUuid: 'NG2d5jHiSBGPE6HLlUN2Bg', }, { @@ -146,6 +196,27 @@ describe('fetchIndexShardSize', () => { }, ]); }); + + it('higher alert threshold', async () => { + esClient.search.mockResponse(esRes); + const oneGBThreshold = 1; + const result = await fetchIndexShardSize( + esClient, + clusters, + oneGBThreshold, + shardIndexPatterns, + size + ); + expect(result).toEqual([ + { + ccs: undefined, + shardIndex: '.monitoring-es-7-2022.01.27', + shardSize: 1.01, + clusterUuid: 'NG2d5jHiSBGPE6HLlUN2Bg', + }, + ]); + }); + it('should call ES with correct query', async () => { await fetchIndexShardSize(esClient, clusters, threshold, shardIndexPatterns, size); expect(esClient.search).toHaveBeenCalledWith({ @@ -201,6 +272,7 @@ describe('fetchIndexShardSize', () => { }, }); }); + it('should call ES with correct query when ccs disabled', async () => { // @ts-ignore Globals.app.config.ui.ccs.enabled = false; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index f8163f5c35bc62..5c32794cbf2128 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -26,7 +26,7 @@ const memoizedIndexPatterns = (globPatterns: string) => { ) as RegExPatterns; }; -const gbMultiplier = 1000000000; +const gbMultiplier = Math.pow(1024, 3); export async function fetchIndexShardSize( esClient: ElasticsearchClient, diff --git a/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts b/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts index dc8ab97c5f1878..598a62265f1c2f 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/ping/ping.ts @@ -94,12 +94,12 @@ export const MonitorType = t.intersection([ id: t.string, status: t.string, type: t.string, + check_group: t.string, }), t.partial({ duration: t.type({ us: t.number, }), - check_group: t.string, ip: t.string, name: t.string, timespan: t.type({ @@ -268,6 +268,7 @@ export const makePing = (f: { status: f.status || 'up', duration: { us: f.duration || 100000 }, name: f.name, + check_group: 'myCheckGroup', }, ...(f.location ? { observer: { geo: { name: f.location } } } : {}), ...(f.url ? { url: { full: f.url } } : {}), diff --git a/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx b/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx index fc365abd823b92..66d086fcea9d7b 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx @@ -82,7 +82,7 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib async navigateToEditMonitor() { await this.clickByTestSubj('syntheticsMonitorListActions'); - await page.click('text=Edit'); + await page.click('text=Edit', { timeout: 2 * 60 * 1000 }); await this.findByText('Edit monitor'); }, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx index 81ecb7dc60027f..ebc6cc265dfa50 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/browser_steps_list.tsx @@ -103,7 +103,7 @@ export const BrowserStepsList = ({ steps, error, loading, showStepNumber = false aria-label={VIEW_DETAILS} title={VIEW_DETAILS} size="s" - href={`${basePath}/app/uptime/journey/${item.monitor.check_group}/step/${item.synthetics?.step?.index}`} + href={`${basePath}/app/synthetics/journey/${item.monitor.check_group}/step/${item.synthetics?.step?.index}`} target="_self" iconType="apmTrace" /> diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/empty_thumbnail.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/empty_thumbnail.tsx index bfc5851ade6191..38ab5c66acbae0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/empty_thumbnail.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/empty_thumbnail.tsx @@ -15,7 +15,7 @@ export const THUMBNAIL_HEIGHT = 64; export const thumbnailStyle = css` padding: 0; - margin: 0; + margin: auto; width: ${THUMBNAIL_WIDTH}px; height: ${THUMBNAIL_HEIGHT}px; object-fit: contain; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_image_popover.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_image_popover.tsx index 48ff7237223fb4..dda8c5343eb085 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_image_popover.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_image_popover.tsx @@ -9,10 +9,13 @@ import React from 'react'; import { css } from '@emotion/react'; import { EuiImage, EuiPopover, useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useRouteMatch } from 'react-router-dom'; +import { EmptyImage } from '../screenshot/empty_image'; import { ScreenshotRefImageData } from '../../../../../../common/runtime_types'; import { useCompositeImage } from '../../../hooks/use_composite_image'; import { EmptyThumbnail, thumbnailStyle } from './empty_thumbnail'; +import { STEP_DETAIL_ROUTE } from '../../../../../../common/constants'; const POPOVER_IMG_HEIGHT = 360; const POPOVER_IMG_WIDTH = 640; @@ -22,6 +25,7 @@ interface ScreenshotImageProps { imageCaption: JSX.Element; isStepFailed: boolean; isLoading: boolean; + asThumbnail?: boolean; } const ScreenshotThumbnail: React.FC = ({ @@ -30,6 +34,7 @@ const ScreenshotThumbnail: React.FC { return imageData ? ( - ) : ( + ) : asThumbnail ? ( + ) : ( + ); }; /** @@ -64,6 +71,7 @@ const RecomposedScreenshotImage: React.FC< setImageData, isStepFailed, isLoading, + asThumbnail, }) => { // initially an undefined URL value is passed to the image display, and a loading spinner is rendered. // `useCompositeImage` will call `setImageData` when the image is composited, and the updated `imageData` will display. @@ -76,6 +84,7 @@ const RecomposedScreenshotImage: React.FC< imageData={imageData} isStepFailed={isStepFailed} isLoading={isLoading} + asThumbnail={asThumbnail} /> ); }; @@ -88,6 +97,7 @@ export interface StepImagePopoverProps { isImagePopoverOpen: boolean; isStepFailed: boolean; isLoading: boolean; + asThumbnail?: boolean; } const JourneyStepImage: React.FC< @@ -104,6 +114,7 @@ const JourneyStepImage: React.FC< setImageData, isStepFailed, isLoading, + asThumbnail = true, }) => { if (imgSrc) { return ( @@ -113,6 +124,7 @@ const JourneyStepImage: React.FC< imageData={imageData} isStepFailed={isStepFailed} isLoading={isLoading} + asThumbnail={asThumbnail} /> ); } else if (imgRef) { @@ -125,6 +137,7 @@ const JourneyStepImage: React.FC< setImageData={setImageData} isStepFailed={isStepFailed} isLoading={isLoading} + asThumbnail={asThumbnail} /> ); } @@ -139,6 +152,7 @@ export const JourneyStepImagePopover: React.FC = ({ isImagePopoverOpen, isStepFailed, isLoading, + asThumbnail = true, }) => { const { euiTheme } = useEuiTheme(); @@ -158,12 +172,16 @@ export const JourneyStepImagePopover: React.FC = ({ const isImageLoading = isLoading || (!!imgRef && !imageData); + const isStepDetailPage = useRouteMatch(STEP_DETAIL_ROUTE)?.isExact; + + const thumbnailS = isStepDetailPage ? null : thumbnailStyle; + return ( = ({ imageData={imageData} isStepFailed={isStepFailed} isLoading={isImageLoading} + asThumbnail={asThumbnail} /> } isOpen={isImagePopoverOpen} @@ -195,12 +214,10 @@ export const JourneyStepImagePopover: React.FC = ({ object-fit: contain; `} /> + ) : asThumbnail ? ( + ) : ( - + )} ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_with_label.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_with_label.tsx index 12dcd4db95f00b..f153aa07f2f4f2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_with_label.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_with_label.tsx @@ -8,7 +8,7 @@ import React, { CSSProperties } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; import { JourneyStep } from '../../../../../../common/runtime_types'; -import { JourneyStepScreenshotContainer } from './journey_step_screenshot_container'; +import { JourneyStepScreenshotContainer } from '../screenshot/journey_step_screenshot_container'; import { getTextColorForMonitorStatus, parseBadgeStatus } from './status_badge'; interface Props { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/empty_image.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/empty_image.tsx new file mode 100644 index 00000000000000..57295b3e1daf23 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/empty_image.tsx @@ -0,0 +1,102 @@ +/* + * 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 { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import { + useEuiTheme, + useEuiBackgroundColor, + EuiIcon, + EuiLoadingContent, + EuiText, +} from '@elastic/eui'; + +export const IMAGE_WIDTH = 360; +export const IMAGE_HEIGHT = 203; + +export const imageStyle = css` + padding: 0; + margin: auto; + width: ${IMAGE_WIDTH}px; + height: ${IMAGE_HEIGHT}px; + object-fit: contain; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +`; + +export const EmptyImage = ({ + isLoading = false, + width = IMAGE_WIDTH, + height = IMAGE_HEIGHT, +}: { + isLoading: boolean; + width?: number; + height?: number; +}) => { + const { euiTheme } = useEuiTheme(); + + return ( +
+ {isLoading ? ( + + ) : ( +
+ + {IMAGE_UN_AVAILABLE} +
+ )} +
+ ); +}; + +export const SCREENSHOT_LOADING_ARIA_LABEL = i18n.translate( + 'xpack.synthetics.monitor.step.screenshot.ariaLabel', + { + defaultMessage: 'Step screenshot is being loaded.', + } +); + +export const SCREENSHOT_NOT_AVAILABLE = i18n.translate( + 'xpack.synthetics.monitor.step.screenshot.notAvailable', + { + defaultMessage: 'Step screenshot is not available.', + } +); + +export const IMAGE_UN_AVAILABLE = i18n.translate( + 'xpack.synthetics.monitor.step.screenshot.unAvailable', + { + defaultMessage: 'Image unavailable', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot.tsx new file mode 100644 index 00000000000000..9199c3f7db7e8e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot.tsx @@ -0,0 +1,39 @@ +/* + * 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, { useMemo } from 'react'; +import { useJourneySteps } from '../../monitor_details/hooks/use_journey_steps'; +import { parseBadgeStatus } from '../monitor_test_result/status_badge'; +import { JourneyStepScreenshotContainer } from './journey_step_screenshot_container'; + +export const JourneyScreenshot = ({ checkGroupId }: { checkGroupId: string }) => { + const { loading: stepsLoading, stepEnds } = useJourneySteps(checkGroupId); + const stepLabels = stepEnds.map((stepEnd) => stepEnd?.synthetics?.step?.name ?? ''); + + const lastSignificantStep = useMemo(() => { + const copy = [...stepEnds]; + // Sort desc by timestamp + copy.sort( + (stepA, stepB) => + Number(new Date(stepB['@timestamp'])) - Number(new Date(stepA['@timestamp'])) + ); + return copy.find( + (stepEnd) => parseBadgeStatus(stepEnd?.synthetics?.step?.status ?? 'skipped') !== 'skipped' + ); + }, [stepEnds]); + + return ( + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_container.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx similarity index 98% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_container.test.tsx rename to x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx index 4c95fade23d1a9..61219b57ae86d1 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_container.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx @@ -14,7 +14,7 @@ import * as observabilityPublic from '@kbn/observability-plugin/public'; import { getShortTimeStamp } from '../../../utils/monitor_test_result/timestamp'; import '../../../utils/testing/__mocks__/use_composite_image.mock'; import { mockRef } from '../../../utils/testing/__mocks__/screenshot_ref.mock'; -import * as retrieveHooks from './use_retrieve_step_image'; +import * as retrieveHooks from '../monitor_test_result/use_retrieve_step_image'; jest.mock('@kbn/observability-plugin/public'); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_container.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.tsx similarity index 89% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_container.tsx rename to x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.tsx index 6c2653f7efaa6b..24045d5ac458a7 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_container.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.tsx @@ -10,6 +10,7 @@ import { css } from '@emotion/react'; import useIntersection from 'react-use/lib/useIntersection'; import { i18n } from '@kbn/i18n'; +import { EmptyImage } from './empty_image'; import { isScreenshotImageBlob, isScreenshotRef, @@ -18,10 +19,10 @@ import { import { SyntheticsSettingsContext } from '../../../contexts'; -import { useRetrieveStepImage } from './use_retrieve_step_image'; -import { ScreenshotOverlayFooter } from './screenshot_overlay_footer'; -import { JourneyStepImagePopover } from './journey_step_image_popover'; -import { EmptyThumbnail } from './empty_thumbnail'; +import { useRetrieveStepImage } from '../monitor_test_result/use_retrieve_step_image'; +import { ScreenshotOverlayFooter } from '../monitor_test_result/screenshot_overlay_footer'; +import { JourneyStepImagePopover } from '../monitor_test_result/journey_step_image_popover'; +import { EmptyThumbnail } from '../monitor_test_result/empty_thumbnail'; interface Props { checkGroup?: string; @@ -29,6 +30,7 @@ interface Props { stepStatus?: string; initialStepNo?: number; allStepsLoaded?: boolean; + asThumbnail?: boolean; retryFetchOnRevisit?: boolean; // Set to `true` fro "Run Once" / "Test Now" modes } @@ -39,6 +41,7 @@ export const JourneyStepScreenshotContainer = ({ allStepsLoaded, initialStepNo = 1, retryFetchOnRevisit = false, + asThumbnail = true, }: Props) => { const [stepNumber, setStepNumber] = useState(initialStepNo); const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false); @@ -135,9 +138,12 @@ export const JourneyStepScreenshotContainer = ({ isImagePopoverOpen={isImagePopoverOpen} isStepFailed={stepStatus === 'failed'} isLoading={Boolean(loading)} + asThumbnail={asThumbnail} /> - ) : ( + ) : asThumbnail ? ( + ) : ( + )} ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx index 950d439173004c..10de9f5a4c629f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { useHistory } from 'react-router-dom'; -import { EuiButtonEmpty } from '@elastic/eui'; +import { EuiLink, EuiIcon } from '@elastic/eui'; import { InPortal } from 'react-reverse-portal'; import { MonitorDetailsLinkPortalNode } from './portals'; @@ -25,8 +25,8 @@ export const MonitorDetailsLink = ({ name, id }: { name: string; id: string }) = pathname: `monitor/${id}`, }); return ( - - {name} - + + {name} + ); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx index a6d2070d0e96a9..13f3d245947078 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx @@ -6,10 +6,14 @@ */ import { useFetcher } from '@kbn/observability-plugin/public'; -import { SyntheticsJourneyApiResponse } from '../../../../../../common/runtime_types'; +import { useParams } from 'react-router-dom'; +import { isStepEnd } from '../../common/monitor_test_result/browser_steps_list'; +import { JourneyStep, SyntheticsJourneyApiResponse } from '../../../../../../common/runtime_types'; import { fetchJourneySteps } from '../../../state'; export const useJourneySteps = (checkGroup: string | undefined) => { + const { stepIndex } = useParams<{ stepIndex: string }>(); + const { data, loading } = useFetcher(() => { if (!checkGroup) { return Promise.resolve(null); @@ -18,5 +22,24 @@ export const useJourneySteps = (checkGroup: string | undefined) => { return fetchJourneySteps({ checkGroup }); }, [checkGroup]); - return { data: data as SyntheticsJourneyApiResponse, loading: loading ?? false }; + const isFailed = + data?.steps.some( + (step) => + step.synthetics?.step?.status === 'failed' || step.synthetics?.step?.status === 'skipped' + ) ?? false; + + const stepEnds: JourneyStep[] = (data?.steps ?? []).filter(isStepEnd); + + const stepLabels = stepEnds.map((stepEnd) => stepEnd?.synthetics?.step?.name ?? ''); + + return { + data: data as SyntheticsJourneyApiResponse, + loading: loading ?? false, + isFailed, + stepEnds, + stepLabels, + currentStep: stepIndex + ? data?.steps.find((step) => step.synthetics?.step?.index === Number(stepIndex)) + : undefined, + }; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx index 00ef508ed0d2c8..891d1694de4bce 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/test_runs_table.tsx @@ -23,7 +23,7 @@ import { import { Criteria } from '@elastic/eui/src/components/basic_table/basic_table'; import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; -import { ConfigKey, DataStream, JourneyStep, Ping } from '../../../../../../common/runtime_types'; +import { ConfigKey, DataStream, Ping } from '../../../../../../common/runtime_types'; import { formatTestDuration, formatTestRunAt, @@ -33,13 +33,11 @@ import { useSyntheticsSettingsContext } from '../../../contexts/synthetics_setti import { sortPings } from '../../../utils/monitor_test_result/sort_pings'; import { selectPingsError } from '../../../state'; import { parseBadgeStatus, StatusBadge } from '../../common/monitor_test_result/status_badge'; -import { isStepEnd } from '../../common/monitor_test_result/browser_steps_list'; -import { JourneyStepScreenshotContainer } from '../../common/monitor_test_result/journey_step_screenshot_container'; import { useKibanaDateFormat } from '../../../../../hooks/use_kibana_date_format'; import { useSelectedMonitor } from '../hooks/use_selected_monitor'; import { useMonitorPings } from '../hooks/use_monitor_pings'; -import { useJourneySteps } from '../hooks/use_journey_steps'; +import { JourneyScreenshot } from '../../common/screenshot/journey_screenshot'; type SortableField = 'timestamp' | 'monitor.status' | 'monitor.duration.us'; @@ -98,7 +96,9 @@ export const TestRunsTable = ({ paginable = true, from, to }: TestRunsTableProps align: 'left', field: 'timestamp', name: SCREENSHOT_LABEL, - render: (_timestamp: string, item) => , + render: (_timestamp: string, item) => ( + + ), }, ] : []) as Array>), @@ -197,35 +197,6 @@ export const TestRunsTable = ({ paginable = true, from, to }: TestRunsTableProps ); }; -const JourneyScreenshot = ({ ping }: { ping: Ping }) => { - const { data: stepsData, loading: stepsLoading } = useJourneySteps(ping?.monitor?.check_group); - const stepEnds: JourneyStep[] = (stepsData?.steps ?? []).filter(isStepEnd); - const stepLabels = stepEnds.map((stepEnd) => stepEnd?.synthetics?.step?.name ?? ''); - - const lastSignificantStep = useMemo(() => { - const copy = [...stepEnds]; - // Sort desc by timestamp - copy.sort( - (stepA, stepB) => - Number(new Date(stepB['@timestamp'])) - Number(new Date(stepA['@timestamp'])) - ); - return copy.find( - (stepEnd) => parseBadgeStatus(stepEnd?.synthetics?.step?.status ?? 'skipped') !== 'skipped' - ); - }, [stepEnds]); - - return ( - - ); -}; - const TestDetailsLink = ({ isBrowserMonitor, timestamp, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/components/screenshot/last_successful_screenshot.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/components/screenshot/last_successful_screenshot.tsx new file mode 100644 index 00000000000000..3874d29197d3cd --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/components/screenshot/last_successful_screenshot.tsx @@ -0,0 +1,47 @@ +/* + * 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 { useFetcher } from '@kbn/observability-plugin/public'; +import { EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { fetchLastSuccessfulCheck } from '../../../../state'; +import { JourneyStep } from '../../../../../../../common/runtime_types'; +import { EmptyImage } from '../../../common/screenshot/empty_image'; +import { JourneyStepScreenshotContainer } from '../../../common/screenshot/journey_step_screenshot_container'; + +export const LastSuccessfulScreenshot = ({ step }: { step: JourneyStep }) => { + const { stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); + + const { data, loading } = useFetcher(() => { + return fetchLastSuccessfulCheck({ + timestamp: step['@timestamp'], + monitorId: step.monitor.id, + stepIndex: Number(stepIndex), + location: step.observer?.geo?.name, + }); + }, [step._id, step['@timestamp']]); + + if (loading || !data) { + return ; + } + + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/components/step_image.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/components/step_image.tsx new file mode 100644 index 00000000000000..a08ba79444ccb0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/components/step_image.tsx @@ -0,0 +1,90 @@ +/* + * 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, { useState } from 'react'; +import { EuiButtonGroup, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { LastSuccessfulScreenshot } from './screenshot/last_successful_screenshot'; +import { JourneyStep } from '../../../../../../common/runtime_types'; +import { JourneyStepScreenshotContainer } from '../../common/screenshot/journey_step_screenshot_container'; + +export const StepImage = ({ + step, + ping, + isFailed, + stepLabels, +}: { + ping: JourneyStep; + step: JourneyStep; + isFailed?: boolean; + stepLabels?: string[]; +}) => { + const toggleButtons = [ + { + id: `received`, + label: RECEIVED_LABEL, + }, + { + id: `expected`, + label: EXPECTED_LABEL, + }, + ]; + + const [idSelected, setIdSelected] = useState(`received`); + + const onChangeDisabled = (optionId: string) => { + setIdSelected(optionId); + }; + + return ( + <> + +

{SCREENSHOT_LABEL}

+
+ +
+ {idSelected === 'received' ? ( + + ) : ( + + )} + + + {isFailed && ( + onChangeDisabled(id)} + buttonSize="s" + isFullWidth + /> + )} +
+ + ); +}; + +const SCREENSHOT_LABEL = i18n.translate('xpack.synthetics.stepDetails.screenshot', { + defaultMessage: 'Screenshot', +}); + +const EXPECTED_LABEL = i18n.translate('xpack.synthetics.stepDetails.expected', { + defaultMessage: 'Expected', +}); + +const RECEIVED_LABEL = i18n.translate('xpack.synthetics.stepDetails.received', { + defaultMessage: 'Received', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_detail_page.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_detail_page.ts new file mode 100644 index 00000000000000..4c79502ce665f3 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_detail_page.ts @@ -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 { useParams } from 'react-router-dom'; +import { useMemo } from 'react'; +import { useSyntheticsSettingsContext } from '../../../contexts'; +import { useJourneySteps } from '../../monitor_details/hooks/use_journey_steps'; +import { JourneyStep, SyntheticsJourneyApiResponse } from '../../../../../../common/runtime_types'; + +export const useStepDetailPage = (): { + activeStep?: JourneyStep; + checkGroupId: string; + handleNextStepHref: string; + handlePreviousStepHref: string; + handleNextRunHref: string; + handlePreviousRunHref: string; + hasNextStep: boolean; + hasPreviousStep: boolean; + journey?: SyntheticsJourneyApiResponse; + stepIndex: number; +} => { + const { checkGroupId, stepIndex: stepIndexString } = useParams<{ + checkGroupId: string; + stepIndex: string; + }>(); + + const stepIndex = Number(stepIndexString); + + const { data: journey } = useJourneySteps(checkGroupId); + + const memoized = useMemo( + () => ({ + hasPreviousStep: stepIndex > 1 ? true : false, + activeStep: journey?.steps?.find((step) => step.synthetics?.step?.index === stepIndex), + hasNextStep: journey && journey.steps && stepIndex < journey.steps.length ? true : false, + }), + [journey, stepIndex] + ); + + const { basePath } = useSyntheticsSettingsContext(); + + const handleNextStepHref = `${basePath}/app/synthetics/journey/${checkGroupId}/step/${ + stepIndex + 1 + }`; + + const handlePreviousStepHref = `${basePath}/app/synthetics/journey/${checkGroupId}/step/${ + stepIndex - 1 + }`; + + const handleNextRunHref = `${basePath}/app/synthetics/journey/${journey?.details?.next?.checkGroup}/step/1`; + + const handlePreviousRunHref = `${basePath}/app/synthetics/journey/${journey?.details?.previous?.checkGroup}/step/1`; + + return { + checkGroupId, + journey, + stepIndex, + ...memoized, + handleNextStepHref, + handlePreviousStepHref, + handleNextRunHref, + handlePreviousRunHref, + }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_details_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_details_breadcrumbs.ts new file mode 100644 index 00000000000000..b42417083cc3a4 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_step_details_breadcrumbs.ts @@ -0,0 +1,28 @@ +/* + * 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'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; +import { MONITORS_ROUTE } from '../../../../../../common/constants'; +import { PLUGIN } from '../../../../../../common/constants/plugin'; + +export const useStepDetailsBreadcrumbs = (extraCrumbs?: Array<{ text: string; href?: string }>) => { + const kibana = useKibana(); + const appPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; + + useBreadcrumbs([ + { + text: MONITOR_MANAGEMENT_CRUMB, + href: `${appPath}/${MONITORS_ROUTE}`, + }, + ...(extraCrumbs ?? []), + ]); +}; + +const MONITOR_MANAGEMENT_CRUMB = i18n.translate('xpack.synthetics.monitorsPage.monitorsMCrumb', { + defaultMessage: 'Monitors', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_detail_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_detail_page.tsx new file mode 100644 index 00000000000000..09122158e45ed0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_detail_page.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 React from 'react'; +import { useParams } from 'react-router-dom'; +import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { StepImage } from './components/step_image'; +import { useJourneySteps } from '../monitor_details/hooks/use_journey_steps'; +import { MonitorDetailsLinkPortal } from '../monitor_add_edit/monitor_details_portal'; +import { useStepDetailsBreadcrumbs } from './hooks/use_step_details_breadcrumbs'; + +export const StepDetailPage = () => { + const { checkGroupId } = useParams<{ checkGroupId: string; stepIndex: string }>(); + + useTrackPageview({ app: 'synthetics', path: 'stepDetail' }); + useTrackPageview({ app: 'synthetics', path: 'stepDetail', delay: 15000 }); + + const { data, loading, isFailed, currentStep, stepLabels } = useJourneySteps(checkGroupId); + + useStepDetailsBreadcrumbs([{ text: data?.details?.journey.monitor.name ?? '' }]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( + <> + {data?.details?.journey && ( + + )} + + + + {data?.details?.journey && currentStep && ( + + )} + + + + + + + {/* TODO: Add breakdown of network timings donut*/} + + + {/* TODO: Add breakdown of network events*/} + + + + + + + + {/* TODO: Add step metrics*/} + + + + + + {/* TODO: Add breakdown of object list*/} + + {/* TODO: Add breakdown of object weight*/} + + + + + + {/* TODO: Add breakdown of network events*/} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_title.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_title.tsx new file mode 100644 index 00000000000000..5cb758e4b3d370 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_title.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useJourneySteps } from '../monitor_details/hooks/use_journey_steps'; + +export const StepTitle = () => { + const { checkGroupId, stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); + + const { data } = useJourneySteps(checkGroupId); + + const currentStep = data?.steps.find((step) => step.synthetics.step?.index === Number(stepIndex)); + + return ( + + {currentStep?.synthetics?.step?.name} + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_theme_context.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_theme_context.tsx index 32844de9d04c2a..1415b1076cdd9d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_theme_context.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_theme_context.tsx @@ -6,7 +6,7 @@ */ import { euiLightVars, euiDarkVars } from '@kbn/ui-theme'; -import React, { createContext, useMemo } from 'react'; +import React, { createContext, useContext, useMemo } from 'react'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { DARK_THEME, LIGHT_THEME, PartialTheme, Theme } from '@elastic/charts'; @@ -96,3 +96,5 @@ export const SyntheticsThemeContextProvider: React.FC = ({ return ; }; + +export const useSyntheticsThemeContext = () => useContext(SyntheticsThemeContext); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 6319610f8e8c79..06b684dd307195 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -24,6 +24,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-plugin/public'; import { ErrorDetailsPage } from './components/error_details/error_details_page'; +import { StepTitle } from './components/step_details_page/step_title'; import { MonitorAddPage } from './components/monitor_add_edit/monitor_add_page'; import { MonitorEditPage } from './components/monitor_add_edit/monitor_edit_page'; import { MonitorDetailsPageTitle } from './components/monitor_details/monitor_details_page_title'; @@ -46,6 +47,7 @@ import { MONITOR_ERRORS_ROUTE, MONITOR_HISTORY_ROUTE, MONITOR_ROUTE, + STEP_DETAIL_ROUTE, ERROR_DETAILS_ROUTE, OVERVIEW_ROUTE, } from '../../../common/constants'; @@ -59,6 +61,7 @@ import { MonitorDetailsLastRun } from './components/monitor_details/monitor_deta import { MonitorSummary } from './components/monitor_details/monitor_summary/monitor_summary'; import { MonitorHistory } from './components/monitor_details/monitor_history/monitor_history'; import { MonitorErrors } from './components/monitor_details/monitor_errors/monitor_errors'; +import { StepDetailPage } from './components/step_details_page/step_detail_page'; type RouteProps = LazyObservabilityPageTemplateProps & { path: string; @@ -285,6 +288,24 @@ const getRoutes = ( ], }, }, + { + title: i18n.translate('xpack.synthetics.stepDetailsRoute.title', { + defaultMessage: 'Step details | {baseTitle}', + values: { baseTitle }, + }), + path: STEP_DETAIL_ROUTE, + component: () => , + dataTestSubj: 'syntheticsMonitorEditPage', + pageHeader: { + pageTitle: , + rightSideItems: [], + breadcrumbs: [ + { + text: , + }, + ], + }, + }, { title: i18n.translate('xpack.synthetics.errorDetailsRoute.title', { defaultMessage: 'Error details | {baseTitle}', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx index 55ae549a032b34..3168c1b07ee337 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx @@ -95,6 +95,7 @@ const createMockStore = () => { const mockAppUrls: Record = { uptime: '/app/uptime', + synthetics: '/app/synthetics', observability: '/app/observability', '/home#/tutorial/uptimeMonitors': '/home#/tutorial/uptimeMonitors', }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_title.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_title.test.tsx index f9e3572ead5111..e4594e8c60630e 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_title.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_title.test.tsx @@ -42,6 +42,7 @@ describe('MonitorTitle component', () => { id: defaultMonitorId, status: 'up', type: 'http', + check_group: 'test-group', }, url: { full: 'https://www.elastic.co/', @@ -58,6 +59,7 @@ describe('MonitorTitle component', () => { id: 'browser', status: 'up', type: 'browser', + check_group: 'test-group', }, url: { full: 'https://www.elastic.co/', diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/ping_list.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/ping_list.test.tsx index ddb33e4dd5feaf..f0e60b29028282 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/ping_list.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/ping_list.test.tsx @@ -32,6 +32,7 @@ describe('PingList component', () => { name: '', status: 'down', type: 'tcp', + check_group: 'test-group', }, }, { @@ -47,6 +48,7 @@ describe('PingList component', () => { name: '', status: 'down', type: 'tcp', + check_group: 'test-group', }, }, ]; @@ -120,6 +122,7 @@ describe('PingList component', () => { "type": "io", }, "monitor": Object { + "check_group": "test-group", "duration": Object { "us": 1430, }, @@ -160,6 +163,7 @@ describe('PingList component', () => { "type": "io", }, "monitor": Object { + "check_group": "test-group", "id": "auto-tcp-0X81440A68E839814D", "ip": "255.255.255.0", "name": "", diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/status_details/monitor_status.bar.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/status_details/monitor_status.bar.test.tsx index 640d207fbb1384..af5df91160026a 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/status_details/monitor_status.bar.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/status_details/monitor_status.bar.test.tsx @@ -28,6 +28,7 @@ describe('MonitorStatusBar component', () => { id: 'id1', status: 'up', type: 'http', + check_group: 'test-group', }, url: { full: 'https://www.example.com/', diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/columns/monitor_status_column.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/columns/monitor_status_column.test.tsx index a69ebb3d349fdf..7869125014b02a 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/columns/monitor_status_column.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/columns/monitor_status_column.test.tsx @@ -37,6 +37,7 @@ describe('MonitorListStatusColumn', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: { @@ -58,6 +59,7 @@ describe('MonitorListStatusColumn', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: { @@ -79,6 +81,7 @@ describe('MonitorListStatusColumn', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: { @@ -103,6 +106,7 @@ describe('MonitorListStatusColumn', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: { @@ -124,6 +128,7 @@ describe('MonitorListStatusColumn', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: { @@ -145,6 +150,7 @@ describe('MonitorListStatusColumn', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: { @@ -169,6 +175,7 @@ describe('MonitorListStatusColumn', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: { @@ -190,6 +197,7 @@ describe('MonitorListStatusColumn', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: { @@ -211,6 +219,7 @@ describe('MonitorListStatusColumn', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/__snapshots__/monitor_list_drawer.test.tsx.snap index a07a55df6dbfaf..0c037492bbc6be 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/__snapshots__/monitor_list_drawer.test.tsx.snap +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/__snapshots__/monitor_list_drawer.test.tsx.snap @@ -111,6 +111,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are Object { "docId": "foo", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 121, }, @@ -125,6 +126,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are Object { "docId": "foo-0", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 100000, }, @@ -139,6 +141,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are Object { "docId": "foo-1", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 1, }, @@ -153,6 +156,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are Object { "docId": "foo-2", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 2, }, @@ -289,6 +293,7 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there is o Object { "docId": "foo", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 121, }, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.test.tsx index f1a9d1b2629a6b..7318fc5188af82 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.test.tsx @@ -29,6 +29,7 @@ describe('MonitorStatusList component', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: {}, @@ -44,6 +45,7 @@ describe('MonitorStatusList component', () => { id: 'myMonitor', type: 'icmp', duration: { us: 123 }, + check_group: 'test-group', }, observer: { geo: {}, @@ -59,6 +61,7 @@ describe('MonitorStatusList component', () => { id: 'myUpMonitor', type: 'icmp', duration: { us: 234 }, + check_group: 'test-group', }, observer: { geo: { @@ -165,6 +168,7 @@ describe('MonitorStatusList component', () => { id: 'myMonitor', type: 'icmp', duration: { us: 234 }, + check_group: 'test-group', }, observer: { geo: { @@ -182,6 +186,7 @@ describe('MonitorStatusList component', () => { id: 'myMonitor', type: 'icmp', duration: { us: 234 }, + check_group: 'test-group', }, observer: { geo: { @@ -199,6 +204,7 @@ describe('MonitorStatusList component', () => { id: 'myMonitor', type: 'icmp', duration: { us: 234 }, + check_group: 'test-group', }, observer: { geo: { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/state/reducers/monitor_status.test.ts b/x-pack/plugins/synthetics/public/legacy_uptime/state/reducers/monitor_status.test.ts index f6c637e5fdb1b2..c7675d96077723 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/state/reducers/monitor_status.test.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/state/reducers/monitor_status.test.ts @@ -25,6 +25,7 @@ describe('selectedFiltersReducer', () => { us: 1, }, type: 'browser', + check_group: 'test-group', }, }, }; @@ -42,6 +43,7 @@ describe('selectedFiltersReducer', () => { us: 1, }, type: 'browser', + check_group: 'test-group', }, }; expect( diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_monitor_availability.test.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_monitor_availability.test.ts index d84c4025d5dd07..7069cfce177409 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_monitor_availability.test.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_monitor_availability.test.ts @@ -411,6 +411,7 @@ describe('monitor availability', () => { "monitorInfo": Object { "docId": "myDocId", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 100000, }, @@ -432,6 +433,7 @@ describe('monitor availability', () => { "monitorInfo": Object { "docId": "myDocId", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 100000, }, @@ -453,6 +455,7 @@ describe('monitor availability', () => { "monitorInfo": Object { "docId": "myDocId", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 100000, }, @@ -542,6 +545,7 @@ describe('monitor availability', () => { "monitorInfo": Object { "docId": "myDocId", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 100000, }, @@ -563,6 +567,7 @@ describe('monitor availability', () => { "monitorInfo": Object { "docId": "myDocId", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 100000, }, @@ -584,6 +589,7 @@ describe('monitor availability', () => { "monitorInfo": Object { "docId": "myDocId", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 100000, }, @@ -605,6 +611,7 @@ describe('monitor availability', () => { "monitorInfo": Object { "docId": "myDocId", "monitor": Object { + "check_group": "myCheckGroup", "duration": Object { "us": 100000, }, diff --git a/yarn.lock b/yarn.lock index 2b750cbf1980e8..5ce08cea40dffa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3116,6 +3116,18 @@ version "0.0.0" uid "" +"@kbn/core-logging-browser-internal@link:bazel-bin/packages/core/logging/core-logging-browser-internal": + version "0.0.0" + uid "" + +"@kbn/core-logging-browser-mocks@link:bazel-bin/packages/core/logging/core-logging-browser-mocks": + version "0.0.0" + uid "" + +"@kbn/core-logging-common-internal@link:bazel-bin/packages/core/logging/core-logging-common-internal": + version "0.0.0" + uid "" + "@kbn/core-logging-server-internal@link:bazel-bin/packages/core/logging/core-logging-server-internal": version "0.0.0" uid "" @@ -19366,10 +19378,10 @@ moment-timezone@*, moment-timezone@^0.5.34: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== -monaco-editor@*, monaco-editor@^0.22.3: - version "0.22.3" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.22.3.tgz#69b42451d3116c6c08d9b8e052007ff891fd85d7" - integrity sha512-RM559z2CJbczZ3k2b+ouacMINkAYWwRit4/vs0g2X/lkYefDiu0k2GmgWjAuiIpQi+AqASPOKvXNmYc8KUSvVQ== +monaco-editor@*, monaco-editor@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.24.0.tgz#990b55096bcc95d08d8d28e55264c6eb17707269" + integrity sha512-o1f0Lz6ABFNTtnEqqqvlY9qzNx24rQZx1RgYNQ8SkWkE+Ka63keHH/RqxQ4QhN4fs/UYOnvAtEUZsPrzccH++A== monitor-event-loop-delay@^1.0.0: version "1.0.0"