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 eef3f24151e165..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", 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/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/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/yarn.lock b/yarn.lock index c86e92cd6a0205..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 ""