Added live stats

This commit is contained in:
Matt 2014-03-19 14:24:29 -06:00
parent 4c7e41b71b
commit ca60e5e7f4
9 changed files with 246 additions and 173 deletions

View File

@ -20,7 +20,8 @@
"enabled": true,
"siteTitle": "Cryppit",
"port": 80,
"statUpdateInterval": 5
"statUpdateInterval": 5,
"hashrateWindow": 600
},
"proxy": {
"enabled": false,

View File

@ -174,7 +174,7 @@ var startPaymentProcessor = function(poolConfigs){
worker.on('exit', function(code, signal){
logError('paymentProcessor', 'system', 'Payment processor died, spawning replacement...');
setTimeout(function(){
startPaymentProcessor.apply(null, arguments);
startPaymentProcessor(poolConfigs);
}, 2000);
});
};
@ -192,7 +192,7 @@ var startWebsite = function(portalConfig, poolConfigs){
worker.on('exit', function(code, signal){
logError('website', 'system', 'Website process died, spawning replacement...');
setTimeout(function(){
startWebsite.apply(null, arguments);
startWebsite(portalConfig, poolConfigs);
}, 2000);
});
};

View File

@ -1,116 +0,0 @@
var redis = require('redis');
var async = require('async');
var os = require('os');
module.exports = function(logger, poolConfigs){
var _this = this;
var redisClients = [];
Object.keys(poolConfigs).forEach(function(coin){
var poolConfig = poolConfigs[coin];
var internalConfig = poolConfig.shareProcessing.internal;
var redisConfig = internalConfig.redis;
for (var i = 0; i < redisClients.length; i++){
var client = redisClients[i];
if (client.client.port === redisConfig.port && client.client.host === redisConfig.host){
client.coins.push(coin);
return;
}
}
redisClients.push({
coins: [coin],
client: redis.createClient(redisConfig.port, redisConfig.host)
});
});
//Every 10 minutes clear out old hashrate stats for each coin from redis
var clearExpiredHashrates = function(){
redisClients.forEach(function(client){
var tenMinutesAgo = (Date.now() / 1000 | 0) - (60 * 10);
var redisCommands = client.coins.map(function(coin){
return ['zremrangebyscore', coin + '_hashrate', '-inf', tenMinutesAgo];
});
client.client.multi(redisCommands).exec(function(err, replies){
if (err)
console.log('error with clearing old hashrates ' + JSON.stringify(err));
});
});
};
setInterval(clearExpiredHashrates, 10 * 60 * 1000);
clearExpiredHashrates();
this.stats = {};
this.getStats = function(callback){
async.map(redisClients, function(client, callback){
var tenMinutesAgo = (Date.now() / 1000 | 0) - (60 * 10);
var redisCommands = client.coins.map(function(coin){
return ['zrangebyscore', coin + '_hashrate', tenMinutesAgo, '+inf'];
});
client.client.multi(redisCommands).exec(function(err, replies){
if (err){
console.log('error with getting hashrate stats ' + JSON.stringify(err));
callback(err);
}
else{
var replyObj = {};
for(var i = 0; i < replies.length; i++){
replyObj[client.coins[i]] = replies[i];
}
callback(null, replyObj);
}
});
}, function(err, results){
var portalStats = {
global:{
workers: 0,
shares: 0
},
pools:{
}
};
results.forEach(function(r){
var coin = Object.keys(r)[0];
var coinStats = {workers: {}, shares: 0};
r[coin].forEach(function(ins){
var parts = ins.split(':');
var workerShares = parseInt(parts[0]);;
coinStats.shares += workerShares
var worker = parts[1];
if (worker in coinStats.workers)
coinStats.workers[worker] += workerShares
else
coinStats.workers[worker] = workerShares
});
portalStats.pools[coin] = coinStats;
portalStats.global.shares += coinStats.shares;
portalStats.global.workers += Object.keys(coinStats.workers).length;
});
_this.stats = portalStats;
callback();
});
/*
{ global: {
}
*/
//get stats like hashrate and in/valid shares/blocks and workers in current round
};
};

123
libs/stats.js Normal file
View File

@ -0,0 +1,123 @@
var redis = require('redis');
var async = require('async');
var os = require('os');
module.exports = function(logger, portalConfig, poolConfigs){
var _this = this;
var redisClients = [];
Object.keys(poolConfigs).forEach(function(coin){
var poolConfig = poolConfigs[coin];
var internalConfig = poolConfig.shareProcessing.internal;
var redisConfig = internalConfig.redis;
for (var i = 0; i < redisClients.length; i++){
var client = redisClients[i];
if (client.client.port === redisConfig.port && client.client.host === redisConfig.host){
client.coins.push(coin);
return;
}
}
redisClients.push({
coins: [coin],
client: redis.createClient(redisConfig.port, redisConfig.host)
});
});
this.stats = {};
this.getStats = function(callback){
var allCoinStats = [];
async.each(redisClients, function(client, callback){
var windowTime = (Date.now() / 1000 | 0) - portalConfig.website.hashrateWindow;
var redisCommands = [];
var commandsPerCoin = 4;
//Clear out old hashrate stats for each coin from redis
client.coins.forEach(function(coin){
redisCommands.push(['zremrangebyscore', coin + '_hashrate', '-inf', windowTime]);
redisCommands.push(['zrangebyscore', coin + '_hashrate', windowTime, '+inf']);
redisCommands.push(['hgetall', coin + '_stats']);
redisCommands.push(['scard', coin + '_blocks']);
});
client.client.multi(redisCommands).exec(function(err, replies){
if (err){
console.log('error with getting hashrate stats ' + JSON.stringify(err));
callback(err);
}
else{
for(var i = 0; i < replies.length; i += commandsPerCoin){
var coinStats = {
coinName: client.coins[i / commandsPerCoin | 0],
hashrates: replies[i + 1],
poolStats: replies[i + 2],
poolPendingBlocks: replies[i + 3]
};
allCoinStats.push(coinStats)
}
callback();
}
});
}, function(err){
if (err){
console.log('error getting all stats' + JSON.stringify(err));
callback();
return;
}
var portalStats = {
global:{
workers: 0,
hashrate: 0
},
pools: allCoinStats
};
allCoinStats.forEach(function(coinStats){
coinStats.workers = {};
coinStats.shares = 0;
coinStats.hashrates.forEach(function(ins){
var parts = ins.split(':');
var workerShares = parseInt(parts[0]);
coinStats.shares += workerShares;
var worker = parts[1];
if (worker in coinStats.workers)
coinStats.workers[worker] += workerShares
else
coinStats.workers[worker] = workerShares
});
coinStats.hashrate = (coinStats.shares * 4294967296 / portalConfig.website.hashrateWindow) / 100000000 | 0;
delete coinStats.hashrates;
portalStats.global.hashrate += coinStats.hashrate;
portalStats.global.workers += Object.keys(coinStats.workers).length;
});
console.log(JSON.stringify(portalStats, null, 4));
_this.stats = portalStats;
callback();
});
/*
{ global: {
}
*/
//get stats like hashrate and in/valid shares/blocks and workers in current round
};
};

View File

@ -31,7 +31,7 @@ var async = require('async');
var dot = require('dot');
var express = require('express');
var api = require('./api.js');
var stats = require('./stats.js');
module.exports = function(logger){
@ -41,7 +41,7 @@ module.exports = function(logger){
var websiteConfig = portalConfig.website;
var portalApi = new api(logger, poolConfigs);
var portalStats = new stats(logger, portalConfig, poolConfigs);
var logIdentify = 'Website';
@ -75,7 +75,7 @@ module.exports = function(logger){
for (var pageName in pageTemplates){
pageProcessed[pageName] = pageTemplates[pageName]({
poolsConfigs: poolConfigs,
stats: portalApi.stats,
stats: portalStats.stats,
portalConfig: portalConfig
});
}
@ -107,13 +107,20 @@ module.exports = function(logger){
});
portalApi.getStats(function(){
portalStats.getStats(function(){
readPageFiles(Object.keys(pageFiles));
});
var buildUpdatedWebsite = function(){
portalApi.getStats(function(){
portalStats.getStats(function(){
processTemplates();
var statData = 'data: ' + JSON.stringify(portalStats.stats) + '\n\n';
for (var uid in liveStatConnections){
var res = liveStatConnections[uid];
res.write(statData);
}
});
};
@ -136,17 +143,21 @@ module.exports = function(logger){
var data = pageTemplates.index({
page: requestedPage,
selected: pageId,
stats: portalApi.stats,
stats: portalStats.stats,
poolConfigs: poolConfigs,
portalConfig: portalConfig
});
res.send(data);
res.end(data);
}
else
next();
};
var liveStatConnections = {};
app.get('/:page', route);
app.get('/', route);
@ -156,21 +167,35 @@ module.exports = function(logger){
case 'get_page':
var requestedPage = getPage(req.query.id);
if (requestedPage){
res.send(requestedPage);
res.end(requestedPage);
return;
}
case 'live_stats':
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
res.write('\n');
var uid = Math.random().toString();
liveStatConnections[uid] = res;
req.on("close", function() {
delete liveStatConnections[uid];
});
return;
default:
next();
}
res.send('you did api method ' + req.params.method);
//res.send('you did api method ' + req.params.method);
});
app.use('/static', express.static('website'));
app.use(function(err, req, res, next){
console.error(err.stack);
res.send(500, 'Something broke!');
res.end(500, 'Something broke!');
});
app.listen(portalConfig.website.port, function(){

View File

@ -1,33 +0,0 @@
var stats = {
global:{
hashrate: 1000, //in KH/s
validShares: 1,
invalidShares: 1,
validBlocks: 1,
invalidBlocks: 1,
blocksPending: 1,
blocksConfirmed: 1,
blocksOrphaned: 1,
connectedMiners: 344
},
pools:[
{
coin: "Dogecoin",
sybmol: 'doge',
stats:{
hashrate: 1000, //in KH/s
validShares: 1,
invalidShares: 1,
validBlocks: 1,
invalidBlocks: 1,
blocksPending: 1,
blocksConfirmed: 1,
blocksOrphaned: 1,
connectedMiners: 34545,
totalPayedOut: 3343.789797
}
}
]
}

View File

@ -23,11 +23,36 @@
<header class="header">
<div class="pure-menu pure-menu-open pure-menu-horizontal">
<ul>
<li id="home" class="{{? it.selected === '' }}selected{{?}}"><a class="hot-swapper" href="/"><i class="fa fa-home"></i>&nbsp; Home</a></li>
<li class="{{? it.selected === 'getting_started' }}selected{{?}}"><a class="hot-swapper" href="/getting_started"><i class="fa fa-arrow-right"></i>&nbsp; Getting Started</a></li>
<li class="{{? it.selected === 'stats' }}selected{{?}}"><a class="hot-swapper" href="/stats"><i class="fa fa-bar-chart-o"></i>&nbsp; Stats</a></li>
<li class="{{? it.selected === 'api' }}selected{{?}}"><a class="hot-swapper" href="/api"><i class="fa fa-code"></i>&nbsp; API</a></li>
<li id="home" class="{{? it.selected === '' }}selected{{?}}">
<a class="hot-swapper" href="/"><i class="fa fa-home"></i>&nbsp;
Home
</a>
</li>
<li class="{{? it.selected === 'getting_started' }}selected{{?}}">
<a class="hot-swapper" href="/getting_started">
<i class="fa fa-rocket"></i>&nbsp;
Getting Started
</a>
</li>
<li class="{{? it.selected === 'stats' }}selected{{?}}">
<a class="hot-swapper" href="/stats">
<i class="fa fa-bar-chart-o"></i>&nbsp;
Stats
</a>
</li>
<li class="{{? it.selected === 'api' }}selected{{?}}">
<a class="hot-swapper" href="/api">
<i class="fa fa-code"></i>&nbsp;
API
</a>
</li>
</ul>
<div class="stats">
<div><i class="fa fa-users"></i>&nbsp;<span id="statsMiners">{{=it.stats.global.workers}}</span> Miners</div>
<div><i class="fa fa-tachometer"></i>&nbsp;<span id="statsHashrate">{{=it.stats.global.hashrate}}</span> KH/s</div>
</div>
</div>
</header>
@ -40,10 +65,13 @@
<footer>
<div>This site is powered by the open source&nbsp;<a href="https://github.com/zone117x/node-open-mining-portal/">NOMP</a>&nbsp;project created by Matthew Little and licensed under the&nbsp;<a href="http://www.gnu.org/licenses/gpl-2.0.html">GPL</a></div>
<div><i class="fa fa-heart"></i>&nbsp; Support this project by donating&nbsp;<i class="fa fa-btc"></i>&nbsp;BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR</div>
<div>
This site is powered by the open source&nbsp;<a target="_blank" href="https://github.com/zone117x/node-open-mining-portal/">NOMP</a>&nbsp;
project created by Matthew Little and licensed under the&nbsp;<a href="http://www.gnu.org/licenses/gpl-2.0.html">GPL</a>
</div>
<div>
<i class="fa fa-heart"></i>&nbsp; Support this project by donating&nbsp;<i class="fa fa-btc"></i>&nbsp;BTC: 1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR
</div>
<div id="communityFooter">
Community&nbsp;<i class="fa fa-comment"></i>&nbsp;: &nbsp;<a target="_blank" href="https://kiwiirc.com/client/irc.freenode.net/nomp">#nomp IRC</a>
&nbsp;&nbsp;|&nbsp;&nbsp;

View File

@ -12,6 +12,7 @@ $(function(){
};
$('.hot-swapper').click(function(event){
if (event.which !== 1) return;
var pageId = $(this).attr('href').slice(1);
hotSwap(pageId, true);
event.preventDefault();
@ -26,4 +27,11 @@ $(function(){
}, 0);
});
var statsSource = new EventSource("/api/live_stats");
statsSource.addEventListener('message', function(e){
var stats = JSON.parse(e.data);
$('#statsMiners').text(stats.global.workers);
$('#statsHashrate').text(stats.global.hashrate);
});
});

View File

@ -22,9 +22,21 @@ header{
background-color: #f3f2ef;
}
header > div{
display: -moz-box;
display: -webkit-flexbox;
display: -ms-flexbox;
display: -webkit-flex;
display: -moz-flex;
display: flex;
}
header > div > ul{
flex: 1;
}
header .selected, header a:active, header a:focus{
background-color: #404040 !important;
}
.selected > a {
@ -41,17 +53,42 @@ header a:hover {
}
.stats{
display: flex;
flex-direction: column;
justify-content: center;
}
.stats > div{
padding: 2px 15px;
border-radius: 4px;
color: #f2f2f2;
font-size: 0.95em;
width: 120px;
text-align: center;
}
.stats > div:nth-child(1){
background-color: #71a380;
margin-bottom: 4px;
}
.stats > div:nth-child(2){
background-color: #7196a3;
}
.pure-menu-heading{
padding-left: 0 !important;
}
header > div{
background-color: transparent !important;
max-width: 768px;
margin-left: auto;
margin-right: auto;
}
main > div, header > div{
max-width: 800px;
}
main {
flex: 1 1 auto;
@ -59,11 +96,11 @@ main {
}
main > div{
max-width: 768px;
margin-left: auto;
margin-right: auto;
}
footer{
text-align: center;
color: #b3b3b3;