mirror of https://github.com/calzoneman/sync.git
Deprecate stats table in favor of prometheus integration
This commit is contained in:
parent
c7bec6251e
commit
e780e7dadb
21
NEWS.md
21
NEWS.md
|
@ -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
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'";
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
1426
www/js/chart.js
1426
www/js/chart.js
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue