Authored by 陈峰

Merge branch 'feature/page-cache' of http://git.yoho.cn/OPENTECH/yoho-node-ci in…

…to feature/page-cache
/**
* 采集数据
*
* @class Collect
* @author shenzm<zhimin.shen@yoho.cn>
* @date 2016/10/12
*/
import ssh from 'ssh2';
import Rp from 'request-promise';
import Trace from '../logger/trace.js';
import config from '../../config/config.js';
import {
Server
} from '../models';
var tracer = new Trace({
host: config.influxdb.host,
port: config.influxdb.port
});
class Collect {
constructor(host, projectname) {
this.host = host;
this.projectname = projectname;
this.scriptRunning = false;
this.retry = {};
}
async collect() {
let self = this;
let server = await Server.findByHost(self.host);
self.server = {
host: server.host,
username: server.username,
password: server.password,
port: server.port
}
let obj = {
'total': 0,
'status': {}
};
Rp({
uri: `http://${self.host}:9615`,
json: true
}).then(function(data) {
var processes = data.processes || [];
processes.forEach(function(p) {
if (p.name === 'pm2-server-monit') {
var cpuUsg = p.pm2_env.axm_monitor['CPU usage'].value;
var freeMen = p.pm2_env.axm_monitor['Free memory'].value;
obj.cpuUsg = cpuUsg ? parseFloat(cpuUsg.replace('%', '')).toFixed(2) : '';
obj.freeMen = freeMen ? parseFloat(freeMen.replace('%', '')).toFixed(2) : '';
}
if (p.name === self.projectname) {
obj.total++;
if (!obj.status[p.pm2_env.status]) {
obj.status[p.pm2_env.status] = 1;
} else {
obj.status[p.pm2_env.status]++;
}
}
});
if (obj.cpuUsg === undefined) { // install server monit
const script = 'pm2 install pm2-server-monit';
self.execScript(script);
}
// add into influxDB todo
tracer.report('server_data', {
host: self.host
}, obj);
}).catch(function(err) {
const script = 'pm2 web';
self.execScript(script);
});
}
execScript(script) {
let self = this;
if (self.scriptRunning || self.retry[`${self.host}_${script}`] > 5) {
// 脚本执行中,或者重试次数大于5次以上时,不执行脚本
return;
}
let retryCount = self.retry[`${self.host}_${script}`] || 0;
self.retry[`${self.host}_${script}`] = ++retryCount;
self.scriptRunning = true;
let conn = new ssh.Client();
conn.on('ready', () => {
self._log(`>>>>host:[${self.host}] script[${script}]`);
conn.exec(`${script}`, (err, stream) => {
if (err) {
conn.end();
self._log(`host:[${self.host}] script:[${script}] exec fail error: ${err}`);
self.scriptRunning = false;
} else {
stream.stdout.on('data', (data) => {
//self._log(data.toString());
});
stream.stderr.on('data', (data) => {
//self._log(data.toString());
});
stream.on('exit', (code) => {
conn.end();
if (code === 0) {
self._log(`host:[${self.host}] script[${script}] exec success`);
} else {
self._log(`host:[${self.host}] script[${script}] exec fail`);
}
self.scriptRunning = false;
});
}
});
}).on('error', (err) => {
self._log(`connect error ${self.host} ${err}`);
self.scriptRunning = false;
}).connect(Object.assign(self.server, {
readyTimeout: 5000
}));
}
_log(msg) {
console.log(msg);
}
}
export default Collect;
\ No newline at end of file
... ...
... ... @@ -170,7 +170,6 @@ class Deploy {
}
_log(msg) {
console.log(msg)
ws.broadcast(`/deploy/${this.project._id}/log`, {
host: this.info.host,
msg: msg
... ...
/**
* 跟踪监控工具, 将监控数据写入influxdb
*
* @usage:
* <code>
* let trace = new Trace({
* host: '54.222.219.223',
* port: 4444
* });
*
* let testTrace = trace.trace('test_key'); // createOrChoose a measurement
*
* // testTrace(someTags, someFields);
* testTrace({ foo: 'bar', foobar: 'baz2'}, {value: 123, value2: 'aaa 123', value3: 1.3, value4: false});
*
* </code>
*
* @author: jiangfeng<jeff.jiang@yoho.cn>
* @date: 16/8/1
*/
... ... @@ -36,79 +22,13 @@ class Trace {
this.options = options;
}
/**
* create or choose a measurement to write point in.
* @param name {string} the measurement name
* @param options {object} some options. the protocol of influxdb connection.
* @returns {function()} a point write function
*/
trace(name, options) {
options = _.assign({
protocol: 'udp'
}, options);
let self = this;
if (options.protocol === 'udp') {
return (key, fields) => {
return self.udpTrace(name, options, key, fields);
};
} else if (options.protocol === 'http') {
return (key, fields) => {
return self.httpTrace(name, options, key, fields);
};
report(name, keys, data) {
if (this.options.appName) {
keys.appName = this.options.appName;
}
}
/**
* write point into influxdb by UDP
*
* @param name {string} the measurement name
* @param options {object}
* @param key {object} some keys of data
* @param fields {object} some fields of data
* @returns {Promise}
*/
udpTrace(name, options, key, fields) {
if (_.isArray(key)) {
key.forEach(p => {
let line = `${this._escape(name)},${this._makeLine(p)}`;
return this._updPostLine(line);
});
} else {
let line = `${this._escape(name)},${this._makeLine(key, false)} ${this._makeLine(fields, true)}`;
return this._updPostLine(line);
}
}
/**
* upd send.
* @param line {string} @see infulxdb's line protocol
* @returns {Promise}
* @private
*/
_updPostLine(line) {
let self = this;
return new Promise((resolve, reject) => {
let socket = dgram.createSocket("udp4");
let buff = new Buffer(line);
socket.send(buff, 0, buff.length, self.options.port, self.options.host, (err, rp) => {
socket.close();
console.log(rp);
if (err) {
reject(err);
} else {
resolve();
}
});
});
let line = `${this._escape(name)},${this._makeLine(keys, false)} ${this._makeLine(data, true)}`;
return this._send(line);
}
/**
... ... @@ -137,79 +57,51 @@ class Trace {
}
/**
* data escape with influxdb's line protocol.
*
* @param value {*}
* @param withQuote {boolean}
* @returns {*}
* upd send.
* @param line {string} @see infulxdb's line protocol
* @returns {Promise}
* @private
*/
_send(line) {
let self = this;
return new Promise((resolve, reject) => {
let socket = dgram.createSocket('udp4');
let buff = new Buffer(line);
socket.send(buff, 0, buff.length, self.options.port, self.options.host, (err) => {
socket.close();
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
_escape(value, withQuote) {
if (_.isString(value)) {
value = _.replace(value, /,/g, '\\,');
value = _.replace(value, /=/g, '\\=');
if (withQuote) {
value = '"' + value + '"';
} else {
value = _.replace(value, /,/g, '\\,');
value = _.replace(value, /=/g, '\\=');
value = _.replace(value, /\s/g, '\\ ');
}
} else if (_.isInteger(value)) {
if (withQuote) {
value = value + 'i';
}
} else if (_.isObject(value)) {
value = '"' + _.replace(JSON.stringify(value), /"/g, '\\"') + '"';
} else if (_.isNull(value) || _.isNil(value)) {
value = '""';
}
return value;
}
/**
* write point into influxdb by HTTP. use the open source node-influx module.
* @see https://github.com/node-influx/node-influx
*
* @param name {string} the measurement name
* @param options {object}
* @param key {object} some keys of data
* @param fields {object} some fields of data
* @returns {Promise}
*/
httpTrace(name, options, key, fields) {
let client = this.getHttpClient();
return new Promise((resolve, reject) => {
if (_.isArray(key)) {
client.writePoints(name, key, options, (err, rp) => {
if (err) {
reject(err);
} else {
resolve(rp)
}
});
} else {
client.writePoint(name, fields, key, options, (err, rp) => {
if (err) {
reject(err);
} else {
resolve(rp)
}
});
}
});
}
/**
* the singleton http client.
*
* @returns {*}
*/
getHttpClient() {
if (!this.httpClient) {
this.httpClient = influx(this.options);
}
return this.httpClient;
}
}
module.exports = Trace;
module.exports = Trace;
\ No newline at end of file
... ...
'use strict';
import Collect from '../../ci/collect_data';
import {
Project
} from '../../models';
export default {
async collect(ctx) {
let projects,
servers = {};
setInterval(async() => {
if (!projects) projects = await Project.findAll();
if (!projects || !projects.length) {
return;
}
projects.forEach(async(p) => {
const hosts = p.deploy['production'].target;
hosts.forEach((host) => {
if (!servers[host]) {
servers[host] = new Collect(host, p.name);
}
servers[host].collect();
});
});
}, 5000);
}
};
\ No newline at end of file
... ...
'use strict';
import Router from 'koa-router';
import moment from 'moment';
import _ from 'lodash';
import {Degrade, DegradeServer} from '../../models';
import getter from '../../zookeeper/getter';
import setter from '../../zookeeper/setter';
import tester from '../../zookeeper/tester';
const router = new Router();
const ctl = {
async getServer() {
let server = await DegradeServer.findAll({});
server = _.last(server);
if (server) {
return `${server.ip}:${server.port}`;
}
},
async index (ctx) {
let serverPath = await ctl.getServer();
let serverSplit = serverPath ? serverPath.split(':') : ['', ''];
await ctx.render('action/degrade', {
ip: serverSplit[0],
port: serverSplit[1]
});
},
async connect(ctx) {
let {ip, port} = ctx.query;
let server = `${ip}:${port}`;
let connected = await tester(server);
// connecting test
if (!connected) {
ctx.body = `<p class="connect-err">
<i class="fa fa-wheelchair" aria-hidden="true"></i>
Sorry, I can not connect to <span class="server-name">${server}</span>.Please check whether your ip/port is correct or the zookeeper server is running
</p>`;
return;
}
let degrades = await Degrade.findAll();
for(let i of degrades) {
i.checked = await getter(server, i.path);
}
let pc = _.filter(degrades, o => _.startsWith(o.path, '/pc'));
let wap = _.filter(degrades, o => _.startsWith(o.path, '/wap'));
await ctx.render('action/degrade_list', {
layout: false,
pc: pc,
wap: wap
});
},
async server(ctx) {
let ip = ctx.request.body.ip;
let port = ctx.request.body.port;
let serverCount = await DegradeServer.count({});
// keep one server
if (serverCount) {
let serverConfig = await DegradeServer.findAll({});
let id = _.last(serverConfig)._id; // get the latest item
await DegradeServer.update({
_id: id
}, {
$set: {
ip: ip,
port: port
}
});
} else {
await DegradeServer.insert({
ip: ip,
port: port
});
}
ctx.body = {
code: 200,
message: `${serverCount ? 'update' : 'new'} server success`
};
},
async setter(ctx) {
let {checked, id} = ctx.query;
let theDegrade = await Degrade.findById(id);
let path = theDegrade.path;
let serverPath = await ctl.getServer();
await setter(serverPath, path, checked.toString());
ctx.body = {
code: 200,
message: 'update success'
};
}
};
router.get('/', ctl.index);
router.post('/server', ctl.server);
router.get('/connect', ctl.connect);
router.get('/setter', ctl.setter);
export default router;
\ No newline at end of file
'use strict';
import Router from 'koa-router';
import moment from 'moment';
import _ from 'lodash';
import process from 'process';
import {Degrade, DegradeServer} from '../../models';
import getter from '../../zookeeper/getter';
import setter from '../../zookeeper/setter';
import tester from '../../zookeeper/tester';
const router = new Router();
const ctl = {
async index (ctx) {
let qcloudServer = await DegradeServer.findOne({
type: 'qcloud'
});
if (!qcloudServer) {
qcloudServer = {};
}
let awsServer = await DegradeServer.findOne({
type: 'aws'
});
if (!awsServer) {
awsServer = {};
}
await ctx.render('action/degrade', {
qCloudConfig: {
ip: qcloudServer.ip,
port: qcloudServer.port
},
awsConfig: {
ip: awsServer.ip,
port: awsServer.port
}
});
},
async connect(ctx) {
let {ip, port} = ctx.query;
let server = `${ip}:${port}`;
let connected = await tester(server);
// connecting test
if (!connected) {
ctx.body = `<p class="connect-err">
<i class="fa fa-wheelchair" aria-hidden="true"></i>
Sorry, I can not connect to <span class="server-name">${server}</span>.Please check whether your ip/port is correct or the zookeeper server is running
</p>`;
return;
}
let degrades = await Degrade.findAll();
for(let i of degrades) {
// 从zookeeper读取配置信息,memcached只做PHP读取使用
i.checked = await getter(server, i.path);
}
let pc = _.filter(degrades, o => _.startsWith(o.path, '/pc'));
let wap = _.filter(degrades, o => _.startsWith(o.path, '/wap'));
await ctx.render('action/degrade_list', {
layout: false,
pc: pc,
wap: wap
});
},
async server(ctx) {
let {ip, port, type} = ctx.request.body;
let serverCount = await DegradeServer.count({
type: type
});
// keep one server
if (serverCount) {
let serverConfig = await DegradeServer.findOne({
type: type
});
let id = serverConfig._id; // get the latest item
await DegradeServer.update({
_id: id
}, {
$set: {
ip: ip,
port: port
}
});
} else {
await DegradeServer.insert({
ip: ip,
port: port,
type: type
});
}
ctx.body = {
code: 200,
message: `${serverCount ? 'update' : 'new'} ${type} server success`
};
},
async setter(ctx) {
let {checked, id, type} = ctx.query;
let theDegrade = await Degrade.findById(id);
let path = theDegrade.path;
let server = await await DegradeServer.findOne({
type: type
});
let result = await setter(`${server.ip}:${server.port}`, path, checked.toString());
// result结果以zookeeper写为准
if (result) {
ctx.body = {
code: 200,
message: 'update success'
};
} else {
ctx.body = {
code: 500,
message: 'update fail,Please retry'
}
}
}
};
router.get('/', ctl.index);
router.post('/server', ctl.server);
router.get('/connect', ctl.connect);
router.get('/setter', ctl.setter);
export default router;
... ...
... ... @@ -61,6 +61,9 @@ const p = {
deploy.env = env;
deploy.name = envs[env];
if (typeof deploy.target === 'string') {
deploy.target = [deploy.target];
}
let promises = deploy.target.map((host) => {
console.log('read host :' + host);
... ... @@ -220,6 +223,10 @@ const p = {
let project = await Project.findById(building.projectId);
let targets = project.deploy[building.env].target;
if (typeof targets === 'string') {
targets = [targets];
}
targets.forEach(async(host) => {
let doc = await DeployInfo.findOne({
... ... @@ -394,7 +401,11 @@ const p = {
return;
}
const hosts = project.deploy[env].target;
let hosts = project.deploy[env].target;
if (typeof hosts === 'string') {
hosts = [hosts];
}
hosts.forEach((host) => {
var obj = {
'total': 0,
... ...
... ... @@ -10,6 +10,7 @@ import Koa from 'koa';
import hbs from '../../middleware/yoho-koa-hbs';
import helpers from '../../lib/helpers';
import routers from './routers'
import collectData from './actions/collect_data';
const app = new Koa();
... ... @@ -29,6 +30,9 @@ const mastersUrl = [
'/users'
];
// 服务器监控数据采集
collectData.collect();
app.use(async(ctx, next) => {
ctx.locals = {
title: 'Yoho Node.js 持续集成平台'
... ... @@ -38,7 +42,7 @@ app.use(async(ctx, next) => {
ctx.locals.layout = null;
}
if (ctx.session && ctx.session.user ) {
if (ctx.session && ctx.session.user) {
ctx.locals.is_master = ctx.session.user.role === '1000';
ctx.locals.current_user = ctx.session.user;
}
... ...
... ... @@ -35,136 +35,161 @@
float: right;
cursor: pointer;
}
.config-panel .server-port {
width: 100px;
}
.config-panel .form-inline {
padding: 20px 0;
border-bottom: 1px solid #d9edf7;
}
</style>
<div class="degrade-page">
<div class="panel panel-info">
<div class="panel-heading">zookeeper server</div>
<div class="panel-heading">zookeeper configure</div>
<div class="panel-body">
<div class="form-inline">
<div class="current-server{{#unless ip}} hide{{/unless}}">
当前server地址:
<span id="server-in-use" class="server-in-use" data-ip="{{ip}}" data-port="{{port}}">{{ip}}:{{port}}</span>
<span id="server-modify-btn" class="ip-port-modify-btn">修改</span>
</div>
<div class="server-editor{{#if ip}} hide{{/if}}">
<div class="form-group">
<label for="server-ip">Server IP</label>
<input id="server-ip" class="form-control" type="text" placeholder="input server ip" value="{{ip}}">
<div class="row">
<div class="col-md-6 config-panel" data-type="qcloud">
<div class="panel panel-success">
<div class="panel-heading">
腾讯云
<span class="refresh">
<i class="glyphicon glyphicon-refresh"></i>
</span>
</div>
<div class="panel-body">
{{# qCloudConfig}}
<div class="form-inline">
<div class="current-server{{#unless ip}} hide{{/unless}}">
当前server地址:
<span class="server-in-use" data-ip="{{ip}}" data-port="{{port}}">{{ip}}:{{port}}</span>
<span class="ip-port-modify-btn">修改</span>
</div>
<div class="server-editor{{#if ip}} hide{{/if}}">
<div class="form-group">
<label>Server IP</label>
<input class="form-control server-ip" type="text" value="{{ip}}">
</div>
<div class="form-group">
<label for="server-port">Server Port</label>
<input class="form-control server-port" type="text" value="{{port}}">
</div>
<span class="btn btn-default server-sure">保存</span>
</div>
</div>
{{/ qCloudConfig}}
<div class="panel-body list-container">
Waitting for connecting to server...
</div>
</div>
</div>
<div class="form-group">
<label for="server-port">Server Port</label>
<input id="server-port" class="form-control" type="text" placeholder="input server port" value="{{port}}">
</div>
<div class="col-md-6 config-panel" data-type="aws">
<div class="panel panel-warning">
<div class="panel-heading">
AWS
<span class="refresh">
<i class="glyphicon glyphicon-refresh"></i>
</span>
</div>
<div class="panel-body">
{{# awsConfig}}
<div class="form-inline">
<div class="current-server{{#unless ip}} hide{{/unless}}">
当前server地址:
<span class="server-in-use" data-ip="{{ip}}" data-port="{{port}}">{{ip}}:{{port}}</span>
<span class="ip-port-modify-btn">修改</span>
</div>
<div class="server-editor{{#if ip}} hide{{/if}}">
<div class="form-group">
<label>Server IP</label>
<input class="form-control server-ip" type="text" value="{{ip}}">
</div>
<div class="form-group">
<label>Server Port</label>
<input class="form-control server-port" type="text" value="{{port}}">
</div>
<span class="btn btn-default server-sure">保存</span>
</div>
</div>
{{/ awsConfig}}
<div class="panel-body list-container">
Waitting for connecting to server...
</div>
</div>
</div>
<span id="server-sure" class="btn btn-default">保存</span>
<span id="err-tip" class="err-tip hide">
<i class="glyphicon glyphicon-remove-sign"></i>
小哥,填错了吧
</span>
</div>
</div>
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
degrade point
<span id="refresh-connection" class="refresh">
<i class="glyphicon glyphicon-refresh"></i>
</span>
</div>
<div id="list-container" class="panel-body">
Waitting for connecting to server...
</div>
</div>
</div>
<script>
$(function() {
var $ip = $('#server-ip');
var $port = $('#server-port');
var $tip = $('#err-tip');
var $listContainer = $('#list-container');
function validateIP(ip) {
if (ip === 'localhost' ||
/^(\d)+\.(\d)+\.(\d)+\.(\d)+$/.test(ip))
{
$ip.closest('.form-group').removeClass('has-error');
return true;
}
$ip.closest('.form-group').addClass('has-error');
return false;
}
function validatePort(port) {
if (/^[1-9]\d*$/.test(port) && +port >= 1 && +port <= 65535) {
$port.closest('.form-group').removeClass('has-error');
return true;
}
$port.closest('.form-group').addClass('has-error');
return false;
}
$('.ip-port-modify-btn').click(function() {
var $form = $(this).closest('.form-inline');
$('#server-modify-btn').click(function() {
$('.server-editor').removeClass('hide');
$('.current-server').addClass('hide');
$('.server-editor', $form).removeClass('hide');
$('.current-server', $form).addClass('hide');
});
// server
$('#server-sure').click(function() {
var ip = $.trim($ip.val());
var port = $.trim($port.val());
$('.server-sure').click(function() {
var $panel = $(this).closest('.config-panel');
if (!validateIP(ip) || !validatePort(port)) {
$tip.removeClass('hide');
return;
}
var $ip = $('.server-ip', $panel);
var $port = $('.server-port', $panel);
$tip.addClass('hide');
var ip = $.trim($ip.val());
var port = $.trim($port.val());
$.ajax({
type: 'POST',
url: '/degrade/server',
data: {
ip: ip,
port: port
port: port,
type: $panel.data('type')
}
}).then(function(data) {
if (data.code === 200) {
//$.pjax.reload('#pjax-container');
$('.server-editor').addClass('hide');
$('.current-server').removeClass('hide');
$('.server-editor', $panel).addClass('hide');
$('.current-server', $panel).removeClass('hide');
$('#server-in-use').text(ip + ':' + port).data('ip', ip).data('port', port);
$('.server-in-use', $panel).text(ip + ':' + port).data('ip', ip).data('port', port);
$('#list-container').html('waiting for connecting server...');
$('.list-container', $panel).html('waiting for connecting server...');
}
});
});
$listContainer.on('click', '#degrade-tab li', function() {
$('.list-container').on('click', '.degrade-tab li', function() {
var $this = $(this);
var $panel = $this.closest('.config-panel');
if ($this.hasClass('active')) {
return;
}
$('li', $('#degrade-tab')).toggleClass('active');
$this.parent().children('li').toggleClass('active');
var index = $this.index();
if (index === 0) {
//PC active
$('.pc-degrade').removeClass('hide');
$('.wap-degrade').addClass('hide');
$('.pc-degrade', $panel).removeClass('hide');
$('.wap-degrade', $panel).addClass('hide');
} else {
// wap active
$('.wap-degrade').removeClass('hide');
$('.pc-degrade').addClass('hide');
$('.wap-degrade', $panel).removeClass('hide');
$('.pc-degrade', $panel).addClass('hide');
}
}).on('change', '.degrade-content input[type="checkbox"]', function() {
var $checkbox = $(this),
... ... @@ -180,19 +205,26 @@
checked: checked,
id: id
}
}).then(function(data) {
if (data.code !== 200) {
$checkbox.prop('checked', !checked);
alert(data.message);
}
});
});
$('#refresh-connection').click(function() {
$('.refresh').click(function() {
var $panel = $(this).closest('.config-panel');
$.ajax({
type: 'GET',
url: '/degrade/connect',
data: {
ip: $('#server-in-use').data('ip'),
port: $('#server-in-use').data('port')
ip: $('.server-in-use', $panel).data('ip'),
port: $('.server-in-use', $panel).data('port')
}
}).then(function(data) {
$('#list-container').html(data);
$('.list-container', $panel).html(data);
});
})
})
... ...
'usu strict';
import _ from 'lodash';
import zookeeper from 'node-zookeeper-client';
module.exports = (server, path, val) => new Promise((resolve, reject) => {
const client = zookeeper.createClient(server);
client.once('connected', function () {
client.setData(path, new Buffer(val.toString()), function(err, data, stat) {
console.log('path %s data change to', path, val);
resolve();
client.close();
});
});
client.connect();
});
'usu strict';
import _ from 'lodash';
import zookeeper from 'node-zookeeper-client';
module.exports = (server, path, val) => new Promise((resolve, reject) => {
const client = zookeeper.createClient(server);
client.once('connected', function () {
client.setData(path, new Buffer(val.toString()), function(err, data, stat) {
if (err) {
console.log('update path %s data error');
resolve();
} else {
console.log('path %s data change to', path, val);
resolve(true);
}
client.close();
});
});
client.connect();
});
... ...
... ... @@ -5,15 +5,19 @@ import path from 'path';
const env = process.env.NODE_ENV || 'development';
const defaults = {
port: 9000,
buildDir: path.normalize(__dirname + '/../packages/'),
dbDir: path.normalize(__dirname + '/../db')
port: 9000,
buildDir: path.normalize(__dirname + '/../packages/'),
dbDir: path.normalize(__dirname + '/../db'),
influxdb: {
host: '54.222.219.223',
port: 4444
}
};
const specific = {
development: {},
test: {},
production: {}
development: {},
test: {},
production: {},
};
export default Object.assign(defaults, specific[env]);
\ No newline at end of file
... ...