1.概述
Kibana的源代码位于Git目录中 src
下。这其中有两个目录kibana
与server
,
kibana
目录中是浏览器端源码server
目录中是服务器端源码
本文以下对server
中的内容作以简单说明。
server
目录结构如下图所示:
这些内容可以从功能上作以下分类:
- 启动引导:
bin
- 主程序:
index.js
- Express框架Server:
app.js
- Express框架路由模块:
routes
- 全局配置:
config
- 独立的工具模块:
lib
- 视图模板:
views
dev
目录中的内容是开发期关于Express的相关临时设置。
2.启动引导
Kibana其实有两种启动形式,一种是在开发期,直接在git目录下启动,另一种是在发布之后。不过,无论哪种形式,启动引导的结果都是执行src/server/index.js
这一主程序入口。两种形式的区别在于引导程序不同,以及相关环境变量配置不同。
2.1 开发期直接在git目录下启动引导
在git目录下直接执行grunt dev
即可启动kibana。关于该grunt任务的详细解释请见《Kibana源码工程结构与相关技术和工具》。这里仅关注该任务中的最后一个子任务——kibana_server
,任务定义如下:
module.exports = function (grunt) {
grunt.registerTask('kibana_server', function (keepalive) {
var done = this.async();
var config = require('../src/server/config');
config.quiet = !grunt.option('debug') && !grunt.option('verbose');
if (grunt.option('port')) {
config.port = config.kibana.port = grunt.option('port');
}
var server = require('../src/server');
server.start(function (err) {
if (err) return done(err);
grunt.log.ok('Server started on port', config.kibana.port);
if (keepalive !== 'keepalive') done();
});
});
};
可以看到:
- 配置项是从
src/server/config/index.js
中获取的 server
对象是从src/server/index.js
中获取的,server.start
方法的调用启动了kibana的主程序
2.2 发布版本中的启动引导程序
以windows系统发布版本为例。kibana的启动脚本是位于kibana/bin
目录下的kibana.bat
文件。其内容如下:
@echo off
SETLOCAL
set SCRIPT_DIR=%~dp0
for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI
set NODE=%DIR%\node\node.exe
set SERVER=%DIR%\src\bin\kibana.js
set NODE_ENV="production"
set CONFIG_PATH=%DIR%\config\kibana.yml
TITLE Kibana Server 4.1.3-snapshot
"%NODE%" "%SERVER%" %*
:finally
ENDLOCAL
可以看到,该脚本中指定了
- 设置引导程序为:
kibana/src/bin/kibana.js
- 配置:运行环境为
production
- 配置:配置文件为
kibana/config/kibana.yml
kibana/src/bin/kibana.js
的内容简要如下
#!/usr/bin/env node
//...
var env = (process.env.NODE_ENV) ? process.env.NODE_ENV : 'development';
//...
program.description('Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch.');
program.version(package.version);
program.option('-e, --elasticsearch <uri>', 'Elasticsearch instance');
program.option('-c, --config <path>', 'Path to the config file');
program.option('-p, --port <port>', 'The port to bind to', parseInt);
program.option('-q, --quiet', 'Turns off logging');
program.option('-H, --host <host>', 'The host to bind to');
program.option('-l, --log-file <path>', 'The file to log to');
program.option('--plugins <path>', 'Path to scan for plugins');
program.parse(process.argv);
// This needs to be set before the config is loaded. CONFIG_PATH is used to
// override the kibana.yml config path which gets read when the config/index.js
// is parsed for the first time.
if (program.config) {
process.env.CONFIG_PATH = program.config;
}
// This needs to be set before the config is loaded. PLUGINS_PATH is used to
// set the external plugins folder.
if (program.plugins) {
process.env.PLUGINS_FOLDER = program.plugins;
}
// Load the config
var config = require('../config');
if (program.elasticsearch) {
config.elasticsearch = program.elasticsearch;
}
if (program.port) {
config.port = program.port;
}
if (program.quiet) {
config.quiet = program.quiet;
}
if (program.logFile) {
config.log_file = program.logFile;
}
if (program.host) {
config.host = program.host;
}
// Load and start the server. This must happen after all the config changes
// have been made since the server also requires the config.
var server = require('../');
var logger = require('../lib/logger');
server.start(function (err) {
// If we get here then things have gone sideways and we need to give up.
if (err) {
logger.fatal({ err: err });
process.exit(1);
}
if (config.kibana.pid_file) {
return fs.writeFile(config.kibana.pid_file, process.pid, function (err) {
if (err) {
logger.fatal({ err: err }, 'Failed to write PID file to %s', config.kibana.pid_file);
process.exit(1);
}
});
}
});
其中内容主要是
- 判断了
process.env.NODE_ENV
环境变量 - 从
kibana/config/index.js
中获取config
对象(注意:kibana/config/index.js
文件就是源码目录下src/server/config/index.js
文件,文件路径的变化发生在构建任务grunt build
期间,详情参见《Kibana源码工程结构与相关技术和工具 》) - 处理命令行参数信息
- 从
kibana/src/index.js
中获取获取server
对象,并且调用server.start
方法启动程序(注意:该index.js
文件就是源码目录下的src/server/index.js
文件,文件路径的变化发生在构建任务grunt build
期间,详情参见《Kibana源码工程结构与相关技术和工具 》)
3.读取配置
配置读取在程序引导过程中,由一句如此的代码完成:
//git目录下直接启动
var config = require('../src/server/config');
//发布版本中,由kibana.js启动
var config = require('../config');
两种形式下,虽然路径有所改变,但其实执行的是同一份代码,以git源码目录为准是:src/server/config/index.js
。(文件路径改变在grunt build
时期)
在该脚本中,主要是区别了两种形势下,从不同的目录中加载kibana.yml
文件,相关代码如下:
//如果系统环境变量中设定了yml文件位置,则以设定为准,否则加载相同目录下的kibana.yml文件(发布版本中的系统启动脚本中设定了此变量)
5: var configPath = process.env.CONFIG_PATH || path.join(__dirname, 'kibana.yml');
6: var kibana = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'));
还区别了,两种形式下,Web静态资源目录的位置,相关代码如下:
30: // Check if the local public folder is present. This means we are running in
31: // the NPM module. If it's not there then we are running in the git root.
32: var public_folder = path.resolve(__dirname, '..', 'public');
33: if (!checkPath(public_folder)) public_folder = path.resolve(__dirname, '..', '..', 'kibana');
4.主程序 src/server/index.js
为外层的引导程序包装了server
对象,相关源码如下:
line 81:
module.exports = {
server: server,
start: function (cb) {
return initialization()
.then(start)
.then(function () {
cb && cb();
}, function (err) {
logger.error({ err: err });
if (cb) {
cb(err);
} else {
process.exit();
}
});
}
};
被包装的server对象是一个http服务对象,其创建过程如下:
/**
* Create HTTPS/HTTP server.
*/
//...
35: server = http.createServer(app);
//...
这其中前后还包括了关于应用日志的设置,还有关于是否开启https服务的判断。不过普通地创建http服务对象的代码如上句所示。那么app
对象是哪里来的呢?
5: var app = require('./app');
在同级目录的app.js
文件中定义了app
对象。
5.src/server/app.js与app对象
前文提到了app.js
文件中定义了app
对象,以供index.js
中创建http server对象。
app
对象是使用express
框架定义的服务对象。参考Express项目主页上更多信息。
从app.js
的源码中可以看到,其中为http server定义了:
- 日志中间件
- cookie中间件
- 认证中间件
- 请求报文内容解析中间件
- 等等……
并且,挂载了两个路由模块:
- routes/index.js:处理路径
/config
的GET
请求 - routes/proxy.js:处理路径
/elasticsearch
的请求
5.1 /elasticsearch
请求代理
routes/proxy.js
中的内容表明了,发送至/elasticsearch
的请求是如何被处理的。处理过程很清晰,如下所示:
第一步:收集请求数据:
// We need to capture the raw body before moving on
router.use(function (req, res, next) {
var chunks = [];
req.on('data', function (chunk) {
chunks.push(chunk);
});
req.on('end', function () {
req.rawBody = Buffer.concat(chunks);
next();
});
});
第二步:校验请求内容是否合法:
router.use(function (req, res, next) {
try {
validateRequest(req);
//console.info(req);
return next();
} catch (err) {
logger.error({ req: req }, err.message || 'Bad Request');
res.status(403).send(err.message || 'Bad Request');
}
});
第三步:准备转发请求内容:
var uri = _.defaults({}, router.proxyTarget);
// Add a slash to the end of the URL so resolve doesn't remove it.
var path = (/\/$/.test(uri.path)) ? uri.path : uri.path + '/';
path = url.resolve(path, '.' + req.url);
if (uri.auth) {
var auth = new Buffer(uri.auth);
req.headers.authorization = 'Basic ' + auth.toString('base64');
}
var options = {
agent: router.proxyAgent,
url: uri.protocol + '//' + uri.host + path,
method: req.method,
headers: _.defaults({}, req.headers),
strictSSL: config.kibana.verify_ssl,
timeout: config.request_timeout
};
options.headers['x-forward-for'] = req.connection.remoteAddress || req.socket.remoteAddress;
options.headers['x-forward-port'] = getPort(req);
options.headers['x-forward-proto'] = req.connection.pair ? 'https' : 'http';
// Only send the body if it's a PATCH, PUT, or POST
if (req.rawBody) {
options.headers['content-length'] = req.rawBody.length;
options.body = req.rawBody.toString('utf8');
} else {
options.headers['content-length'] = 0;
}
// To support the elasticsearch_preserve_host feature we need to change the
// host header to the target host header. I don't quite understand the value
// of this... but it's a feature we had before so I guess we are keeping it.
if (config.kibana.elasticsearch_preserve_host) {
options.headers.host = router.proxyTarget.host;
}
// Create the request and pipe the response
console.info('options//');
console.info(options);
第四步:向ES转发请求,并将请求响应直接对接到用户请求的响应上:
var esRequest = request(options);
esRequest.on('error', function (err) {
logger.error({ err: err });
var code = 502;
var body = { message: 'Bad Gateway' };
if (err.code === 'ECONNREFUSED') {
body.message = 'Unable to connect to Elasticsearch';
}
if (err.message === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
body.message = 'SSL handshake with Elasticsearch failed';
}
body.err = err.message;
if (!res.headersSent) res.status(code).json(body);
});
//将请求响应直接对接到用户请求响应上
esRequest.pipe(res);
});
以上,共四大步骤。
另外,这个脚本中的29行代码如下:
if (app.get('env') === 'development') {
require('./dev')(app);
}
该段意思在于,通过var app = express()
获得app
对象后,为了适应开发期的目录结构,刻意做了临时的设置变更,这些设置变更将在src/server/dev/index.js
中完成。
6.总结
总体上,kibana的服务端充当了ES查询代理角色。附加地做了配置管理和一些基本的认证和日志工作。