Deprecate stats table in favor of prometheus integration

This commit is contained in:
Calvin Montgomery 2017-07-17 21:58:58 -07:00
parent c7bec6251e
commit e780e7dadb
14 changed files with 93 additions and 1583 deletions

21
NEWS.md
View File

@ -1,3 +1,24 @@
2017-07-17
==========
The `stats` database table and associated ACP subpage have been removed in favor
of integration with [Prometheus](https://prometheus.io/). You can enable
Prometheus reporting by copying `conf/example/prometheus.toml` to
`conf/prometheus.toml` and editing it to your liking. I recommend integrating
Prometheus with [Grafana](https://grafana.com/) for dashboarding needs.
The particular metrics that were saved in the `stats` table are reported by the
following Prometheus metrics:
* Channel count: `cytube_channels_num_active` gauge.
* User count: `cytube_sockets_num_connected` gauge (labeled by socket.io
transport).
* CPU/Memory: default metrics emitted by the
[`prom-client`](https://github.com/siimon/prom-client) module.
More Prometheus metrics will be added in the future to make CyTube easier to
monitor :)
2017-07-15 2017-07-15
========== ==========

View File

@ -166,13 +166,6 @@ channel-save-interval: 5
channel-storage: channel-storage:
type: 'file' type: 'file'
# Configure statistics tracking
stats:
# Interval (in milliseconds) between data points - default 1h
interval: 3600000
# Maximum age of a datapoint (ms) before it is deleted - default 24h
max-age: 86400000
# Configure periodic clearing of old alias data # Configure periodic clearing of old alias data
aliases: aliases:
# Interval (in milliseconds) between subsequent runs of clearing # Interval (in milliseconds) between subsequent runs of clearing

View File

@ -2,7 +2,7 @@
"author": "Calvin Montgomery", "author": "Calvin Montgomery",
"name": "CyTube", "name": "CyTube",
"description": "Online media synchronizer and chat", "description": "Online media synchronizer and chat",
"version": "3.41.1", "version": "3.42.0",
"repository": { "repository": {
"url": "http://github.com/calzoneman/sync" "url": "http://github.com/calzoneman/sync"
}, },

View File

@ -257,12 +257,6 @@ function handleForceUnload(user, data) {
Logger.eventlog.log("[acp] " + eventUsername(user) + " forced unload of " + name); Logger.eventlog.log("[acp] " + eventUsername(user) + " forced unload of " + name);
} }
function handleListStats(user) {
db.listStats(function (err, rows) {
user.socket.emit("acp-list-stats", rows);
});
}
function init(user) { function init(user) {
var s = user.socket; var s = user.socket;
s.on("acp-announce", handleAnnounce.bind(this, user)); s.on("acp-announce", handleAnnounce.bind(this, user));
@ -276,7 +270,6 @@ function init(user) {
s.on("acp-delete-channel", handleDeleteChannel.bind(this, user)); s.on("acp-delete-channel", handleDeleteChannel.bind(this, user));
s.on("acp-list-activechannels", handleListActiveChannels.bind(this, user)); s.on("acp-list-activechannels", handleListActiveChannels.bind(this, user));
s.on("acp-force-unload", handleForceUnload.bind(this, user)); s.on("acp-force-unload", handleForceUnload.bind(this, user));
s.on("acp-list-stats", handleListStats.bind(this, user));
const globalBanDB = db.getGlobalBanDB(); const globalBanDB = db.getGlobalBanDB();
globalBanDB.listGlobalBans().then(bans => { globalBanDB.listGlobalBans().then(bans => {

View File

@ -13,26 +13,6 @@ const LOGGER = require('@calzoneman/jsli')('bgtask');
var init = null; var init = null;
/* Stats */
function initStats(Server) {
var STAT_INTERVAL = parseInt(Config.get("stats.interval"));
var STAT_EXPIRE = parseInt(Config.get("stats.max-age"));
setInterval(function () {
var chancount = Server.channels.length;
var usercount = 0;
Server.channels.forEach(function (chan) {
usercount += chan.users.length;
});
var mem = process.memoryUsage().rss;
db.addStatPoint(Date.now(), usercount, chancount, mem, function () {
db.pruneStats(Date.now() - STAT_EXPIRE);
});
}, STAT_INTERVAL);
}
/* Alias cleanup */ /* Alias cleanup */
function initAliasCleanup(Server) { function initAliasCleanup(Server) {
var CLEAN_INTERVAL = parseInt(Config.get("aliases.purge-interval")); var CLEAN_INTERVAL = parseInt(Config.get("aliases.purge-interval"));
@ -91,7 +71,6 @@ module.exports = function (Server) {
} }
init = Server; init = Server;
initStats(Server);
initAliasCleanup(Server); initAliasCleanup(Server);
initChannelDumper(Server); initChannelDumper(Server);
initPasswordResetCleanup(Server); initPasswordResetCleanup(Server);

View File

@ -82,10 +82,6 @@ var defaults = {
"max-channels-per-user": 5, "max-channels-per-user": 5,
"max-accounts-per-ip": 5, "max-accounts-per-ip": 5,
"guest-login-delay": 60, "guest-login-delay": 60,
stats: {
interval: 3600000,
"max-age": 86400000
},
aliases: { aliases: {
"purge-interval": 3600000, "purge-interval": 3600000,
"max-age": 2592000000 "max-age": 2592000000

View File

@ -360,37 +360,6 @@ module.exports.getIPs = function (name, callback) {
/* END REGION */ /* END REGION */
/* REGION stats */
module.exports.addStatPoint = function (time, ucount, ccount, mem, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
var query = "INSERT INTO stats VALUES (?, ?, ?, ?)";
module.exports.query(query, [time, ucount, ccount, mem], callback);
};
module.exports.pruneStats = function (before, callback) {
if (typeof callback !== "function") {
callback = blackHole;
}
var query = "DELETE FROM stats WHERE time < ?";
module.exports.query(query, [before], callback);
};
module.exports.listStats = function (callback) {
if (typeof callback !== "function") {
return;
}
var query = "SELECT * FROM stats ORDER BY time ASC";
module.exports.query(query, callback);
};
/* END REGION */
/* Misc */ /* Misc */
module.exports.loadAnnouncement = function () { module.exports.loadAnnouncement = function () {
var query = "SELECT * FROM `meta` WHERE `key`='announcement'"; var query = "SELECT * FROM `meta` WHERE `key`='announcement'";

View File

@ -65,15 +65,6 @@ const TBL_ALIASES = "" +
"PRIMARY KEY (`visit_id`), INDEX (`ip`)" + "PRIMARY KEY (`visit_id`), INDEX (`ip`)" +
")"; ")";
const TBL_STATS = "" +
"CREATE TABLE IF NOT EXISTS `stats` (" +
"`time` BIGINT NOT NULL," +
"`usercount` INT NOT NULL," +
"`chancount` INT NOT NULL," +
"`mem` INT NOT NULL," +
"PRIMARY KEY (`time`))" +
"CHARACTER SET utf8";
const TBL_META = "" + const TBL_META = "" +
"CREATE TABLE IF NOT EXISTS `meta` (" + "CREATE TABLE IF NOT EXISTS `meta` (" +
"`key` VARCHAR(255) NOT NULL," + "`key` VARCHAR(255) NOT NULL," +
@ -132,7 +123,6 @@ module.exports.init = function (queryfn, cb) {
password_reset: TBL_PASSWORD_RESET, password_reset: TBL_PASSWORD_RESET,
user_playlists: TBL_USER_PLAYLISTS, user_playlists: TBL_USER_PLAYLISTS,
aliases: TBL_ALIASES, aliases: TBL_ALIASES,
stats: TBL_STATS,
meta: TBL_META, meta: TBL_META,
channel_data: TBL_CHANNEL_DATA channel_data: TBL_CHANNEL_DATA
}; };

View File

@ -19,6 +19,7 @@ const verifySession = Promise.promisify(session.verifySession);
const getAliases = Promise.promisify(db.getAliases); const getAliases = Promise.promisify(db.getAliases);
import { CachingGlobalBanlist } from './globalban'; import { CachingGlobalBanlist } from './globalban';
import proxyaddr from 'proxy-addr'; import proxyaddr from 'proxy-addr';
import { Counter, Gauge } from 'prom-client';
const LOGGER = require('@calzoneman/jsli')('ioserver'); const LOGGER = require('@calzoneman/jsli')('ioserver');
@ -187,6 +188,54 @@ function isIPGlobalBanned(ip) {
return globalIPBanlist.isIPGlobalBanned(ip); return globalIPBanlist.isIPGlobalBanned(ip);
} }
const promSocketCount = new Gauge({
name: 'cytube_sockets_num_connected',
help: 'Gauge of connected socket.io clients',
labelNames: ['transport']
});
const promSocketAccept = new Counter({
name: 'cytube_sockets_accept_count',
help: 'Counter for number of connections accepted. Excludes rejected connections.'
});
const promSocketDisconnect = new Counter({
name: 'cytube_sockets_disconnect_count',
help: 'Counter for number of connections disconnected.'
});
function emitMetrics(sock) {
try {
let transportName = sock.client.conn.transport.name;
promSocketCount.inc({ transport: transportName });
promSocketAccept.inc(1, new Date());
sock.client.conn.on('upgrade', newTransport => {
try {
// Sanity check
if (newTransport !== transportName) {
promSocketCount.dec({ transport: transportName });
transportName = newTransport.name;
promSocketCount.inc({ transport: transportName });
}
} catch (error) {
LOGGER.error('Error emitting transport upgrade metrics for socket (ip=%s): %s',
sock._realip, error.stack);
}
});
sock.on('disconnect', () => {
try {
promSocketCount.dec({ transport: transportName });
promSocketDisconnect.inc(1, new Date());
} catch (error) {
LOGGER.error('Error emitting disconnect metrics for socket (ip=%s): %s',
sock._realip, error.stack);
}
});
} catch (error) {
LOGGER.error('Error emitting metrics for socket (ip=%s): %s',
sock._realip, error.stack);
}
}
/** /**
* Called after a connection is accepted * Called after a connection is accepted
*/ */
@ -227,6 +276,8 @@ function handleConnection(sock) {
return; return;
} }
emitMetrics(sock);
LOGGER.info("Accepted socket from " + ip); LOGGER.info("Accepted socket from " + ip);
counters.add("socket.io:accept", 1); counters.add("socket.io:accept", 1);

View File

@ -1,10 +1,11 @@
import http from 'http'; import http from 'http';
import { register } from 'prom-client'; import { register, collectDefaultMetrics } from 'prom-client';
import { parse as parseURL } from 'url'; import { parse as parseURL } from 'url';
const LOGGER = require('@calzoneman/jsli')('prometheus-server'); const LOGGER = require('@calzoneman/jsli')('prometheus-server');
let server = null; let server = null;
let defaultMetricsTimer = null;
export function init(prometheusConfig) { export function init(prometheusConfig) {
if (server !== null) { if (server !== null) {
@ -13,6 +14,8 @@ export function init(prometheusConfig) {
return; return;
} }
defaultMetricsTimer = collectDefaultMetrics();
server = http.createServer((req, res) => { server = http.createServer((req, res) => {
if (req.method !== 'GET' if (req.method !== 'GET'
|| parseURL(req.url).pathname !== prometheusConfig.getPath()) { || parseURL(req.url).pathname !== prometheusConfig.getPath()) {
@ -44,4 +47,6 @@ export function init(prometheusConfig) {
export function shutdown() { export function shutdown() {
server.close(); server.close();
server = null; server = null;
clearInterval(defaultMetricsTimer);
defaultMetricsTimer = null;
} }

View File

@ -51,6 +51,7 @@ import session from './session';
import { LegacyModule } from './legacymodule'; import { LegacyModule } from './legacymodule';
import { PartitionModule } from './partition/partitionmodule'; import { PartitionModule } from './partition/partitionmodule';
import * as Switches from './switches'; import * as Switches from './switches';
import { Gauge } from 'prom-client';
var Server = function () { var Server = function () {
var self = this; var self = this;
@ -226,6 +227,10 @@ Server.prototype.isChannelLoaded = function (name) {
return false; return false;
}; };
const promActiveChannels = new Gauge({
name: 'cytube_channels_num_active',
help: 'Number of channels currently active'
});
Server.prototype.getChannel = function (name) { Server.prototype.getChannel = function (name) {
var cname = name.toLowerCase(); var cname = name.toLowerCase();
if (this.partitionDecider && if (this.partitionDecider &&
@ -242,6 +247,7 @@ Server.prototype.getChannel = function (name) {
} }
var c = new Channel(name); var c = new Channel(name);
promActiveChannels.inc();
c.on("empty", function () { c.on("empty", function () {
self.unloadChannel(c); self.unloadChannel(c);
}); });
@ -310,6 +316,7 @@ Server.prototype.unloadChannel = function (chan, options) {
} }
} }
chan.dead = true; chan.dead = true;
promActiveChannels.dec();
}; };
Server.prototype.packChannelList = function (publicOnly, isAdmin) { Server.prototype.packChannelList = function (publicOnly, isAdmin) {

View File

@ -11,7 +11,7 @@ import csrf from './csrf';
import * as HTTPStatus from './httpstatus'; import * as HTTPStatus from './httpstatus';
import { CSRFError, HTTPError } from '../errors'; import { CSRFError, HTTPError } from '../errors';
import counters from '../counters'; import counters from '../counters';
import { Summary } from 'prom-client'; import { Summary, Counter } from 'prom-client';
const LOGGER = require('@calzoneman/jsli')('webserver'); const LOGGER = require('@calzoneman/jsli')('webserver');
@ -35,6 +35,11 @@ function initPrometheus(app) {
+ 'until the "finish" event on the response object.', + 'until the "finish" event on the response object.',
labelNames: ['method', 'statusCode'] labelNames: ['method', 'statusCode']
}); });
const requests = new Counter({
name: 'cytube_http_req_count',
help: 'HTTP Request count',
labelNames: ['method', 'statusCode']
});
app.use((req, res, next) => { app.use((req, res, next) => {
const startTime = process.hrtime(); const startTime = process.hrtime();
@ -43,6 +48,7 @@ function initPrometheus(app) {
const diff = process.hrtime(startTime); const diff = process.hrtime(startTime);
const diffMs = diff[0]*1e3 + diff[1]*1e-6; const diffMs = diff[0]*1e3 + diff[1]*1e-6;
latency.labels(req.method, res.statusCode).observe(diffMs); latency.labels(req.method, res.statusCode).observe(diffMs);
requests.labels(req.method, res.statusCode).inc(1, new Date());
} catch (error) { } catch (error) {
LOGGER.error('Failed to record HTTP Prometheus metrics: %s', error.stack); LOGGER.error('Failed to record HTTP Prometheus metrics: %s', error.stack);
} }

View File

@ -34,7 +34,6 @@ addMenuItem("#acp-user-lookup", "Users");
addMenuItem("#acp-channel-lookup", "Channels"); addMenuItem("#acp-channel-lookup", "Channels");
addMenuItem("#acp-loaded-channels", "Active Channels"); addMenuItem("#acp-loaded-channels", "Active Channels");
addMenuItem("#acp-eventlog", "Event Log"); addMenuItem("#acp-eventlog", "Event Log");
addMenuItem("#acp-stats", "Stats");
/* Log Viewer */ /* Log Viewer */
function readSyslog() { function readSyslog() {
@ -541,79 +540,6 @@ function filterEventLog() {
$("#acp-eventlog-filter").change(filterEventLog); $("#acp-eventlog-filter").change(filterEventLog);
$("#acp-eventlog-refresh").click(readEventlog); $("#acp-eventlog-refresh").click(readEventlog);
/* Stats */
$("a:contains('Stats')").click(function () {
socket.emit("acp-list-stats");
});
socket.on("acp-list-stats", function (rows) {
var labels = [];
var ucounts = [];
var ccounts = [];
var mcounts = [];
var lastdate = "";
rows.forEach(function (r) {
var d = new Date(parseInt(r.time));
var t = "";
if (d.toDateString() !== lastdate) {
lastdate = d.toDateString();
t = d.getFullYear()+"-"+(d.getMonth()+1)+"-"+d.getDate();
t += " " + d.toTimeString().split(" ")[0];
} else {
t = d.toTimeString().split(" ")[0];
}
labels.push(t);
ucounts.push(r.usercount);
ccounts.push(r.chancount);
mcounts.push(r.mem / 1048576);
});
var userdata = {
labels: labels,
datasets: [
{
fillColor: "rgba(151, 187, 205, 0.5)",
strokeColor: "rgba(151, 187, 205, 1)",
pointColor: "rgba(151, 187, 205, 1)",
pointStrokeColor: "#fff",
data: ucounts
}
]
};
var channeldata = {
labels: labels,
datasets: [
{
fillColor: "rgba(151, 187, 205, 0.5)",
strokeColor: "rgba(151, 187, 205, 1)",
pointColor: "rgba(151, 187, 205, 1)",
pointStrokeColor: "#fff",
data: ccounts
}
]
};
var memdata = {
labels: labels,
datasets: [
{
fillColor: "rgba(151, 187, 205, 0.5)",
strokeColor: "rgba(151, 187, 205, 1)",
pointColor: "rgba(151, 187, 205, 1)",
pointStrokeColor: "#fff",
data: mcounts
}
]
};
new Chart($("#stat_users")[0].getContext("2d")).Line(userdata);
new Chart($("#stat_channels")[0].getContext("2d")).Line(channeldata);
new Chart($("#stat_mem")[0].getContext("2d")).Line(memdata);
});
/* Initialize keyed table sorts */ /* Initialize keyed table sorts */
$("table").each(function () { $("table").each(function () {
var table = $(this); var table = $(this);

File diff suppressed because it is too large Load Diff