diff --git a/package.json b/package.json index 9a800978914d..5ae4d076cf87 100644 --- a/package.json +++ b/package.json @@ -58,10 +58,8 @@ "file-loader": "^0.8.4", "font-awesome": "^4.3.0", "glob": "^4.3.2", - "good": "^5.1.2", - "good-console": "^4.1.0", - "good-file": "^4.0.2", - "good-reporter": "^3.1.0", + "good": "^6.2.0", + "good-squeeze": "^2.1.0", "hapi": "^8.6.1", "http-auth": "^2.2.5", "imports-loader": "^0.6.4", diff --git a/src/server/config/config.js b/src/server/config/config.js index 154b0e6fcb29..bc20ab4cea38 100644 --- a/src/server/config/config.js +++ b/src/server/config/config.js @@ -7,8 +7,6 @@ function Config(schema, defaults) { this.schema = schema || Joi.object({}).default(); this.config = {}; this.set(defaults); - - console.log(this.get('env')); } Config.prototype.extendSchema = function (key, schema) { diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 26cc6aadb8ba..b45d92253474 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -4,22 +4,6 @@ var path = require('path'); var package = require('../utils/closestPackageJson').getSync(); var fromRoot = require('../utils/fromRoot'); -var env = (function () { - switch (process.env.NODE_ENV) { - case 'production': - case 'prod': - case undefined: - return 'production'; - case 'development': - case 'dev': - return 'development'; - default: - throw new TypeError(`Unexpected NODE_ENV "${process.env.NODE_ENV}", expected production or development.`); - } -}()); -var dev = env === 'development'; -var prod = env === 'production'; - module.exports = Joi.object({ env: Joi.object({ name: Joi.string().default(Joi.ref('$env')), @@ -67,14 +51,24 @@ module.exports = Joi.object({ logging: Joi.object({ quiet: Joi.boolean().default(false), - file: Joi.string(), - console: Joi.object({ - ops: Joi.any(), - log: Joi.any().default('*'), - response: Joi.any().default('*'), - error: Joi.any().default('*'), - json: Joi.boolean().default(false), - }).default() + + // not nested under a kbnLogger key so that we can ref "quiet" + kbnLogger: Joi.boolean().default(true), + kbnLoggerConfig: Joi.object({ + dest: Joi.string().default('stdout'), + json: Joi.boolean().default(Joi.ref('$prod')) + }).default(), + kbnLoggerEvents: Joi.when('quiet', { + is: true, + then: Joi.object({ + error: Joi.string().default('*') + }).default(), + otherwise: Joi.object({ + log: Joi.string().default('*'), + response: Joi.string().default('*'), + error: Joi.string().default('*') + }).default() + }) }).default(), plugins: Joi.object({ diff --git a/src/server/logging/LogFormat.js b/src/server/logging/LogFormat.js new file mode 100644 index 000000000000..b16c46c8956e --- /dev/null +++ b/src/server/logging/LogFormat.js @@ -0,0 +1,133 @@ +'use strict'; + +let Stream = require('stream'); +let moment = require('moment'); +let _ = require('lodash'); +let numeral = require('numeral'); +let ansicolors = require('ansicolors'); +let stringify = require('json-stringify-safe'); +let querystring = require('querystring'); +let inspect = require('util').inspect; + +function serializeError(err) { + return { + message: err.message, + name: err.name, + stack: err.stack, + code: err.code, + signal: err.signal + }; +} + +let levelColor = function (code) { + if (code < 299) return ansicolors.green(code); + if (code < 399) return ansicolors.yellow(code); + if (code < 499) return ansicolors.magenta(code); + return ansicolors.red(code); +}; + +module.exports = class TransformObjStream extends Stream.Transform { + constructor() { + super({ + readableObjectMode: false, + writableObjectMode: true + }); + } + + _transform(event, enc, next) { + var data = this.readEvent(event); + this.push(this.format(data) + '\n'); + next(); + } + + readEvent(event) { + var data = { + type: event.event, + '@timestamp': moment.utc(event.timestamp).format(), + tags: [].concat(event.tags || []), + pid: event.pid + }; + + if (data.type === 'response') { + _.defaults(data, _.pick(event, [ + 'method', + 'statusCode' + ])); + + data.req = { + url: event.path, + method: event.method, + headers: event.headers, + remoteAddress: event.source.remoteAddress, + userAgent: event.source.remoteAddress, + referer: event.source.referer + }; + + var contentLength = 0; + if (typeof event.responsePayload === 'object') { + contentLength = stringify(event.responsePayload).length; + } else { + contentLength = event.responsePayload.toString().length; + } + + data.res = { + statusCode: event.statusCode, + responseTime: event.responseTime, + contentLength: contentLength + }; + + var query = querystring.stringify(event.query); + if (query) data.req.url += '?' + query; + + + data.message = data.req.method.toUpperCase() + ' '; + data.message += data.req.url; + data.message += ' '; + data.message += levelColor(data.res.statusCode); + data.message += ' '; + data.message += ansicolors.brightBlack(data.res.responseTime + 'ms'); + data.message += ansicolors.brightBlack(' - ' + numeral(contentLength).format('0.0b')); + } + else if (data.type === 'ops') { + _.defaults(data, _.pick(event, [ + 'pid', + 'os', + 'proc', + 'load' + ])); + data.message = ansicolors.brightBlack('memory: '); + data.message += numeral(data.proc.mem.heapUsed).format('0.0b'); + data.message += ' '; + data.message += ansicolors.brightBlack('uptime: '); + data.message += numeral(data.proc.uptime).format('00:00:00'); + data.message += ' '; + data.message += ansicolors.brightBlack('load: ['); + data.message += data.os.load.map(function (val) { + return numeral(val).format('0.00'); + }).join(' '); + data.message += ansicolors.brightBlack(']'); + data.message += ' '; + data.message += ansicolors.brightBlack('delay: '); + data.message += numeral(data.proc.delay).format('0.000'); + } + else if (data.type === 'error') { + data.level = 'error'; + data.message = event.error.message; + data.error = serializeError(event.error); + data.url = event.url; + } + else if (event.data instanceof Error) { + data.level = _.contains(event.tags, 'fatal') ? 'fatal' : 'error'; + data.message = event.data.message; + data.error = serializeError(event.data); + } + else if (_.isPlainObject(event.data) && event.data.message) { + _.assign(data, event.data); + data.message = _.template(event.data.message)(event.data); + } + else { + data.message = _.isString(event.data) ? event.data : inspect(event.data); + } + return data; + } +}; diff --git a/src/server/logging/LogFormatJson.js b/src/server/logging/LogFormatJson.js new file mode 100644 index 000000000000..2bc7a3d59874 --- /dev/null +++ b/src/server/logging/LogFormatJson.js @@ -0,0 +1,15 @@ +'use strict'; + +let LogFormat = require('./LogFormat'); +let stringify = require('json-stringify-safe'); + +let stripColors = function (string) { + return string.replace(/\u001b[^m]+m/g, ''); +}; + +module.exports = class KbnLoggerJsonFormat extends LogFormat { + format(data) { + data.message = stripColors(data.message); + return stringify(data); + } +}; diff --git a/src/server/logging/LogFormatString.js b/src/server/logging/LogFormatString.js new file mode 100644 index 000000000000..d501e85535a5 --- /dev/null +++ b/src/server/logging/LogFormatString.js @@ -0,0 +1,36 @@ +'use strict'; + +let _ = require('lodash'); +let ansicolors = require('ansicolors'); +let moment = require('moment'); + +let LogFormat = require('./LogFormat'); + +let typeColors = { + log: 'blue', + req: 'green', + res: 'green', + ops: 'cyan', + err: 'red', + info: 'blue', + error: 'red', + fatal: 'magenta' +}; + +let color = _.memoize(function (name) { + return ansicolors[typeColors[name]] || _.identity; +}); + +module.exports = class KbnLoggerJsonFormat extends LogFormat { + format(data) { + let type = color(data.type)(_.padLeft(data.type, 6)); + let time = color('time')(moment(data.timestamp).format()); + let msg = data.error ? color('error')(data.error.stack) : color('message')(data.message); + + let tags = data.tags.reduce(function (s, t) { + return s + `[${ color(t)(t) }]`; + }, ''); + + return `${type}: [ ${time} ] ${tags} ${msg}`; + } +}; diff --git a/src/server/logging/LogReporter.js b/src/server/logging/LogReporter.js new file mode 100644 index 000000000000..0ee276e7da50 --- /dev/null +++ b/src/server/logging/LogReporter.js @@ -0,0 +1,35 @@ +'use strict'; + +let _ = require('lodash'); +let Squeeze = require('good-squeeze').Squeeze; +let writeStr = require('fs').createWriteStream; + +let LogFormatJson = require('./LogFormatJson'); +let LogFormatString = require('./LogFormatString'); + +module.exports = class KbnLogger { + constructor(events, config) { + this.squeeze = new Squeeze(events); + this.format = config.json ? new LogFormatJson() : new LogFormatString(); + + if (config.dest === 'stdout') { + this.dest = process.stdout; + } else { + this.dest = writeStr(config.dest, { + mode: 'a', + encoding: 'utf8' + }); + } + } + + init(readstream, emitter, callback) { + readstream + .pipe(this.squeeze) + .pipe(this.format) + .pipe(this.dest); + + emitter.on('stop', _.noop); + + callback(); + } +}; diff --git a/src/server/logging/good_reporters/_event_to_json.js b/src/server/logging/good_reporters/_event_to_json.js deleted file mode 100644 index 6047f479a607..000000000000 --- a/src/server/logging/good_reporters/_event_to_json.js +++ /dev/null @@ -1,133 +0,0 @@ -var moment = require('moment'); -var _ = require('lodash'); -var env = process.env.NODE_ENV || 'development'; -var numeral = require('numeral'); -var ansicolors = require('ansicolors'); -var stringify = require('json-stringify-safe'); -var querystring = require('querystring'); -var format = require('util').format; -var inspect = require('util').inspect; - -function serializeError(err) { - return { - message: err.message, - name: err.name, - stack: err.stack, - code: err.code, - signal: err.signal - }; -} - - -var levelColor = function (code) { - if (code < 299) { - return ansicolors.green(code); - } - if (code < 399) { - return ansicolors.yellow(code); - } - if (code < 499) { - return ansicolors.magenta(code); - } - return ansicolors.red(code); -}; - -function lookup(name) { - switch (name) { - case 'error': - return 'error'; - default: - return 'info'; - } -} - -module.exports = function (name, event) { - var data = { - '@timestamp': moment.utc(event.timestamp).format(), - level: lookup(event), - node_env: env, - tags: event.tags, - pid: event.pid - }; - if (name === 'response') { - _.defaults(data, _.pick(event, [ - 'method', - 'statusCode' - ])); - - data.req = { - url: event.path, - method: event.method, - headers: event.headers, - remoteAddress: event.source.remoteAddress, - userAgent: event.source.remoteAddress, - referer: event.source.referer - }; - - var contentLength = 0; - if (typeof event.responsePayload === 'object') { - contentLength = stringify(event.responsePayload).length; - } else { - contentLength = event.responsePayload.toString().length; - } - - data.res = { - statusCode: event.statusCode, - responseTime: event.responseTime, - contentLength: contentLength - }; - - var query = querystring.stringify(event.query); - if (query) data.req.url += '?' + query; - - - data.message = data.req.method.toUpperCase() + ' '; - data.message += data.req.url; - data.message += ' '; - data.message += levelColor(data.res.statusCode); - data.message += ' '; - data.message += ansicolors.brightBlack(data.res.responseTime + 'ms'); - data.message += ansicolors.brightBlack(' - ' + numeral(contentLength).format('0.0b')); - } - else if (name === 'ops') { - _.defaults(data, _.pick(event, [ - 'pid', - 'os', - 'proc', - 'load' - ])); - data.message = ansicolors.brightBlack('memory: '); - data.message += numeral(data.proc.mem.heapUsed).format('0.0b'); - data.message += ' '; - data.message += ansicolors.brightBlack('uptime: '); - data.message += numeral(data.proc.uptime).format('00:00:00'); - data.message += ' '; - data.message += ansicolors.brightBlack('load: ['); - data.message += data.os.load.map(function (val) { - return numeral(val).format('0.00'); - }).join(' '); - data.message += ansicolors.brightBlack(']'); - data.message += ' '; - data.message += ansicolors.brightBlack('delay: '); - data.message += numeral(data.proc.delay).format('0.000'); - } - else if (name === 'error') { - data.level = 'error'; - data.message = event.error.message; - data.error = serializeError(event.error); - data.url = event.url; - } - else if (event.data instanceof Error) { - data.level = _.contains(event.tags, 'fatal') ? 'fatal' : 'error'; - data.message = event.data.message; - data.error = serializeError(event.data); - } - else if (_.isPlainObject(event.data) && event.data.message) { - _.assign(data, event.data); - data.message = _.template(event.data.message)(event.data); - } - else { - data.message = _.isString(event.data) ? event.data : inspect(event.data); - } - return data; -}; diff --git a/src/server/logging/good_reporters/console.js b/src/server/logging/good_reporters/console.js deleted file mode 100644 index cb0c3572d7ca..000000000000 --- a/src/server/logging/good_reporters/console.js +++ /dev/null @@ -1,58 +0,0 @@ -var ansicolors = require('ansicolors'); -var eventToJson = require('./_event_to_json'); -var GoodReporter = require('good-reporter'); -var util = require('util'); -var moment = require('moment'); -var stringify = require('json-stringify-safe'); -var querystring = require('querystring'); -var numeral = require('numeral'); - -var colors = { - log: 'blue', - req: 'green', - res: 'green', - ops: 'cyan', - err: 'red', - info: 'blue', - error: 'red', - fatal: 'magenta' -}; - -function stripColors(string) { - return string.replace(/\u001b[^m]+m/g, ''); -} - -var Console = module.exports = function (events, options) { - this._json = options.json; - GoodReporter.call(this, events); -}; -util.inherits(Console, GoodReporter); - -Console.prototype.stop = function () { }; - -Console.prototype._report = function (name, data) { - data = eventToJson(name, data); - var nameCrayon = ansicolors[colors[name.substr(0, 3)]]; - var typeCrayon = ansicolors[colors[data.level]]; - var output; - if (this._json) { - data.message = stripColors(data.message); - output = stringify(data); - } else { - output = nameCrayon(name.substr(0, 3)); - output += ': '; - output += typeCrayon(data.level.toUpperCase()); - output += ' '; - output += '[ '; - output += ansicolors.brightBlack(moment(data.timestamp).format()); - output += ' ] '; - - if (data.error) { - output += ansicolors.red(data.error.stack); - } else { - output += data.message; - } - - } - console.log(output); -}; diff --git a/src/server/logging/good_reporters/file.js b/src/server/logging/good_reporters/file.js deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/server/logging/index.js b/src/server/logging/index.js index 2932f7f5d049..13afe6540e5c 100644 --- a/src/server/logging/index.js +++ b/src/server/logging/index.js @@ -1,37 +1,21 @@ -var Promise = require('bluebird'); -var good = require('good'); -var path = require('path'); -var join = path.join; -var Console = require('./good_reporters/console'); +var fromNode = require('bluebird').fromNode; +module.exports = function (kbnServer, server, config) { + if (!config.get('logging.kbnLogger')) return; -module.exports = function (kbnServer) { - var server = kbnServer.server; - - return new Promise(function (resolve, reject) { - var reporters = []; - var config = server.config(); - - // If we are not quite then add the console logger - var filters = {}; - if (!config.get('logging.quiet')) { - if (config.get('logging.console.ops') != null) filters.ops = config.get('logging.console.ops'); - if (config.get('logging.console.log') != null) filters.log = config.get('logging.console.log'); - if (config.get('logging.console.response') != null) filters.response = config.get('logging.console.response'); - if (config.get('logging.console.error') != null) filters.error = config.get('logging.console.error'); - } - reporters.push({ reporter: Console, args: [filters, { json: config.get('logging.console.json') } ] }); + return fromNode(function (cb) { server.register({ - register: good, + register: require('good'), options: { opsInterval: 5000, - logRequestHeaders: true, - logResponsePayload: true, - reporters: reporters + reporters: [ + { + reporter: require('./LogReporter'), + config: config.get('logging.kbnLoggerConfig'), + events: config.get('logging.kbnLoggerEvents'), + } + ] } - }, function (err) { - if (err) return reject(err); - resolve(server); - }); + }, cb); }); }; diff --git a/src/server/optimize/OptmzUiModules.js b/src/server/optimize/OptmzUiModules.js index 70fac53e9db7..9e92ae34a358 100644 --- a/src/server/optimize/OptmzUiModules.js +++ b/src/server/optimize/OptmzUiModules.js @@ -19,8 +19,8 @@ function OptmzUiExports(plugins) { // webpack loaders map loader configuration to regexps var loaders = this.loaders = [ - { test: /\.less$/, loader: 'style!css!less' }, - { test: /\.css$/, loader: 'style!css' }, + { test: /\.less$/, loader: 'style/url!file!less' }, + { test: /\.css$/, loader: 'style/url!file' }, { test: /\.html$/, loader: 'raw' }, { test: /\.(woff|woff2|png)(\?v=[0-9]\.[0-9]\.[0-9])?$/, diff --git a/src/server/optimize/index.js b/src/server/optimize/index.js index 419a105bd737..ca028803f6ea 100644 --- a/src/server/optimize/index.js +++ b/src/server/optimize/index.js @@ -32,7 +32,7 @@ module.exports = function (kbnServer, server, config) { status.green('Optimization complete'); }) .on('error', function (err) { - server.log(['fatal'], err); + server.log('fatal', err); status.red('Optimization failure! ' + err.message); }) .init(); diff --git a/src/server/plugins/Plugin.js b/src/server/plugins/Plugin.js index 433ad091d7bf..6bbd587355e6 100644 --- a/src/server/plugins/Plugin.js +++ b/src/server/plugins/Plugin.js @@ -1,86 +1,96 @@ -var _ = require('lodash'); -var inherits = require('util').inherits; -var Joi = require('joi'); -var Promise = require('bluebird'); -var join = require('path').join; +'use strict'; -function Plugin(kbnServer, path, package, opts) { - this.kbnServer = kbnServer; - this.package = package; - this.path = path; +let _ = require('lodash'); +let inherits = require('util').inherits; +let Joi = require('joi'); +let Promise = require('bluebird'); +let join = require('path').join; - this.id = opts.id || package.name; - this.uiExportSpecs = opts.uiExports || {}; - this.requiredIds = opts.require || []; - this.version = opts.version || package.version; - this.publicDir = _.get(opts, 'publicDir', join(path, 'public')); - this.externalInit = opts.init || _.noop; - this.getConfig = opts.config || _.noop; - this.init = _.once(this.init); -} +module.exports = class Plugin { + constructor(kbnServer, path, pkg, opts) { + this.kbnServer = kbnServer; + this.pkg = pkg; + this.path = path; -Plugin.scoped = function (kbnServer, path, package) { - function ScopedPlugin(opts) { - ScopedPlugin.super_.call(this, kbnServer, path, package, opts || {}); + this.id = opts.id || pkg.name; + this.uiExportSpecs = opts.uiExports || {}; + this.requiredIds = opts.require || []; + this.version = opts.version || pkg.version; + this.publicDir = _.get(opts, 'publicDir', join(path, 'public')); + this.externalInit = opts.init || _.noop; + this.getConfig = opts.config || _.noop; + this.init = _.once(this.init); } - inherits(ScopedPlugin, Plugin); - return ScopedPlugin; -}; -Plugin.prototype.init = function () { - var self = this; + static scoped(kbnServer, path, pkg) { + return class ScopedPlugin extends Plugin { + constructor(opts) { + super(kbnServer, path, pkg, opts || {}); + } + }; + } - var id = self.id; - var version = self.version; - var server = self.kbnServer.server; - var status = self.kbnServer.status; + init() { + let self = this; - var config = server.config(); - server.log(['plugin', 'init', 'debug'], { - message: 'initializing plugin <%= plugin.id %>', - plugin: self - }); + let id = self.id; + let version = self.version; + let server = self.kbnServer.server; + let status = self.kbnServer.status; - return Promise.try(function () { - return self.getConfig(Joi); - }) - .then(function (schema) { - if (schema) config.extendSchema(id, schema); - }) - .then(function () { - return status.decoratePlugin(self); - }) - .then(function () { - return self.kbnServer.uiExports.consumePlugin(self); - }) - .then(function () { + let basetags = ['plugin', id]; + let log = function (tags, data, timestamp) { - var register = function (server, options, next) { - - Promise.try(self.externalInit, [server, options], self).nodeify(next); }; - register.attributes = { name: id, version: version }; - - return Promise.fromNode(function (cb) { - server.register({ - register: register, - options: config.has(id) ? config.get(id) : null - }, cb); + let config = server.config(); + server.log(['plugin', 'debug', 'init'], { + message: 'Initializing plugin', + plugin: self }); - }) - .then(function () { - // Only change the plugin status to green if the - // intial status has not been updated - if (self.status.state === 'uninitialized') { - self.status.green('Ready'); - } - }); -}; + return Promise.try(function () { + return self.getConfig(Joi); + }) + .then(function (schema) { + if (schema) config.extendSchema(id, schema); + }) + .then(function () { + return status.decoratePlugin(self); + }) + .then(function () { + return self.kbnServer.uiExports.consumePlugin(self); + }) + .then(function () { -Plugin.prototype.toString = function () { - return `${this.id}@${this.version}`; -}; + let register = function (server, options, next) { + Promise.try(self.externalInit, [server, options], self).nodeify(next); + }; -module.exports = Plugin; + register.attributes = { name: id, version: version }; + + return Promise.fromNode(function (cb) { + server.register({ + register: register, + options: config.has(id) ? config.get(id) : null + }, cb); + }); + + }) + .then(function () { + // Only change the plugin status to green if the + // intial status has not been updated + if (self.status.state === 'uninitialized') { + self.status.green('Ready'); + } + }); + } + + toJSON() { + return this.pkg; + } + + toString() { + return `${this.id}@${this.version}`; + } +}; diff --git a/src/server/plugins/PluginApi.js b/src/server/plugins/PluginApi.js index cf5fb7b9ff63..5086b9279694 100644 --- a/src/server/plugins/PluginApi.js +++ b/src/server/plugins/PluginApi.js @@ -1,12 +1,14 @@ +'use strict'; + var Plugin = require('./Plugin'); var basename = require('path').basename; var join = require('path').join; -function PluginApi(kibana, pluginPath) { - this.rootDir = kibana.rootDir; - this.package = require(join(pluginPath, 'package.json')); - this.autoload = require('../ui/autoload'); - this.Plugin = Plugin.scoped(kibana, pluginPath, this.package); -} - -module.exports = PluginApi; +module.exports = class PluginApi { + constructor(kibana, pluginPath) { + this.rootDir = kibana.rootDir; + this.package = require(join(pluginPath, 'package.json')); + this.autoload = require('../ui/autoload'); + this.Plugin = Plugin.scoped(kibana, pluginPath, this.package); + } +}; diff --git a/src/server/status/Status.js b/src/server/status/Status.js index 6d77aba3f467..45c07f420eb3 100644 --- a/src/server/status/Status.js +++ b/src/server/status/Status.js @@ -10,8 +10,8 @@ function Status(name, server) { this.message = 'uninitialized'; this.on('change', function (current, previous) { - server.log(['status'], { - message: '[ <%= name %> ] Change status from <%= prev %> to <%= cur %> - <%= curMsg %>', + server.log(['plugin', name, 'status'], { + message: 'Change status from <%= prev %> to <%= cur %> - <%= curMsg %>', name: name, prev: previous.state, cur: current.state,