diff --git a/package.json b/package.json index 314ffa092ad340d..d25a0b0a1f991e5 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "h2o2-latest": "npm:h2o2@8.1.2", "handlebars": "4.0.5", "hapi": "14.2.0", - "hapi-latest": "npm:hapi@17.5.0", + "hapi-latest": "npm:hapi@17.5.2", "hjson": "3.1.0", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.1", @@ -244,7 +244,8 @@ "@types/fetch-mock": "^5.12.2", "@types/getopts": "^2.0.0", "@types/glob": "^5.0.35", - "@types/hapi-latest": "npm:@types/hapi@17.0.12", + "@types/hapi": "^13.0.38", + "@types/hapi-latest": "npm:@types/hapi@17.0.15", "@types/has-ansi": "^3.0.0", "@types/jest": "^22.2.3", "@types/joi": "^10.4.4", diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index e67331e360c56fc..b66306b4b1c5a0e 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -17,7 +17,6 @@ * under the License. */ -import { Server } from 'hapi'; import { debounce, invoke, bindAll, once, uniq } from 'lodash'; import { resolve } from 'path'; @@ -25,29 +24,12 @@ import Log from '../log'; import Worker from './worker'; import { Config } from '../../server/config/config'; import { transformDeprecations } from '../../server/config/transform_deprecations'; -import { setupLogging } from '../../server/logging'; process.env.kbnWorkerType = 'managr'; export default class ClusterManager { static create(opts, settings = {}, basePathProxy) { - const config = Config.withDefaultSchema(transformDeprecations(settings)); - - // New platform forwards all logs to the legacy platform so we need HapiJS server - // here just for logging purposes and nothing else. - const server = new Server(); - setupLogging(server, config); - - return { - kbnServer: { - async ready() {}, - server, - close() {}, - listen() {} - }, - - clusterManager: new ClusterManager(opts, config, basePathProxy) - }; + return new ClusterManager(opts, Config.withDefaultSchema(transformDeprecations(settings)), basePathProxy); } constructor(opts, config, basePathProxy) { diff --git a/src/core/server/config/__tests__/config_service.test.ts b/src/core/server/config/__tests__/config_service.test.ts index e676de9e2c0e688..c2b68d52d0210e1 100644 --- a/src/core/server/config/__tests__/config_service.test.ts +++ b/src/core/server/config/__tests__/config_service.test.ts @@ -19,7 +19,7 @@ /* tslint:disable max-classes-per-file */ import { BehaviorSubject, first, k$, toPromise } from '../../../lib/kbn_observable'; -import { AnyType, schema, TypeOf } from '../schema'; +import { schema, Type, TypeOf } from '../schema'; import { ConfigService, ObjectToRawConfigAdapter } from '..'; import { logger } from '../../logging/__mocks__'; @@ -268,7 +268,7 @@ test('treats config as enabled if config path is not present in config', async ( expect(unusedPaths).toEqual([]); }); -function createClassWithSchema(s: AnyType) { +function createClassWithSchema(s: Type) { return class ExampleClassWithSchema { public static schema = s; diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index 18d24c307fba1b0..201130d58daf93a 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -24,7 +24,7 @@ import { Logger, LoggerFactory } from '../logging'; import { ConfigWithSchema } from './config_with_schema'; import { Env } from './env'; import { RawConfig } from './raw_config'; -import { AnyType } from './schema'; +import { Type } from './schema'; export type ConfigPath = string | string[]; @@ -61,7 +61,7 @@ export class ConfigService { * @param ConfigClass A class (not an instance of a class) that contains a * static `schema` that we validate the config at the given `path` against. */ - public atPath( + public atPath, Config>( path: ConfigPath, ConfigClass: ConfigWithSchema ) { @@ -76,7 +76,7 @@ export class ConfigService { * * @see atPath */ - public optionalAtPath( + public optionalAtPath, Config>( path: ConfigPath, ConfigClass: ConfigWithSchema ) { @@ -120,7 +120,7 @@ export class ConfigService { return config.getFlattenedPaths().filter(path => !isPathHandled(path, handledPaths)); } - private createConfig( + private createConfig, Config>( path: ConfigPath, rawConfig: {}, ConfigClass: ConfigWithSchema diff --git a/src/core/server/config/config_with_schema.ts b/src/core/server/config/config_with_schema.ts index f049d28fa9787fb..31de12a95cc9f76 100644 --- a/src/core/server/config/config_with_schema.ts +++ b/src/core/server/config/config_with_schema.ts @@ -19,7 +19,7 @@ // TODO inline all of these import { Env } from './env'; -import { AnyType, TypeOf } from './schema'; +import { Type, TypeOf } from './schema'; /** * Interface that defines the static side of a config class. @@ -31,7 +31,7 @@ import { AnyType, TypeOf } from './schema'; * in TypeScript, but it can be used to ensure we have a config class that * matches whenever it's used. */ -export interface ConfigWithSchema { +export interface ConfigWithSchema, Config> { /** * Any config class must define a schema that validates the config, based on * the injected `schema` helper. diff --git a/src/core/server/config/schema/index.ts b/src/core/server/config/schema/index.ts index 4a4baffc4d4a21f..8b53f38ac5edbd4 100644 --- a/src/core/server/config/schema/index.ts +++ b/src/core/server/config/schema/index.ts @@ -48,9 +48,13 @@ import { UnionType, } from './types'; -export { AnyType, ObjectType, TypeOf }; +export { ObjectType, TypeOf, Type }; export { ByteSizeValue } from './byte_size_value'; +function any(options?: TypeOptions) { + return new AnyType(options); +} + function boolean(options?: TypeOptions): Type { return new BooleanType(options); } @@ -135,7 +139,7 @@ function oneOf( ): Type; function oneOf(types: [Type, Type], options?: TypeOptions): Type; function oneOf(types: [Type], options?: TypeOptions): Type; -function oneOf(types: RTS, options?: TypeOptions): Type { +function oneOf>>(types: RTS, options?: TypeOptions): Type { return new UnionType(types, options); } @@ -158,6 +162,7 @@ function conditional( } export const schema = { + any, arrayOf, boolean, byteSize, diff --git a/src/core/server/config/schema/types/any_type.ts b/src/core/server/config/schema/types/any_type.ts index 53a32c58f001bf2..c4e8b9d3f6f5dab 100644 --- a/src/core/server/config/schema/types/any_type.ts +++ b/src/core/server/config/schema/types/any_type.ts @@ -17,6 +17,18 @@ * under the License. */ -import { Type } from './type'; +import typeDetect from 'type-detect'; +import { internals } from '../internals'; +import { Type, TypeOptions } from './type'; -export type AnyType = Type; +export class AnyType extends Type { + constructor(options?: TypeOptions) { + super(internals.any(), options); + } + + protected handleError(type: string, { value }: Record) { + if (type === 'any.required') { + return `expected value of type [any] but got [${typeDetect(value)}]`; + } + } +} diff --git a/src/core/server/config/schema/types/object_type.ts b/src/core/server/config/schema/types/object_type.ts index 7e9e8a796cc72ff..e61fcd90ef016f7 100644 --- a/src/core/server/config/schema/types/object_type.ts +++ b/src/core/server/config/schema/types/object_type.ts @@ -19,12 +19,11 @@ import typeDetect from 'type-detect'; import { AnySchema, internals } from '../internals'; -import { AnyType } from './any_type'; import { Type, TypeOptions } from './type'; -export type Props = Record; +export type Props = Record>; -export type TypeOf = RT['type']; +export type TypeOf> = RT['type']; // Because of https://github.com/Microsoft/TypeScript/issues/14041 // this might not have perfect _rendering_ output, but it will be typed. diff --git a/src/core/server/config/schema/types/union_type.ts b/src/core/server/config/schema/types/union_type.ts index 3d39ac0ea212d0a..e6efb4afb66aa9e 100644 --- a/src/core/server/config/schema/types/union_type.ts +++ b/src/core/server/config/schema/types/union_type.ts @@ -20,10 +20,9 @@ import typeDetect from 'type-detect'; import { SchemaTypeError, SchemaTypesError } from '../errors'; import { internals } from '../internals'; -import { AnyType } from './any_type'; import { Type, TypeOptions } from './type'; -export class UnionType extends Type { +export class UnionType>, T> extends Type { constructor(types: RTS, options?: TypeOptions) { const schema = internals.alternatives(types.map(type => type.getSchema())); diff --git a/src/core/server/legacy_compat/legacy_platform_config.ts b/src/core/server/legacy_compat/legacy_platform_config.ts index 2be1629db5ff01f..5f41bbd0c095d9d 100644 --- a/src/core/server/legacy_compat/legacy_platform_config.ts +++ b/src/core/server/legacy_compat/legacy_platform_config.ts @@ -33,6 +33,7 @@ interface LegacyLoggingConfig { quiet?: boolean; dest?: string; json?: boolean; + events?: Record; } /** @@ -50,7 +51,24 @@ export class LegacyConfigToRawConfigAdapter implements RawConfig { private static transformLogging(configValue: LegacyLoggingConfig = {}) { const loggingConfig = { - appenders: { default: { kind: 'legacy-appender' } }, + appenders: { + default: { + kind: 'legacy-appender', + // We set `ops.interval` to max allowed number and `ops` filter to value + // that doesn't exist to avoid logging of ops information by log-only + // server instance we use in the core to emulate "legacy" Kibana log layout. + legacy: { + logging: { + ...configValue, + events: { + ...configValue.events, + ops: '__no-ops__', + }, + }, + ops: { interval: 2147483647 }, + }, + }, + }, root: { level: 'info' }, }; diff --git a/src/core/server/legacy_compat/legacy_service.ts b/src/core/server/legacy_compat/legacy_service.ts index f064fc23d4c8a95..46c016c1fca5b3c 100644 --- a/src/core/server/legacy_compat/legacy_service.ts +++ b/src/core/server/legacy_compat/legacy_service.ts @@ -26,7 +26,6 @@ import { ConfigService, Env, RawConfig } from '../config'; import { DevConfig } from '../dev'; import { BasePathProxyServer, HttpConfig } from '../http'; import { Logger, LoggerFactory } from '../logging'; -import { LegacyLogRecord } from './logging/appenders/legacy_appender'; interface LegacyKbnServer { applyLoggingConfiguration: (settings: Readonly>) => void; @@ -50,7 +49,6 @@ interface LegacyServiceOptions { export class LegacyService implements CoreService { private readonly log: Logger; - private readonly logBuffer: LegacyLogRecord[] = []; private kbnServer?: LegacyKbnServer; private kbnServerSubscription?: Subscription; @@ -62,7 +60,6 @@ export class LegacyService implements CoreService { ) { this.log = logger.get('legacy.service'); - this.setupLogListener(); this.setupConnectionListener(); } @@ -86,8 +83,6 @@ export class LegacyService implements CoreService { next: async kbnServerPromise => { this.kbnServer = await kbnServerPromise; - this.flushLogBuffer(); - await this.kbnServer.listen(); }, error: err => this.log.error(err), @@ -124,26 +119,6 @@ export class LegacyService implements CoreService { }); } - private setupLogListener() { - this.env.legacy.on('kbn:log', async (record: LegacyLogRecord) => { - // We can't log messages until "legacy" Kibana prepares its logging system. - if (this.kbnServer === undefined) { - this.logBuffer.push(record); - return; - } - - this.kbnServer.server.log(record.tags, record.data, record.timestamp); - }); - } - - private flushLogBuffer() { - for (const logRecord of this.logBuffer) { - this.env.legacy.emit('kbn:log', logRecord); - } - - this.logBuffer.length = 0; - } - private async createClusterManager(rawConfig: RawConfig) { const [devConfig, httpConfig] = await k$( $combineLatest( @@ -153,8 +128,7 @@ export class LegacyService implements CoreService { )(first(), toPromise()); const cliArgs = this.env.getCliArgs(); - - const { kbnServer } = require(CLUSTER_MANAGER_PATH).create( + require(CLUSTER_MANAGER_PATH).create( cliArgs, rawConfig.getRaw(), cliArgs.basePath @@ -162,21 +136,17 @@ export class LegacyService implements CoreService { : undefined ); - return kbnServer as LegacyKbnServer; + return { + close() { + // noop + }, + listen() { + // noop + }, + } as LegacyKbnServer; } private async createKbnServer(rawConfig: RawConfig) { - const KbnServer = require('../../../server/kbn_server'); - const kbnServer: LegacyKbnServer = new KbnServer(rawConfig.getRaw(), this.env.legacy); - - // The kbnWorkerType check is necessary to prevent the repl - // from being started multiple times in different processes. - // We only want one REPL. - const { repl } = this.env.getCliArgs(); - if (repl && process.env.kbnWorkerType === 'server') { - require(REPL_PATH).startRepl(this.kbnServer); - } - const httpConfig = await k$(this.configService.atPath('server', HttpConfig))( first(), toPromise() @@ -186,8 +156,19 @@ export class LegacyService implements CoreService { // and connection options won't be passed to the "legacy" Kibana that will block // its initialization, so we should make "legacy" Kibana aware of that case and // let it continue initialization without waiting for connection options. - if (!httpConfig.autoListen) { - this.env.legacy.emit('kbn:connection', { autoListen: false }); + const connection = !httpConfig.autoListen + ? { autoListen: false } + : await new Promise(resolve => this.env.legacy.once('kbn:connection', resolve)); + + const KbnServer = require('../../../server/kbn_server'); + const kbnServer: LegacyKbnServer = new KbnServer(rawConfig.getRaw(), { connection }); + + // The kbnWorkerType check is necessary to prevent the repl + // from being started multiple times in different processes. + // We only want one REPL. + const { repl } = this.env.getCliArgs(); + if (repl && process.env.kbnWorkerType === 'server') { + require(REPL_PATH).startRepl(this.kbnServer); } await kbnServer.ready(); diff --git a/src/core/server/legacy_compat/logging/appenders/legacy_appender.ts b/src/core/server/legacy_compat/logging/appenders/legacy_appender.ts index d1e15a040ca07c1..f8e80f84baeb0b5 100644 --- a/src/core/server/legacy_compat/logging/appenders/legacy_appender.ts +++ b/src/core/server/legacy_compat/logging/appenders/legacy_appender.ts @@ -17,30 +17,35 @@ * under the License. */ -import { EventEmitter } from 'events'; -import { schema } from '../../../config/schema'; +import { Server } from 'hapi'; + +// @ts-ignore: implicit any for JS file +import { Config, transformDeprecations } from '../../../../../server/config'; +// @ts-ignore: implicit any for JS file +import { setupLogging } from '../../../../../server/logging'; +import { schema } from '../../../config/schema'; import { DisposableAppender } from '../../../logging/appenders/appenders'; import { LogRecord } from '../../../logging/log_record'; -export interface LegacyLogRecord { - tags: string | string[]; - data?: string | Error; - timestamp?: Date; -} - -const { literal, object } = schema; - /** * Simple appender that just forwards `LogRecord` to the legacy KbnServer log. * @internal */ export class LegacyAppender implements DisposableAppender { - public static configSchema = object({ - kind: literal('legacy-appender'), + public static configSchema = schema.object({ + kind: schema.literal('legacy-appender'), + legacy: schema.any(), }); - constructor(private readonly legacyChannel: EventEmitter) {} + private readonly logServer: Server; + + constructor(legacyConfig: Readonly>) { + // The "legacy" Kibana uses Hapi server + even-better plugin to log, so to + // have the same look & feel we should use that approach here too. + this.logServer = new Server(); + setupLogging(this.logServer, Config.withDefaultSchema(transformDeprecations(legacyConfig))); + } /** * Forwards `LogRecord` to the legacy platform that will layout and @@ -48,16 +53,14 @@ export class LegacyAppender implements DisposableAppender { * @param record `LogRecord` instance to forward to. */ public append({ level, context, message, error, timestamp, meta = {} }: LogRecord) { - const legacyLogRecord: LegacyLogRecord = { - data: error || message, - tags: [level.id.toLowerCase(), ...context.split('.'), ...(meta.tags || [])], - timestamp, - }; - - this.legacyChannel.emit('kbn:log', legacyLogRecord); + this.logServer.log( + [level.id.toLowerCase(), ...context.split('.'), ...(meta.tags || [])], + error || message, + timestamp.getMilliseconds() + ); } public async dispose() { - // noop + this.logServer.stop(); } } diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts index 901af39d74c3d40..ed960c50a9cf8c3 100644 --- a/src/core/server/logging/appenders/appenders.ts +++ b/src/core/server/logging/appenders/appenders.ts @@ -74,7 +74,7 @@ export class Appenders { return new FileAppender(Layouts.create(config.layout), config.path); case 'legacy-appender': - return new LegacyAppender(env.legacy); + return new LegacyAppender(config.legacy); default: return assertNever(config); diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts index b7c6c9d7dcf087e..eeba3b1b18589ec 100644 --- a/src/core/server/logging/logging_service.ts +++ b/src/core/server/logging/logging_service.ts @@ -17,8 +17,6 @@ * under the License. */ -import { Observable, Subscription } from '../../lib/kbn_observable'; - import { MutableLoggerFactory } from './logger_factory'; import { LoggingConfig } from './logging_config'; @@ -27,19 +25,15 @@ import { LoggingConfig } from './logging_config'; * pushing updates the the logger factory. */ export class LoggingService { - private subscription?: Subscription; - constructor(private readonly loggingFactory: MutableLoggerFactory) {} /** * Takes `LoggingConfig` observable and pushes all config updates to the * internal logger factory. - * @param config$ Observable that tracks all updates in the logging config. + * @param config Logging config. */ - public upgrade(config$: Observable) { - this.subscription = config$.subscribe({ - next: config => this.loggingFactory.updateConfig(config), - }); + public upgrade(config: LoggingConfig) { + this.loggingFactory.updateConfig(config); } /** @@ -47,9 +41,6 @@ export class LoggingService { * and close internal logger factory. */ public async stop() { - if (this.subscription !== undefined) { - this.subscription.unsubscribe(); - } await this.loggingFactory.close(); } } diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index 1d941fc6c71430a..dda32417c905c75 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -20,7 +20,7 @@ import { Server } from '..'; import { ConfigService, Env, RawConfig } from '../config'; -import { Observable } from '../../lib/kbn_observable'; +import { Observable, Subscription } from '../../lib/kbn_observable'; import { LegacyService } from '../legacy_compat/legacy_service'; import { Logger, @@ -45,6 +45,7 @@ export class Root { private server?: Server; private readonly loggingService: LoggingService; private readonly legacyService: LegacyService; + private loggingConfigSubscription?: Subscription; constructor( rawConfig$: Observable, @@ -64,18 +65,7 @@ export class Root { } public async start() { - try { - const loggingConfig$ = this.configService.atPath('logging', LoggingConfig); - this.loggingService.upgrade(loggingConfig$); - } catch (e) { - // This specifically console.logs because we were not able to configure - // the logger. - // tslint:disable no-console - console.error('Configuring logger failed:', e.message); - - await this.shutdown(e); - throw e; - } + this.setupLogging(); this.log.debug('Starting root'); @@ -87,8 +77,6 @@ export class Root { await this.server.start(); } - // Since core logging system relies on the "legacy" Kibana, all log messages - // will be delayed until legacy platform is initialized. await this.legacyService.start(); } catch (e) { await this.shutdown(e); @@ -106,10 +94,41 @@ export class Root { this.server = undefined; } - await this.loggingService.stop(); + await this.teardownLogging(); if (this.options.onShutdown !== undefined) { this.options.onShutdown(reason); } } + + private setupLogging() { + const onLoggingSetupFailure = (err: Error) => { + // This specifically console.logs because we were not able to configure the logger. + // tslint:disable no-console + console.error('Configuring logger failed:', err); + return this.shutdown(err); + }; + + // Error are logged with console.logs because we were not able to configure + // the logger. + this.loggingConfigSubscription = this.configService.atPath('logging', LoggingConfig).subscribe({ + next: async config => { + try { + this.loggingService.upgrade(config); + } catch (err) { + await onLoggingSetupFailure(err); + } + }, + error: async err => await onLoggingSetupFailure(err), + }); + } + + private async teardownLogging() { + if (this.loggingConfigSubscription !== undefined) { + this.loggingConfigSubscription.unsubscribe(); + this.loggingConfigSubscription = undefined; + } + + await this.loggingService.stop(); + } } diff --git a/src/server/http/index.js b/src/server/http/index.js index c5ae80dc5830cad..8c739a559205e2a 100644 --- a/src/server/http/index.js +++ b/src/server/http/index.js @@ -35,9 +35,7 @@ export default async function (kbnServer, server, config) { const shortUrlLookup = shortUrlLookupProvider(server); - server.connection( - await new Promise(resolve => kbnServer.core.once('kbn:connection', resolve)) - ); + server.connection(kbnServer.core.connection); registerHapiPlugins(server); diff --git a/yarn.lock b/yarn.lock index 411141bc58c3aca..9bc7263fbae035d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -350,9 +350,9 @@ dependencies: "@types/node" "*" -"@types/hapi-latest@npm:@types/hapi@17.0.12": - version "17.0.12" - resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-17.0.12.tgz#5751f4d8db4decb4eae6671a4efbeae671278ceb" +"@types/hapi-latest@npm:@types/hapi@17.0.15": + version "17.0.15" + resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-17.0.15.tgz#7a65508d4ba59edc1e79b553cb4d909167dbfac6" dependencies: "@types/boom" "*" "@types/catbox" "*" @@ -363,6 +363,12 @@ "@types/podium" "*" "@types/shot" "*" +"@types/hapi@^13.0.38": + version "13.0.38" + resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-13.0.38.tgz#3671ab29fd465d61e394718ce546b7d5ef21a331" + dependencies: + "@types/node" "*" + "@types/has-ansi@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/has-ansi/-/has-ansi-3.0.0.tgz#636403dc4e0b2649421c4158e5c404416f3f0330" @@ -6142,9 +6148,9 @@ hapi-auth-cookie@6.1.1: hoek "3.x.x" joi "7.x.x" -"hapi-latest@npm:hapi@17.5.0": - version "17.5.0" - resolved "https://registry.yarnpkg.com/hapi/-/hapi-17.5.0.tgz#9fc33f10d6f563d0203853937b60dd13a59b51ce" +"hapi-latest@npm:hapi@17.5.2": + version "17.5.2" + resolved "https://registry.yarnpkg.com/hapi/-/hapi-17.5.2.tgz#9c5823cdcdd17e5621ebc8928aefb144d033caac" dependencies: accept "3.x.x" ammo "3.x.x"