diff --git a/package-lock.json b/package-lock.json index 9fc693f73dabd..aeecba7a4e02b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "kibana", - "version": "6.0.0-alpha3", + "version": "6.0.0-beta1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -125,11 +125,6 @@ "integrity": "sha1-JZrqlH1quqovUEMWliMd7ypSKSY=", "dev": true }, - "@elastic/httpolyglot": { - "version": "0.1.2-elasticpatch1", - "resolved": "https://registry.npmjs.org/@elastic/httpolyglot/-/httpolyglot-0.1.2-elasticpatch1.tgz", - "integrity": "sha1-gSKFp7EA/2ETzICA3SJomEvMSIM=" - }, "@elastic/webpack-directory-name-as-main": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@elastic/webpack-directory-name-as-main/-/webpack-directory-name-as-main-2.0.2.tgz", @@ -10215,9 +10210,9 @@ } }, "makelogs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/makelogs/-/makelogs-4.0.1.tgz", - "integrity": "sha1-3JiZuT1SNTp12OMs5wtDEtiEl3E=", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/makelogs/-/makelogs-4.0.2.tgz", + "integrity": "sha1-T7vhOY/H8LvEiXoeUTlqDBkA2xo=", "dev": true, "requires": { "async": "1.5.2", @@ -10250,6 +10245,19 @@ "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=", "dev": true }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, "cli-width": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-1.1.1.tgz", @@ -10286,19 +10294,6 @@ "through": "2.3.8" }, "dependencies": { - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, "lodash": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", diff --git a/platform/logging/README.md b/platform/logging/README.md index 746e7abe638cb..65fe64b045801 100644 --- a/platform/logging/README.md +++ b/platform/logging/README.md @@ -43,12 +43,30 @@ the log record is ignored. The _all_ and _off_ levels can be used only in configuration and are just handy shortcuts that allow developer to log every log record or disable logging entirely for the specific context. +## Layouts + +Every appender should know exactly how to format log messages before they are written to the console or file on the disk. +This behaviour is controlled by the layouts and configured through `appender.layout` configuration property for every +custom appender (see examples in [Configuration](#configuration)). Currently we don't define any default layout for the +custom appenders, so one should always make the choice explicitly. + +There are two types of layout supported at the moment: `pattern` and `json`. + +With `pattern` layout it's possible to define a string pattern with special placeholders wrapped into curly braces that +will be replaced with data from the actual log message. By default the following pattern is used: +`[{timestamp}][{level}][{context}] {message}`. Also `highlight` option can be enabled for `pattern` layout so that +some parts of the log message are highlighted with different colors that may be quite handy if log messages are forwarded +to the terminal with color support. + +With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context, message +text and any other metadata that may be associated with the log message itself. + ## Configuration As any configuration in the platform, logging configuration is validated against the predefined schema and if there are any issues with it, Kibana will fail to start with the detailed error message. -Once your code acquired a logger instance it should not care about any runtime changes in the configuration that may +Once the code acquired a logger instance it should not care about any runtime changes in the configuration that may happen: all changes will be applied to existing logger instances under the hood. Here is the configuration example that can be used to configure _loggers_, _appenders_ and _layouts_: @@ -63,7 +81,7 @@ logging: highlight: true file: kind: file - path: ~/Downloads/kibana.log + path: /var/log/kibana.log layout: kind: pattern custom: @@ -71,6 +89,9 @@ logging: layout: kind: pattern pattern: [{timestamp}][{level}] {message} + json-file-appender: + kind: file + path: /var/log/kibana-json.log root: appenders: [console, file] @@ -86,21 +107,26 @@ logging: level: fatal - context: optimize appenders: [console] + - context: telemetry + level: all + appenders: [json-file-appender] ``` -Here is what you get with the config above: +Here is what we get with the config above: -| Context | Appenders | Level | -| ------------- |:-------------:| -----:| -| root | console, file | error | -| plugins | custom | warn | -| plugins.pid | custom | info | -| server | console, file | fatal | -| optimize | console | error | +| Context | Appenders | Level | +| ------------- |:------------------------:| -----:| +| root | console, file | error | +| plugins | custom | warn | +| plugins.pid | custom | info | +| server | console, file | fatal | +| optimize | console | error | +| telemetry | json-file-appender | all | -As you see `root` logger has a dedicated configuration node since this context is special and should always exist. By + +The `root` logger has a dedicated configuration node since this context is special and should always exist. By default `root` is configured with `info` level and `default` appender that is also always available. This is the -configuration that all your loggers will use unless you re-configure them explicitly. +configuration that all custom loggers will use unless they're re-configured explicitly. For example to see _all_ log messages that fall back on the `root` logger configuration, just add one line to the configuration: @@ -134,7 +160,7 @@ loggerWithNestedContext.trace('Message with `trace` log level.'); loggerWithNestedContext.debug('Message with `debug` log level.'); ``` -And assuming you're using `console` appender and `trace` level for `server` context, in console you'll see: +And assuming logger for `server` context with `console` appender and `trace` level was used, console output will look like this: ```bash [2017-07-25T18:54:41.639Z][TRACE][server] Message with `trace` log level. [2017-07-25T18:54:41.639Z][DEBUG][server] Message with `debug` log level. @@ -147,10 +173,9 @@ And assuming you're using `console` appender and `trace` level for `server` cont [2017-07-25T18:54:41.639Z][DEBUG][server.http] Message with `debug` log level. ``` -Obviously your log will be less verbose with `warn` level for the `server` context: +The log will be less verbose with `warn` level for the `server` context: ```bash [2017-07-25T18:54:41.639Z][WARN ][server] Message with `warn` log level. [2017-07-25T18:54:41.639Z][ERROR][server] Message with `error` log level. [2017-07-25T18:54:41.639Z][FATAL][server] Message with `fatal` log level. ``` - diff --git a/platform/logging/layouts/JsonLayout.ts b/platform/logging/layouts/JsonLayout.ts new file mode 100644 index 0000000000000..663adf56746ff --- /dev/null +++ b/platform/logging/layouts/JsonLayout.ts @@ -0,0 +1,32 @@ +import { Schema, typeOfSchema } from '../../types'; +import { LogRecord } from '../LogRecord'; +import { Layout } from './Layouts'; + +const createSchema = ({ literal, object }: Schema) => { + return object({ + kind: literal('json') + }); +}; + +const schemaType = typeOfSchema(createSchema); +/** @internal */ +export type JsonLayoutConfigType = typeof schemaType; + +/** + * Layout that just converts `LogRecord` into JSON string. + * @internal + */ +export class JsonLayout implements Layout { + static createConfigSchema = createSchema; + + format({ timestamp, level, context, message, error, meta }: LogRecord): string { + return JSON.stringify({ + '@timestamp': timestamp.toISOString(), + level: level.id.toUpperCase(), + context, + message, + error: error && error.message, + meta + }); + } +} diff --git a/platform/logging/layouts/Layouts.ts b/platform/logging/layouts/Layouts.ts index 0fb9d82201008..40bff2cdd8510 100644 --- a/platform/logging/layouts/Layouts.ts +++ b/platform/logging/layouts/Layouts.ts @@ -1,8 +1,10 @@ +import { assertNever } from '../../lib/utils'; import { Schema } from '../../types'; +import { JsonLayout, JsonLayoutConfigType } from './JsonLayout'; import { PatternLayout, PatternLayoutConfigType } from './PatternLayout'; import { LogRecord } from '../LogRecord'; -type LayoutConfigType = PatternLayoutConfigType; +type LayoutConfigType = PatternLayoutConfigType | JsonLayoutConfigType; /** * Entity that can format `LogRecord` instance into a string. @@ -15,7 +17,12 @@ export interface Layout { /** @internal */ export class Layouts { static createConfigSchema(schema: Schema) { - return PatternLayout.createConfigSchema(schema); + const { oneOf } = schema; + + return oneOf([ + JsonLayout.createConfigSchema(schema), + PatternLayout.createConfigSchema(schema) + ]); } /** @@ -24,6 +31,13 @@ export class Layouts { * @returns Fully constructed `Layout` instance. */ static create(config: LayoutConfigType): Layout { - return new PatternLayout(config.pattern, config.highlight); + switch (config.kind) { + case 'json': + return new JsonLayout(); + case 'pattern': + return new PatternLayout(config.pattern, config.highlight); + default: + return assertNever(config); + } } } diff --git a/platform/logging/layouts/__tests__/JsonLayout.test.ts b/platform/logging/layouts/__tests__/JsonLayout.test.ts new file mode 100644 index 0000000000000..4434085829268 --- /dev/null +++ b/platform/logging/layouts/__tests__/JsonLayout.test.ts @@ -0,0 +1,59 @@ +import * as mockSchema from '../../../lib/schema'; + +import { LogLevel } from '../../LogLevel'; +import { LogRecord } from '../../LogRecord'; +import { JsonLayout } from '../JsonLayout'; + +const records: LogRecord[] = [ + { + timestamp: new Date(2012, 1, 1), + message: 'message-1', + context: 'context-1', + error: new Error('Some error message'), + level: LogLevel.Fatal + }, + { + timestamp: new Date(2012, 1, 1), + message: 'message-2', + context: 'context-2', + level: LogLevel.Error + }, + { + timestamp: new Date(2012, 1, 1), + message: 'message-3', + context: 'context-3', + level: LogLevel.Warn + }, + { + timestamp: new Date(2012, 1, 1), + message: 'message-4', + context: 'context-4', + level: LogLevel.Debug + }, + { + timestamp: new Date(2012, 1, 1), + message: 'message-5', + context: 'context-5', + level: LogLevel.Info + }, + { + timestamp: new Date(2012, 1, 1), + message: 'message-6', + context: 'context-6', + level: LogLevel.Trace + } +]; + +test('`createConfigSchema()` creates correct schema.', () => { + const layoutSchema = JsonLayout.createConfigSchema(mockSchema); + + expect(layoutSchema.validate({ kind: 'json' })).toEqual({ kind: 'json' }); +}); + +test('`format()` correctly formats record.', () => { + const layout = new JsonLayout(); + + for (const record of records) { + expect(layout.format(record)).toMatchSnapshot(); + } +}); diff --git a/platform/logging/layouts/__tests__/Layouts.test.ts b/platform/logging/layouts/__tests__/Layouts.test.ts index 01c9e3207a529..096e19f40281b 100644 --- a/platform/logging/layouts/__tests__/Layouts.test.ts +++ b/platform/logging/layouts/__tests__/Layouts.test.ts @@ -1,8 +1,9 @@ import * as mockSchema from '../../../lib/schema'; +import { JsonLayout } from '../JsonLayout'; import { PatternLayout } from '../PatternLayout'; import { Layouts } from '../Layouts'; -test('`createConfigSchema()` creates correct schema.', () => { +test('`createConfigSchema()` creates correct schema for `pattern` layout.', () => { const layoutsSchema = Layouts.createConfigSchema(mockSchema); const validConfigWithOptional = { kind: 'pattern' }; expect(layoutsSchema.validate(validConfigWithOptional)).toEqual({ @@ -22,13 +23,17 @@ test('`createConfigSchema()` creates correct schema.', () => { highlight: true }); - const wrongConfig1 = { kind: 'json' }; - expect(() => layoutsSchema.validate(wrongConfig1)).toThrow(); - const wrongConfig2 = { kind: 'pattern', pattern: 1 }; expect(() => layoutsSchema.validate(wrongConfig2)).toThrow(); }); +test('`createConfigSchema()` creates correct schema for `json` layout.', () => { + const layoutsSchema = Layouts.createConfigSchema(mockSchema); + + const validConfig = { kind: 'json' }; + expect(layoutsSchema.validate(validConfig)).toEqual({ kind: 'json' }); +}); + test('`create()` creates correct layout.', () => { const patternLayout = Layouts.create({ kind: 'pattern', @@ -36,4 +41,7 @@ test('`create()` creates correct layout.', () => { highlight: false }); expect(patternLayout).toBeInstanceOf(PatternLayout); + + const jsonLayout = Layouts.create({ kind: 'json' }); + expect(jsonLayout).toBeInstanceOf(JsonLayout); }); diff --git a/platform/logging/layouts/__tests__/PatternLayout.test.ts b/platform/logging/layouts/__tests__/PatternLayout.test.ts index ea8cb232a964f..0da8d888a5c0a 100644 --- a/platform/logging/layouts/__tests__/PatternLayout.test.ts +++ b/platform/logging/layouts/__tests__/PatternLayout.test.ts @@ -9,7 +9,7 @@ const records: LogRecord[] = [ timestamp: new Date(2012, 1, 1), message: 'message-1', context: 'context-1', - error: new Error('Error'), + error: new Error('Some error message'), level: LogLevel.Fatal }, { @@ -76,12 +76,7 @@ test('`format()` correctly formats record with full pattern.', () => { const layout = new PatternLayout(); for (const record of records) { - const { timestamp, level, context, message } = record; - const formattedLevel = level.id.toUpperCase().padEnd(5); - - expect(layout.format(record)).toBe( - `[${timestamp.toISOString()}][${formattedLevel}][${context}] ${message}` - ); + expect(layout.format(record)).toMatchSnapshot(); } }); diff --git a/platform/logging/layouts/__tests__/__snapshots__/JsonLayout.test.ts.snap b/platform/logging/layouts/__tests__/__snapshots__/JsonLayout.test.ts.snap new file mode 100644 index 0000000000000..00f3808fa6058 --- /dev/null +++ b/platform/logging/layouts/__tests__/__snapshots__/JsonLayout.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`\`format()\` correctly formats record. 1`] = `"{\\"timestamp\\":\\"2012-01-31T23:00:00.000Z\\",\\"message\\":\\"message-1\\",\\"context\\":\\"context-1\\",\\"error\\":\\"Some error message\\",\\"level\\":{\\"id\\":\\"fatal\\",\\"value\\":2}}"`; + +exports[`\`format()\` correctly formats record. 2`] = `"{\\"timestamp\\":\\"2012-01-31T23:00:00.000Z\\",\\"message\\":\\"message-2\\",\\"context\\":\\"context-2\\",\\"level\\":{\\"id\\":\\"error\\",\\"value\\":3}}"`; + +exports[`\`format()\` correctly formats record. 3`] = `"{\\"timestamp\\":\\"2012-01-31T23:00:00.000Z\\",\\"message\\":\\"message-3\\",\\"context\\":\\"context-3\\",\\"level\\":{\\"id\\":\\"warn\\",\\"value\\":4}}"`; + +exports[`\`format()\` correctly formats record. 4`] = `"{\\"timestamp\\":\\"2012-01-31T23:00:00.000Z\\",\\"message\\":\\"message-4\\",\\"context\\":\\"context-4\\",\\"level\\":{\\"id\\":\\"debug\\",\\"value\\":6}}"`; + +exports[`\`format()\` correctly formats record. 5`] = `"{\\"timestamp\\":\\"2012-01-31T23:00:00.000Z\\",\\"message\\":\\"message-5\\",\\"context\\":\\"context-5\\",\\"level\\":{\\"id\\":\\"info\\",\\"value\\":5}}"`; + +exports[`\`format()\` correctly formats record. 6`] = `"{\\"timestamp\\":\\"2012-01-31T23:00:00.000Z\\",\\"message\\":\\"message-6\\",\\"context\\":\\"context-6\\",\\"level\\":{\\"id\\":\\"trace\\",\\"value\\":7}}"`; diff --git a/platform/logging/layouts/__tests__/__snapshots__/PatternLayout.test.ts.snap b/platform/logging/layouts/__tests__/__snapshots__/PatternLayout.test.ts.snap index b0623799c83cc..0f883a4977bd2 100644 --- a/platform/logging/layouts/__tests__/__snapshots__/PatternLayout.test.ts.snap +++ b/platform/logging/layouts/__tests__/__snapshots__/PatternLayout.test.ts.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`\`format()\` correctly formats record with full pattern. 1`] = `"[2012-01-31T23:00:00.000Z][FATAL][context-1] message-1"`; + +exports[`\`format()\` correctly formats record with full pattern. 2`] = `"[2012-01-31T23:00:00.000Z][ERROR][context-2] message-2"`; + +exports[`\`format()\` correctly formats record with full pattern. 3`] = `"[2012-01-31T23:00:00.000Z][WARN ][context-3] message-3"`; + +exports[`\`format()\` correctly formats record with full pattern. 4`] = `"[2012-01-31T23:00:00.000Z][DEBUG][context-4] message-4"`; + +exports[`\`format()\` correctly formats record with full pattern. 5`] = `"[2012-01-31T23:00:00.000Z][INFO ][context-5] message-5"`; + +exports[`\`format()\` correctly formats record with full pattern. 6`] = `"[2012-01-31T23:00:00.000Z][TRACE][context-6] message-6"`; + exports[`\`format()\` correctly formats record with highlighting. 1`] = `"[2012-01-31T23:00:00.000Z][FATAL][context-1] message-1"`; exports[`\`format()\` correctly formats record with highlighting. 2`] = `"[2012-01-31T23:00:00.000Z][ERROR][context-2] message-2"`;