diff --git a/package.json b/package.json index 70417f63d7446..dad26550ace42 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "d3": "3.5.6", "elasticsearch": "10.1.2", "elasticsearch-browser": "10.1.2", + "even-better": "7.0.2", "expiry-js": "0.1.7", "exports-loader": "0.6.2", "expose-loader": "0.7.0", @@ -104,7 +105,6 @@ "file-loader": "0.8.4", "font-awesome": "4.4.0", "glob-all": "3.0.1", - "good": "6.3.0", "good-squeeze": "2.1.0", "gridster": "0.5.6", "hapi": "8.8.1", @@ -153,6 +153,7 @@ "chokidar": "1.4.3", "eslint": "1.10.3", "eslint-plugin-mocha": "1.1.0", + "event-stream": "3.3.2", "expect.js": "0.3.1", "faker": "1.1.0", "grunt": "0.4.5", diff --git a/src/cli/serve/__tests__/fixtures/reload_logging_config/kibana.test.yml b/src/cli/serve/__tests__/fixtures/reload_logging_config/kibana.test.yml new file mode 100644 index 0000000000000..22c5e93375c5f --- /dev/null +++ b/src/cli/serve/__tests__/fixtures/reload_logging_config/kibana.test.yml @@ -0,0 +1,6 @@ +server: + port: 8274 +logging: + json: true +optimize: + enabled: false diff --git a/src/cli/serve/__tests__/reload_logging_config.js b/src/cli/serve/__tests__/reload_logging_config.js new file mode 100644 index 0000000000000..e53e646974f14 --- /dev/null +++ b/src/cli/serve/__tests__/reload_logging_config.js @@ -0,0 +1,88 @@ +import { spawn } from 'child_process'; +import { writeFileSync, readFile } from 'fs'; +import { relative, resolve } from 'path'; +import { safeDump } from 'js-yaml'; +import es from 'event-stream'; +import readYamlConfig from '../read_yaml_config'; +import expect from 'expect.js'; +const testConfigFile = follow(`fixtures/reload_logging_config/kibana.test.yml`); +const cli = follow(`../../../../bin/kibana`); + +function follow(file) { + return relative(process.cwd(), resolve(__dirname, file)); +} + +function setLoggingJson(enabled) { + const conf = readYamlConfig(testConfigFile); + conf.logging = conf.logging || {}; + conf.logging.json = enabled; + const yaml = safeDump(conf); + writeFileSync(testConfigFile, yaml); + return conf; +} + +describe(`Server logging configuration`, function () { + it(`should be reloadable via SIGHUP process signaling`, function (done) { + let asserted = false; + let json = Infinity; + const conf = setLoggingJson(true); + const child = spawn(cli, [`--config`, testConfigFile]); + + child.on('error', err => { + done(new Error(`error in child process while attempting to reload config. +${err.stack || err.message || err}`)); + }); + + child.on('exit', code => { + expect(asserted).to.eql(true); + expect(code === null || code === 0).to.eql(true); + done(); + }); + + child.stdout + .pipe(es.split()) + .pipe(es.mapSync(function (line) { + if (!line) { + return line; // ignore empty lines + } + if (json--) { + expect(parseJsonLogLine).withArgs(line).to.not.throwError(); + } else { + expectPlainTextLogLine(line); + } + })); + + function parseJsonLogLine(line) { + try { + const data = JSON.parse(line); + const listening = data.tags.indexOf(`listening`) !== -1; + if (listening) { + switchToPlainTextLog(); + } + } catch (err) { + expect(`Error parsing log line as JSON\n +${err.stack || err.message || err}`).to.eql(true); + } + } + + function switchToPlainTextLog() { + json = 2; // ignore both "reloading" messages + setLoggingJson(false); + child.kill(`SIGHUP`); // reload logging config + } + + function expectPlainTextLogLine(line) { + // assert + const tags = `[\u001b[32minfo\u001b[39m][\u001b[36mconfig\u001b[39m]`; + const status = `Reloaded logging configuration due to SIGHUP.`; + const expected = `${tags} ${status}`; + const actual = line.slice(-expected.length); + expect(actual).to.eql(expected); + + // cleanup + asserted = true; + setLoggingJson(true); + child.kill(); + } + }); +}); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 736e74982e062..9b73b11720f28 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -2,11 +2,8 @@ import _ from 'lodash'; import { statSync } from 'fs'; import { isWorker } from 'cluster'; import { resolve } from 'path'; - -import readYamlConfig from './read_yaml_config'; import { fromRoot } from '../../utils'; - -const cwd = process.cwd(); +import readYamlConfig from './read_yaml_config'; let canCluster; try { @@ -28,7 +25,7 @@ const configPathCollector = pathCollector(); const pluginDirCollector = pathCollector(); const pluginPathCollector = pathCollector(); -function initServerSettings(opts, extraCliOptions) { +function readServerSettings(opts, extraCliOptions) { const settings = readYamlConfig(opts.config); const set = _.partial(_.set, settings); const get = _.partial(_.get, settings); @@ -128,7 +125,8 @@ module.exports = function (program) { } } - const settings = initServerSettings(opts, this.getUnknownOptions()); + const getCurrentSettings = () => readServerSettings(opts, this.getUnknownOptions()); + const settings = getCurrentSettings(); if (canCluster && opts.dev && !isWorker) { // stop processing the action and handoff to cluster manager @@ -156,6 +154,13 @@ module.exports = function (program) { process.exit(1); // eslint-disable-line no-process-exit } + process.on('SIGHUP', function reloadConfig() { + const settings = getCurrentSettings(); + kbnServer.server.log(['info', 'config'], 'Reloading logging configuration due to SIGHUP.'); + kbnServer.applyLoggingConfiguration(settings); + kbnServer.server.log(['info', 'config'], 'Reloaded logging configuration due to SIGHUP.'); + }); + return kbnServer; }); }; diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 943914b391277..8ad9d9c0709d8 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -3,6 +3,8 @@ import { constant, once, compact, flatten } from 'lodash'; import { promisify, resolve, fromNode } from 'bluebird'; import { isWorker } from 'cluster'; import { fromRoot, pkg } from '../utils'; +import Config from './config/config'; +import loggingConfiguration from './logging/configuration'; let rootDir = fromRoot('.'); @@ -107,4 +109,16 @@ module.exports = class KbnServer { } }); } + + applyLoggingConfiguration(settings) { + const config = Config.withDefaultSchema(settings); + const loggingOptions = loggingConfiguration(config); + const subset = { + ops: config.get('ops'), + logging: config.get('logging') + }; + const plain = JSON.stringify(subset, null, 2); + this.server.log(['info', 'config'], 'New logging configuration:\n' + plain); + this.server.plugins['even-better'].monitor.reconfigure(loggingOptions); + } }; diff --git a/src/server/logging/configuration.js b/src/server/logging/configuration.js new file mode 100644 index 0000000000000..03fa0d6164a9d --- /dev/null +++ b/src/server/logging/configuration.js @@ -0,0 +1,61 @@ +import _ from 'lodash'; +import logReporter from './log_reporter'; + +export default function loggingConfiguration(config) { + let events = config.get('logging.events'); + + if (config.get('logging.silent')) { + _.defaults(events, {}); + } + else if (config.get('logging.quiet')) { + _.defaults(events, { + log: ['listening', 'error', 'fatal'], + request: ['error'], + error: '*' + }); + } + else if (config.get('logging.verbose')) { + _.defaults(events, { + log: '*', + ops: '*', + request: '*', + response: '*', + error: '*' + }); + } + else { + _.defaults(events, { + log: ['info', 'warning', 'error', 'fatal'], + response: config.get('logging.json') ? '*' : '!', + request: ['info', 'warning', 'error', 'fatal'], + error: '*' + }); + } + + const options = { + opsInterval: config.get('ops.interval'), + requestHeaders: true, + requestPayload: true, + reporters: [ + { + reporter: logReporter, + config: { + json: config.get('logging.json'), + dest: config.get('logging.dest'), + // I'm adding the default here because if you add another filter + // using the commandline it will remove authorization. I want users + // to have to explicitly set --logging.filter.authorization=none to + // have it show up int he logs. + filter: _.defaults(config.get('logging.filter'), { + authorization: 'remove' + }) + }, + events: _.transform(events, function (filtered, val, key) { + // provide a string compatible way to remove events + if (val !== '!') filtered[key] = val; + }, {}) + } + ] + }; + return options; +} diff --git a/src/server/logging/index.js b/src/server/logging/index.js index 3279b1aca0aa6..8f4640aadfe03 100644 --- a/src/server/logging/index.js +++ b/src/server/logging/index.js @@ -1,68 +1,15 @@ -import _ from 'lodash'; import { fromNode } from 'bluebird'; +import evenBetter from 'even-better'; +import loggingConfiguration from './configuration'; -module.exports = function (kbnServer, server, config) { +export default function (kbnServer, server, config) { // prevent relying on kbnServer so this can be used with other hapi servers kbnServer = null; return fromNode(function (cb) { - let events = config.get('logging.events'); - - if (config.get('logging.silent')) { - _.defaults(events, {}); - } - else if (config.get('logging.quiet')) { - _.defaults(events, { - log: ['listening', 'error', 'fatal'], - request: ['error'], - error: '*' - }); - } - else if (config.get('logging.verbose')) { - _.defaults(events, { - log: '*', - ops: '*', - request: '*', - response: '*', - error: '*' - }); - } - else { - _.defaults(events, { - log: ['info', 'warning', 'error', 'fatal'], - response: config.get('logging.json') ? '*' : '!', - request: ['info', 'warning', 'error', 'fatal'], - error: '*' - }); - } - server.register({ - register: require('good'), - options: { - opsInterval: config.get('ops.interval'), - requestHeaders: true, - requestPayload: true, - reporters: [ - { - reporter: require('./log_reporter'), - config: { - json: config.get('logging.json'), - dest: config.get('logging.dest'), - // I'm adding the default here because if you add another filter - // using the commandline it will remove authorization. I want users - // to have to explicitly set --logging.filter.authorization=none to - // have it show up int he logs. - filter: _.defaults(config.get('logging.filter'), { - authorization: 'remove' - }) - }, - events: _.transform(events, function (filtered, val, key) { - // provide a string compatible way to remove events - if (val !== '!') filtered[key] = val; - }, {}) - } - ] - } + register: evenBetter, + options: loggingConfiguration(config) }, cb); }); }; diff --git a/src/server/logging/log_format_string.js b/src/server/logging/log_format_string.js index 544c15691bb9b..189738a1b4c52 100644 --- a/src/server/logging/log_format_string.js +++ b/src/server/logging/log_format_string.js @@ -19,6 +19,7 @@ let typeColors = { req: 'green', res: 'green', ops: 'cyan', + config: 'cyan', err: 'red', info: 'green', error: 'red', diff --git a/src/server/status/index.js b/src/server/status/index.js index f73b09209f1f4..597401df7e478 100644 --- a/src/server/status/index.js +++ b/src/server/status/index.js @@ -6,7 +6,7 @@ import { join } from 'path'; export default function (kbnServer, server, config) { kbnServer.status = new ServerStatus(kbnServer.server); - if (server.plugins.good) { + if (server.plugins['even-better']) { kbnServer.mixin(require('./metrics')); } diff --git a/src/server/status/metrics.js b/src/server/status/metrics.js index b5cfd6784a0f9..28b99011bc988 100644 --- a/src/server/status/metrics.js +++ b/src/server/status/metrics.js @@ -5,7 +5,7 @@ module.exports = function (kbnServer, server, config) { kbnServer.metrics = new Samples(12); - server.plugins.good.monitor.on('ops', function (event) { + server.plugins['even-better'].monitor.on('ops', function (event) { let now = Date.now(); let secSinceLast = (now - lastReport) / 1000; lastReport = now;