mirror of
https://github.com/samsonjs/kwikemon.git
synced 2026-03-25 09:05:51 +00:00
Compare commits
21 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 394fc04404 | |||
| f293afbc9e | |||
| 096500ca82 | |||
| 079d705280 | |||
| 847d133594 | |||
| ecd8249271 | |||
| 64b82dc978 | |||
| 4bf4b09bc9 | |||
| 5e978e94da | |||
| d59674b60f | |||
| 9776c1cfef | |||
| 7d398de64f | |||
| 6cf290baf9 | |||
| 672e1d1d0f | |||
| b9b49c0471 | |||
| 36930d666d | |||
| f8a09636d1 | |||
| 70fc6363ab | |||
| a5d210439c | |||
| 140e441919 | |||
| 3fbf082cbe |
20 changed files with 747 additions and 242 deletions
74
Readme.md
74
Readme.md
|
|
@ -11,28 +11,27 @@ npm install -g kwikemon
|
|||
## Usage
|
||||
|
||||
$ kwikemond &
|
||||
$ curl -s localhost/nginx_status | grep Active | kwikemon nginx-connections
|
||||
$ curl -s localhost/nginx_status | grep Active | kwikemon write nginx-connections
|
||||
$ curl localhost:1111/nignx-connections
|
||||
Active connections: 316
|
||||
$ kwikemon foo bar
|
||||
$ kwikemon set foo bar
|
||||
$ curl localhost:1111/
|
||||
foo: bar
|
||||
nginx-connections: Active connections: 316
|
||||
|
||||
Here's how it works:
|
||||
|
||||
- call `kwikemon thing status` to set the text for the monitor named "thing"
|
||||
- call `kwikemon set thing status` to set the text for the monitor named "thing"
|
||||
- fire up the server, `kwikemond`, that serves up these monitors in a big list or individually
|
||||
|
||||
Alternatively:
|
||||
|
||||
- continuously pipe data to `kwikemon <name of thing you are watching>` on stdin
|
||||
- continuously pipe data to `kwikemon write <name of thing you are watching>` on stdin
|
||||
- every time a full line of text is received on stdin it becomes the new status for <name of thing you are watching>
|
||||
|
||||
To see everything `kwikemon` can do run it without arguments.
|
||||
To see everything `kwikemon` can do run `kwikemon help`.
|
||||
|
||||
# or with -h or --help
|
||||
$ kwikemon
|
||||
$ kwikemon help
|
||||
|
||||
This is very much a work in progress.
|
||||
|
||||
|
|
@ -44,12 +43,12 @@ You can use kwikemon as a library.
|
|||
var kwikemon = require('kiwkemon');
|
||||
|
||||
kwikemon.set('foo', 'bar', function(err) {
|
||||
kwikemon.fetch('foo', function(err, text) {
|
||||
kwikemon.get('foo', function(err, text) {
|
||||
console.log('foo = ' + text);
|
||||
});
|
||||
});
|
||||
|
||||
kwikemon.fetchAll(function(err, monitors) {
|
||||
kwikemon.getAll(function(err, monitors) {
|
||||
Object.keys(monitors).forEach(function (name) {
|
||||
console.log(name + ' = ' + monitors[name]);
|
||||
});
|
||||
|
|
@ -70,28 +69,14 @@ A monitor named `nginx` stores its data in the hash `kwikemon:monitor:nginx`. Ha
|
|||
are:
|
||||
|
||||
- text
|
||||
- expire
|
||||
- created
|
||||
- modified
|
||||
- updates
|
||||
|
||||
The list of all monitors is a set stored at `kwikemon:monitors`.
|
||||
|
||||
#### List
|
||||
|
||||
This is when you should clean out expired entries.
|
||||
|
||||
names = redis.smembers("kwikemon:monitors")
|
||||
monitors = {}
|
||||
for name in names:
|
||||
if redis.exists("kwikemon:monitor:$name"):
|
||||
monitors[name] = redis.hget("kwikemon:monitor:$name", "text")
|
||||
return monitors
|
||||
|
||||
#### Read
|
||||
|
||||
redis.hgetall("kwikemon:monitor:nginx")
|
||||
|
||||
#### Update
|
||||
#### Set
|
||||
|
||||
exists = redis.exists("kwikemon:monitor:nginx")
|
||||
if exists:
|
||||
|
|
@ -111,11 +96,48 @@ This is when you should clean out expired entries.
|
|||
# optional
|
||||
redis.expire("kwikemon:monitor:nginx", <ttl>)
|
||||
|
||||
#### Delete
|
||||
#### Get
|
||||
|
||||
redis.hgetall("kwikemon:monitor:nginx")
|
||||
|
||||
#### Remove
|
||||
|
||||
redis.del("kwikemon:monitor:nginx")
|
||||
redis.srem("kwikemon:monitors", "nginx")
|
||||
|
||||
#### Sweep
|
||||
|
||||
Clean out expired monitors. Call this before anything that relies on counting or iterating through all monitors.
|
||||
|
||||
for name in redis.smembers("kwikemon:monitors"):
|
||||
if not redis.exists("kwikemon:monitor:$name"):
|
||||
remove(name)
|
||||
|
||||
#### Count
|
||||
|
||||
Sweep before running a count.
|
||||
|
||||
sweep()
|
||||
redis.scard("kwikemon:monitors")
|
||||
|
||||
#### List names
|
||||
|
||||
Sweep before listing.
|
||||
|
||||
sweep()
|
||||
redis.smembers("kwikemon:monitors")
|
||||
|
||||
#### Get all
|
||||
|
||||
Sweep before geting all.
|
||||
|
||||
sweep()
|
||||
monitors = {}
|
||||
for name in list():
|
||||
if redis.exists("kwikemon:monitor:$name"):
|
||||
monitors[name] = get(name)
|
||||
return monitors
|
||||
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
116
app.js
Normal file
116
app.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Copyright 2013 Sami Samhuri
|
||||
|
||||
var express = require('express')
|
||||
, Negotiator = require('negotiator')
|
||||
, kwikemon = require('./kwikemon.js')
|
||||
, app = module.exports = express()
|
||||
, version = require('./version.js')
|
||||
;
|
||||
|
||||
// Middleware
|
||||
app.use(express.favicon('/dev/null'));
|
||||
app.use(express.logger());
|
||||
app.use(express.static(__dirname + '/public'));
|
||||
|
||||
// Views
|
||||
app.set('view engine', 'jade');
|
||||
app.set('views', __dirname + '/views');
|
||||
|
||||
// Routes
|
||||
app.get('/', route('monitors', getMonitors));
|
||||
app.get('/:name', route('monitor', getMonitor));
|
||||
|
||||
function route(template, buildContext) {
|
||||
return function(req, res) {
|
||||
buildContext(req, res, function(err, ctx) {
|
||||
if (err) {
|
||||
var message = err.message || String(err)
|
||||
, status = message == 'not found' ? 404 : 500
|
||||
;
|
||||
res.format({
|
||||
html: function() {
|
||||
res.render('error', {
|
||||
version: version,
|
||||
pageTitle: 'Error',
|
||||
err: err
|
||||
});
|
||||
},
|
||||
text: function() {
|
||||
res.send(renderText('error', { err: err }));
|
||||
},
|
||||
json: function() {
|
||||
res.json({ message: message });
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
ctx = ctx || {};
|
||||
res.format({
|
||||
html: function() {
|
||||
ctx.version = version;
|
||||
res.render(template, ctx);
|
||||
},
|
||||
text: function() {
|
||||
res.send(renderText(template, ctx));
|
||||
},
|
||||
json: function() {
|
||||
res.json(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
function renderText(template, ctx) {
|
||||
var text;
|
||||
switch (template) {
|
||||
case 'monitor':
|
||||
text = String(ctx.monitor.text);
|
||||
break;
|
||||
|
||||
case 'monitors':
|
||||
text = Object.keys(ctx.monitors).sort().map(function(name) {
|
||||
return name + ': ' + ctx.monitors[name].text;
|
||||
}).join('\n');
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
text = ctx.err.message || String(ctx.err);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('unknown text template: ' + template);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////
|
||||
// Request handlers //
|
||||
//////////////////////
|
||||
|
||||
function getMonitors(req, res, cb) {
|
||||
kwikemon.getAll(function(err, monitors) {
|
||||
cb(err, err ? null : {
|
||||
pageTitle: 'Monitors',
|
||||
monitors: monitors
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getMonitor(req, res, cb) {
|
||||
var name = req.params.name;
|
||||
kwikemon.get(name, function(err, mon) {
|
||||
if (!mon) {
|
||||
err = new Error('not found');
|
||||
}
|
||||
cb(err, err ? null : {
|
||||
pageTitle: mon.name,
|
||||
monitor: mon
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
224
bin/kwikemon
224
bin/kwikemon
|
|
@ -2,120 +2,166 @@
|
|||
|
||||
var kwikemon = require('../kwikemon.js');
|
||||
|
||||
function usage() {
|
||||
console.log("usage: kwikemon [options]");
|
||||
console.log(" kwikemon <name> [text]");
|
||||
console.log();
|
||||
console.log("options:");
|
||||
console.log(" -c, --clear remove all monitors");
|
||||
console.log(" -f, --fetch <name> show the text of the named monitor");
|
||||
console.log(" -n, --count count monitors");
|
||||
console.log(" -h, --help show what you're reading now");
|
||||
console.log(" -l, --list show all monitors");
|
||||
console.log(" -r, --remove <name> remove the named monitor");
|
||||
console.log(" -s, --sweep clean up expired & deleted monitors");
|
||||
console.log(" -t, --ttl <name> show the TTL of the named monitor");
|
||||
var commands = {};
|
||||
function defineCommand(name, handler, args) {
|
||||
commands[name] = { handler: handler, args: args };
|
||||
}
|
||||
defineCommand('clear', clear);
|
||||
defineCommand('count', count);
|
||||
defineCommand('get', get, 'name');
|
||||
defineCommand('help', usage);
|
||||
defineCommand('list', list);
|
||||
defineCommand('remove', remove, 'name');
|
||||
defineCommand('set', set, 'name text');
|
||||
defineCommand('sweep', sweep);
|
||||
defineCommand('ttl', ttl, 'name [ttl]');
|
||||
defineCommand('write', write, 'name');
|
||||
|
||||
function main() {
|
||||
var cmd = process.argv[2]
|
||||
, handler = commands[cmd] && commands[cmd].handler
|
||||
;
|
||||
if (handler) {
|
||||
handler.apply(null, process.argv.slice(3));
|
||||
}
|
||||
else {
|
||||
if (cmd) {
|
||||
console.log('error: unknown command ' + cmd);
|
||||
}
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
var opt = process.argv[2]
|
||||
, name = process.argv[2]
|
||||
, text = process.argv[3]
|
||||
;
|
||||
if (opt && opt[0] == '-') {
|
||||
switch (opt) {
|
||||
case '-c':
|
||||
case '--clear':
|
||||
kwikemon.removeAll(function(err) {
|
||||
process.exit(0);
|
||||
});
|
||||
break;
|
||||
function clear() {
|
||||
kwikemon.clear(function(err) {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
case '-f':
|
||||
case '--fetch':
|
||||
var name = process.argv[3];
|
||||
kwikemon.fetch(name, function(err, mon) {
|
||||
function count() {
|
||||
kwikemon.count(function(err, n) {
|
||||
console.log(n);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
function expire(name, ttl) {
|
||||
kwikemon.ttl(name, ttl, function(err) {
|
||||
if (err && err.message == 'not found') {
|
||||
console.log('no monitor named', name);
|
||||
process.exit(1);
|
||||
}
|
||||
else if (err) {
|
||||
console.log('error: ' + (err.message || err));
|
||||
process.exit(1);
|
||||
}
|
||||
else {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function get(name) {
|
||||
if (name) {
|
||||
kwikemon.get(name, function(err, mon) {
|
||||
if (mon) {
|
||||
console.log(mon.text);
|
||||
process.exit(0);
|
||||
}
|
||||
else {
|
||||
console.log('error: no monitor named', name);
|
||||
console.log('no monitor named', name);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
else {
|
||||
console.log('get requires a name');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
case '-l':
|
||||
case '--list':
|
||||
kwikemon.fetchAll(function(err, monitors) {
|
||||
Object.keys(monitors).forEach(function(name) {
|
||||
console.log(name + ':', monitors[name].text);
|
||||
});
|
||||
function list() {
|
||||
kwikemon.getAll(function(err, monitors) {
|
||||
Object.keys(monitors).forEach(function(name) {
|
||||
console.log(name + ':', monitors[name].text);
|
||||
});
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
function remove(name) {
|
||||
if (name) {
|
||||
kwikemon.remove(name, function(err) {
|
||||
process.exit(0);
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.log('remove requires a name');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function set(name, text) {
|
||||
if (name && text) {
|
||||
kwikemon.set(name, text, function() {
|
||||
process.exit(0);
|
||||
});
|
||||
break;
|
||||
}
|
||||
else {
|
||||
console.log('set requires a name and some text')
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
case '-n':
|
||||
case '--count':
|
||||
kwikemon.count(function(err, n) {
|
||||
console.log(n);
|
||||
process.exit(0);
|
||||
});
|
||||
break;
|
||||
function sweep() {
|
||||
kwikemon.sweep(function(err) {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
case '-r':
|
||||
case '--remove':
|
||||
name = process.argv[3];
|
||||
if (name) {
|
||||
kwikemon.remove(name, function(err) {
|
||||
process.exit(0);
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.log("error: --remove requires a name");
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case '-s':
|
||||
case '--sweep':
|
||||
kwikemon.sweep(function(err) {
|
||||
process.exit(0);
|
||||
});
|
||||
break;
|
||||
|
||||
case '-t':
|
||||
case '--ttl':
|
||||
var name = process.argv[3];
|
||||
kwikemon.fetchTTL(name, function(err, ttl) {
|
||||
function ttl(name, ttl) {
|
||||
if (ttl) return expire(name, Number(ttl));
|
||||
if (name) {
|
||||
kwikemon.ttl(name, function(err, ttl) {
|
||||
if (typeof ttl == 'number') {
|
||||
console.log(ttl);
|
||||
process.exit(0);
|
||||
}
|
||||
else {
|
||||
console.log('error: no monitor named', name);
|
||||
console.log('no monitor named', name);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
usage();
|
||||
process.exit(name == '-h' || name == '--help' ? 0 : 1);
|
||||
}
|
||||
else {
|
||||
console.log('ttl requires a name');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
else if (name && text) {
|
||||
kwikemon.set(name, text, function() {
|
||||
process.exit(0);
|
||||
|
||||
function write(name) {
|
||||
if (name) {
|
||||
process.stdin.pipe(kwikemon.writer(name));
|
||||
process.stdin.on('end', function() {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.log('write requires a name');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function usage() {
|
||||
console.log('usage: kwikemon <command> [args...]');
|
||||
console.log('commands:');
|
||||
Object.keys(commands).sort().forEach(function(name) {
|
||||
var args = commands[name].args || '';
|
||||
console.log(' ' + name + ' ' + args);
|
||||
});
|
||||
}
|
||||
else if (name) {
|
||||
process.stdin.pipe(kwikemon.createWriter(name));
|
||||
process.stdin.on('end', function() {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
else {
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (require.main == module) {
|
||||
main();
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
var server = require('../server.js')
|
||||
, port = process.argv[2]
|
||||
, host = process.argv[3]
|
||||
var port = process.argv[2] || 1111
|
||||
, host = process.argv[3] || '127.0.0.1'
|
||||
, app = require('../app.js')
|
||||
;
|
||||
|
||||
server.start(port, host);
|
||||
app.listen(port, host);
|
||||
console.log('kwikemond listening on ' + host + ':' + port);
|
||||
|
|
|
|||
106
kwikemon.js
106
kwikemon.js
|
|
@ -8,29 +8,47 @@ module.exports = {
|
|||
|
||||
// read
|
||||
, exists: callbackOptional(exists)
|
||||
, fetch: callbackOptional(fetch)
|
||||
, fetchTTL: callbackOptional(fetchTTL)
|
||||
, list: list
|
||||
, fetchAll: fetchAll
|
||||
, get: callbackOptional(get)
|
||||
, ttl: callbackOptional(ttl)
|
||||
, count: count
|
||||
, list: list
|
||||
, getAll: getAll
|
||||
|
||||
// remove
|
||||
, remove: callbackOptional(remove)
|
||||
, removeAll: removeAll
|
||||
, clear: clear
|
||||
, sweep: sweep
|
||||
|
||||
// change redis client
|
||||
, redis: setRedis
|
||||
, redis: redis
|
||||
};
|
||||
|
||||
var async = require('async')
|
||||
, redis = require('redis').createClient()
|
||||
, fs = require('fs')
|
||||
, Redis = require('redis')
|
||||
, redisClient
|
||||
, toml = require('toml')
|
||||
, LineEmitter = require('./line_emitter.js')
|
||||
;
|
||||
|
||||
function setRedis(newRedis) {
|
||||
if (redis) redis.end();
|
||||
redis = newRedis;
|
||||
function redis(newRedis) {
|
||||
if (newRedis){
|
||||
if (redisClient) redisClient.end();
|
||||
redisClient = newRedis;
|
||||
}
|
||||
else {
|
||||
if (!redisClient) {
|
||||
var configFile = process.env.HOME + '/.kwikemon.toml'
|
||||
, config = {}
|
||||
;
|
||||
if (fs.existsSync(configFile)) {
|
||||
config = toml.parse(fs.readFileSync(configFile));
|
||||
}
|
||||
config.redis = config.redis || {};
|
||||
redisClient = Redis.createClient(config.redis.port, config.redis.host, config.redis.options);
|
||||
}
|
||||
return redisClient;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the callback argument of a function optional.
|
||||
|
|
@ -39,15 +57,15 @@ function setRedis(newRedis) {
|
|||
// that accepts the callback is returned, with the
|
||||
// rest of the arguments fixed (like bind).
|
||||
//
|
||||
// function fetch(id, cb) { db.fetch(id, cb); }
|
||||
// fetch = callbackOptional(fetch);
|
||||
// function get(id, cb) { db.get(id, cb); }
|
||||
// get = callbackOptional(get);
|
||||
//
|
||||
// function print(err, x) { if (err) throw err; console.log(x); }
|
||||
//
|
||||
// fetch(1, print);
|
||||
// get(1, print);
|
||||
//
|
||||
// var fetch1 = fetch(1);
|
||||
// fetch1(print);
|
||||
// var get1 = get(1);
|
||||
// get1(print);
|
||||
function callbackOptional(fn, ctx) {
|
||||
return function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
|
|
@ -69,7 +87,7 @@ function k(name) {
|
|||
}
|
||||
|
||||
function exists(name, cb) {
|
||||
redis.exists(k(name), function(err, exists) {
|
||||
redis().exists(k(name), function(err, exists) {
|
||||
if (err) return cb(err);
|
||||
cb(null, exists == 1);
|
||||
});
|
||||
|
|
@ -88,10 +106,11 @@ function set(name, text, options, cb) {
|
|||
;
|
||||
exists(name, function(err, exists) {
|
||||
var fields = {
|
||||
text: text
|
||||
name: name
|
||||
, text: text
|
||||
, modified: Date.now()
|
||||
}
|
||||
, multi = redis.multi()
|
||||
, multi = redis().multi()
|
||||
;
|
||||
if (!exists) {
|
||||
fields.created = Date.now();
|
||||
|
|
@ -118,16 +137,37 @@ function writer(name) {
|
|||
return le;
|
||||
}
|
||||
|
||||
function fetch(name, cb) {
|
||||
redis.hgetall(k(name), cb);
|
||||
function get(name, cb) {
|
||||
redis().hgetall(k(name), cb);
|
||||
}
|
||||
|
||||
function fetchTTL(name, cb) {
|
||||
redis.ttl(k(name), cb);
|
||||
function expire(name, ttl, cb) {
|
||||
exists(name, function(err, exists) {
|
||||
if (err || !exists) {
|
||||
return cb(err || new Error('not found'));
|
||||
}
|
||||
redis().multi()
|
||||
.hset(k(name), 'expire', ttl)
|
||||
.expire(k(name), ttl)
|
||||
.exec(cb);
|
||||
});
|
||||
}
|
||||
|
||||
function ttl(name, ttl, cb) {
|
||||
if (typeof ttl == 'number') {
|
||||
expire(name, ttl, cb);
|
||||
}
|
||||
else {
|
||||
cb = ttl;
|
||||
redis().ttl(k(name), cb);
|
||||
}
|
||||
}
|
||||
|
||||
function count(cb) {
|
||||
redis.scard('kwikemon:monitors', cb);
|
||||
sweep(function(err) {
|
||||
if (err) return cb(err);
|
||||
redis().scard('kwikemon:monitors', cb);
|
||||
})
|
||||
}
|
||||
|
||||
function sweep(cb) {
|
||||
|
|
@ -138,7 +178,7 @@ function sweep(cb) {
|
|||
if (i == n) cb();
|
||||
}
|
||||
;
|
||||
redis.smembers('kwikemon:monitors', function(err, names) {
|
||||
redis().smembers('kwikemon:monitors', function(err, names) {
|
||||
if (err) return cb(err);
|
||||
n = names.length;
|
||||
if (n == 0) return cb();
|
||||
|
|
@ -160,24 +200,24 @@ function sweep(cb) {
|
|||
function list(cb) {
|
||||
sweep(function(err) {
|
||||
if (err) return cb(err);
|
||||
redis.smembers('kwikemon:monitors', cb);
|
||||
redis().smembers('kwikemon:monitors', cb);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchAll(cb) {
|
||||
function getAll(cb) {
|
||||
var monitors = {};
|
||||
list(function(err, names) {
|
||||
if (err) return cb(err);
|
||||
var fetchers = names.sort().map(function(name) {
|
||||
var geters = names.sort().map(function(name) {
|
||||
return function(done) {
|
||||
fetch(name, function(err, text) {
|
||||
get(name, function(err, text) {
|
||||
if (err) return done(err);
|
||||
monitors[name] = text;
|
||||
done();
|
||||
});
|
||||
};
|
||||
});
|
||||
async.parallel(fetchers, function(err, _) {
|
||||
async.parallel(geters, function(err, _) {
|
||||
if (err) return cb(err);
|
||||
cb(null, monitors)
|
||||
});
|
||||
|
|
@ -185,16 +225,16 @@ function fetchAll(cb) {
|
|||
}
|
||||
|
||||
function remove(name, cb) {
|
||||
redis.multi()
|
||||
redis().multi()
|
||||
.del(k(name))
|
||||
.srem('kwikemon:monitors', name)
|
||||
.exec(cb);
|
||||
}
|
||||
|
||||
function removeAll(cb) {
|
||||
redis.smembers('kwikemon:monitors', function(err, names) {
|
||||
function clear(cb) {
|
||||
redis().smembers('kwikemon:monitors', function(err, names) {
|
||||
if (err) return cb(err);
|
||||
var multi = redis.multi();
|
||||
var multi = redis().multi();
|
||||
names.forEach(function(name) {
|
||||
multi.del(k(name));
|
||||
multi.srem('kwikemon:monitors', name);
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -1,5 +1,5 @@
|
|||
{ "name": "kwikemon"
|
||||
, "version": "0.0.2"
|
||||
, "version": "0.0.6"
|
||||
, "description": "monitor one-off things on your servers"
|
||||
, "author": "Sami Samhuri <sami@samhuri.net>"
|
||||
, "license": "MIT"
|
||||
|
|
@ -11,9 +11,12 @@
|
|||
, "kwikemond": "./bin/kwikemond"
|
||||
}
|
||||
, "dependencies": {
|
||||
"paramify": "0.0.x"
|
||||
"async": "0.2.x"
|
||||
, "express": "3.0.x"
|
||||
, "jade": "0.31.x"
|
||||
, "negotiator": "0.2.x"
|
||||
, "redis": "0.8.x"
|
||||
, "async": "0.2.x"
|
||||
, "toml": "0.4.x"
|
||||
}
|
||||
, "devDependencies": {
|
||||
"mocha": "1.10.x"
|
||||
|
|
@ -22,4 +25,4 @@
|
|||
, "engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
public/css/style.css
Normal file
15
public/css/style.css
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
ul#monitors {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li.monitor {
|
||||
|
||||
}
|
||||
|
||||
#footer {
|
||||
width: 80%;
|
||||
margin: 0.5em auto;
|
||||
padding: 0.3em 1em;
|
||||
text-align: center;
|
||||
border-top: solid 1px #aaa;
|
||||
}
|
||||
5
ruby/Gemfile
Normal file
5
ruby/Gemfile
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'hashie', '~> 2.0.5'
|
||||
gem 'redis', '~> 3.0.4'
|
||||
gem 'toml', '~> 0.0.4'
|
||||
18
ruby/Gemfile.lock
Normal file
18
ruby/Gemfile.lock
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
blankslate (2.1.2.4)
|
||||
hashie (2.0.5)
|
||||
parslet (1.5.0)
|
||||
blankslate (~> 2.0)
|
||||
redis (3.0.4)
|
||||
toml (0.0.4)
|
||||
parslet
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
hashie (~> 2.0.5)
|
||||
redis (~> 3.0.4)
|
||||
toml (~> 0.0.4)
|
||||
17
ruby/kwikemon.gemspec
Normal file
17
ruby/kwikemon.gemspec
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
$LOAD_PATH << File.expand_path('../lib', __FILE__)
|
||||
require 'kwikemon/version'
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'kwikemon'
|
||||
s.version = Kwikemon::VERSION
|
||||
s.license = 'MIT'
|
||||
s.summary = 'Ruby client for kwikemon.'
|
||||
s.description = 'Read & write simple monitors using Redis.'
|
||||
s.author = 'Sami Samhuri'
|
||||
s.email = 'sami@samhuri.net'
|
||||
s.homepage = 'https://github.com/samsonjs/kwikemon'
|
||||
s.require_path = './lib'
|
||||
s.files = ['lib/kwikemon.rb', 'lib/kwikemon/monitor.rb', 'lib/kwikemon/version.rb']
|
||||
s.add_dependency 'redis', '~> 3.0.4'
|
||||
s.required_ruby_version = '>= 1.9.1'
|
||||
end
|
||||
139
ruby/lib/kwikemon.rb
Normal file
139
ruby/lib/kwikemon.rb
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Copyright 2013 Sami Samhuri <sami@samhuri.net>
|
||||
#
|
||||
# MIT License
|
||||
# http://sjs.mit-license.org
|
||||
|
||||
lib_dir = File.expand_path('../', __FILE__)
|
||||
$LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
|
||||
|
||||
require 'hashie'
|
||||
require 'redis'
|
||||
require 'toml'
|
||||
require 'kwikemon/monitor'
|
||||
require 'kwikemon/version'
|
||||
|
||||
module Kwikemon
|
||||
|
||||
extend self
|
||||
|
||||
include Enumerable
|
||||
|
||||
def redis
|
||||
@redis ||= Redis.new(config.redis || {})
|
||||
end
|
||||
|
||||
def redis=(redis)
|
||||
@redis = redis
|
||||
end
|
||||
|
||||
def key_prefix
|
||||
@key_prefix ||= "kwikemon"
|
||||
end
|
||||
|
||||
def key_prefix=(key_prefix)
|
||||
@key_prefix = key_prefix
|
||||
end
|
||||
|
||||
def key(x)
|
||||
"#{key_prefix}:#{x}"
|
||||
end
|
||||
|
||||
Monitor.on(:create) do |name|
|
||||
redis.sadd(key('monitors'), name)
|
||||
end
|
||||
|
||||
Monitor.on(:remove) do |name|
|
||||
redis.srem(key('monitors'), name)
|
||||
end
|
||||
|
||||
|
||||
# Set `name` to `value`.
|
||||
#
|
||||
# @param name [#to_s] name of the monitor
|
||||
# @param text [#to_s] status text
|
||||
def set(name, text)
|
||||
Monitor.new(name, text).save
|
||||
end
|
||||
|
||||
# Check if `name` exists3
|
||||
#
|
||||
# @param name [#to_s] name of the monitor
|
||||
# @return [true, false] true if monitor exists, otherwise false
|
||||
def exists?(name)
|
||||
Monitor.new(name).exists?
|
||||
end
|
||||
|
||||
# Get the value of `name`. Returns `nil` if it doesn't exist.
|
||||
#
|
||||
# @param name [#_tos] name of the monitor
|
||||
# @return [String, nil] status text, or `nil` if it doesn't exist
|
||||
def get(name)
|
||||
Monitor.new(name).text
|
||||
end
|
||||
|
||||
# Get the TTL in seconds of `name`. Returns `nil` if it doesn't exit.
|
||||
#
|
||||
# @param name [#_tos] name of the monitor
|
||||
# @return [String, nil] TTL, or `nil` if it doesn't exist
|
||||
def ttl(name)
|
||||
Monitor.new(name).ttl
|
||||
end
|
||||
|
||||
# Count all monitors.
|
||||
def count
|
||||
redis.scard(key('monitors'))
|
||||
end
|
||||
|
||||
# List all monitor names.
|
||||
def list
|
||||
redis.smembers(key('monitors'))
|
||||
end
|
||||
|
||||
def each
|
||||
list.each { |m| yield(m) }
|
||||
end
|
||||
|
||||
# Get a `Hash` of all monitors.
|
||||
def get_all
|
||||
list.inject({}) do |ms, name|
|
||||
ms[name] = Monitor.new(name).text
|
||||
ms
|
||||
end
|
||||
end
|
||||
|
||||
# Remove the monitor named `name`.
|
||||
def remove(name)
|
||||
Monitor.new(name).remove
|
||||
end
|
||||
|
||||
# Clear all monitors.
|
||||
def clear
|
||||
list.each do |name|
|
||||
remove(name)
|
||||
end
|
||||
end
|
||||
|
||||
# Clean up expired monitors.
|
||||
def sweep
|
||||
list.each do |name|
|
||||
remove(name) unless exists?(name)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def config
|
||||
@config ||= Hashie::Mash.new(load_config)
|
||||
end
|
||||
|
||||
def load_config
|
||||
path = File.join(ENV['HOME'], '.kwikemon.toml')
|
||||
if File.exists?(path)
|
||||
TOML.load_file(path)
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
104
ruby/lib/kwikemon/monitor.rb
Normal file
104
ruby/lib/kwikemon/monitor.rb
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Copyright 2013 Sami Samhuri <sami@samhuri.net>
|
||||
#
|
||||
# MIT License
|
||||
# http://sjs.mit-license.org
|
||||
|
||||
module Kwikemon
|
||||
|
||||
class Monitor
|
||||
|
||||
DefaultTTL = 86400 # 1 day
|
||||
|
||||
attr_accessor :redis
|
||||
attr_reader :name, :text, :ttl, :created, :modified
|
||||
|
||||
@listeners = Hash.new { |h, k| h[k] = [] }
|
||||
|
||||
def Monitor.on(event, &block)
|
||||
@listeners[event] << block
|
||||
end
|
||||
|
||||
def Monitor.emit(event, *args)
|
||||
@listeners[event].each { |handler| handler.call(*args) }
|
||||
end
|
||||
|
||||
def initialize(name, text = nil)
|
||||
@name = name
|
||||
@text = text
|
||||
end
|
||||
|
||||
def save
|
||||
if exists?
|
||||
update(text)
|
||||
else
|
||||
create
|
||||
end
|
||||
end
|
||||
|
||||
def exists?
|
||||
redis.exists(key)
|
||||
end
|
||||
|
||||
def create
|
||||
raise MonitorError.new('name cannot be blank') if name.to_s.strip.length == 0
|
||||
redis.hmset(key, *to_a)
|
||||
self.class.emit(:create, name)
|
||||
self
|
||||
end
|
||||
|
||||
def update(text, ttl = nil)
|
||||
raise MonitorError.new('name cannot be blank') if name.to_s.strip.length == 0
|
||||
redis.hmset(key, 'text', text, 'modified', Time.now.to_i)
|
||||
redis.ttl(key, ttl) if ttl
|
||||
self
|
||||
end
|
||||
|
||||
def remove
|
||||
redis.del(key)
|
||||
self.class.emit(:remove, name)
|
||||
self
|
||||
end
|
||||
|
||||
def key
|
||||
Kwikemon.key("monitor:#{name}")
|
||||
end
|
||||
|
||||
def ttl
|
||||
@ttl ||= exists? ? redis.ttl(key) : nil
|
||||
end
|
||||
|
||||
def created
|
||||
@created ||= exists? ? redis.hget(key, 'created').to_i : nil
|
||||
end
|
||||
|
||||
def modified
|
||||
@modified ||= exists? ? redis.hget(key, 'modified').to_i : nil
|
||||
end
|
||||
|
||||
def text
|
||||
@text ||= exists? ? redis.hget(key, 'text') : nil
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def redis
|
||||
Kwikemon.redis
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{ name: name,
|
||||
text: text,
|
||||
ttl: ttl || DefaultTTL,
|
||||
created: created || Time.now.to_i,
|
||||
modified: modified || Time.now.to_i
|
||||
}
|
||||
end
|
||||
|
||||
def to_a
|
||||
to_hash.to_a
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
3
ruby/lib/kwikemon/version.rb
Normal file
3
ruby/lib/kwikemon/version.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module Kwikemon
|
||||
VERSION = '0.0.9'
|
||||
end
|
||||
65
server.js
65
server.js
|
|
@ -1,65 +0,0 @@
|
|||
// Copyright 2013 Sami Samhuri
|
||||
|
||||
module.exports = {
|
||||
create: create
|
||||
, start: start
|
||||
, stop: stop
|
||||
};
|
||||
|
||||
var http = require('http')
|
||||
, paramify = require('paramify')
|
||||
, kwikemon = require('./kwikemon.js')
|
||||
, _server
|
||||
;
|
||||
|
||||
function create() {
|
||||
return http.createServer(handleRequest);
|
||||
}
|
||||
|
||||
function start(port, host) {
|
||||
port = port || 1111;
|
||||
host = host || '127.0.0.1';
|
||||
_server = create();
|
||||
_server.listen(port, host);
|
||||
console.log('kwikemond listening on ' + host + ':' + port);
|
||||
return _server;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
_server.close();
|
||||
_server = null;
|
||||
}
|
||||
|
||||
function handleRequest(req, res) {
|
||||
var name = req.url.replace(/^\//, '')
|
||||
, type = 'html'
|
||||
, m
|
||||
;
|
||||
if (name == 'favicon.ico') return res.end();
|
||||
if (m = name.match(/\.(json|txt)$/)) {
|
||||
type = m[1];
|
||||
name = name.replace(RegExp('\.' + type + '$'), '');
|
||||
}
|
||||
if (name) {
|
||||
kwikemon.fetch(name, function(err, text) {
|
||||
if (err) {
|
||||
res.end('error: ' + (err.message || 'unknown'));
|
||||
return;
|
||||
}
|
||||
res.end(text);
|
||||
});
|
||||
}
|
||||
// all
|
||||
else {
|
||||
kwikemon.fetchAll(function(err, monitors) {
|
||||
if (err) {
|
||||
res.end('error: ' + (err.message || 'unknown'));
|
||||
return;
|
||||
}
|
||||
Object.keys(monitors).sort().forEach(function(name) {
|
||||
res.write(name + ': ' + monitors[name] + '\n');
|
||||
});
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
53
test.js
53
test.js
|
|
@ -12,14 +12,14 @@ before(function(done) {
|
|||
done();
|
||||
});
|
||||
});
|
||||
beforeEach(kwikemon.removeAll);
|
||||
after(kwikemon.removeAll);
|
||||
beforeEach(kwikemon.clear);
|
||||
after(kwikemon.clear);
|
||||
|
||||
describe("kwikemon", function() {
|
||||
describe("#set", function() {
|
||||
it("should set text", function(done) {
|
||||
kwikemon.set('foo', 'bar', function(err) {
|
||||
kwikemon.fetch('foo', function(err, mon) {
|
||||
kwikemon.get('foo', function(err, mon) {
|
||||
assert(mon.text == 'bar');
|
||||
done();
|
||||
});
|
||||
|
|
@ -28,7 +28,7 @@ describe("kwikemon", function() {
|
|||
|
||||
it("should overwrite text", function(done) {
|
||||
kwikemon.set('foo', 'baz', function(err) {
|
||||
kwikemon.fetch('foo', function(err, mon) {
|
||||
kwikemon.get('foo', function(err, mon) {
|
||||
assert(mon.text == 'baz');
|
||||
done();
|
||||
});
|
||||
|
|
@ -37,7 +37,7 @@ describe("kwikemon", function() {
|
|||
|
||||
it("should set custom ttls", function(done) {
|
||||
kwikemon.set('foo', 'bar', { ttl: 1 }, function(err) {
|
||||
kwikemon.fetchTTL('foo', function(err, ttl) {
|
||||
kwikemon.ttl('foo', function(err, ttl) {
|
||||
assert(ttl <= 1);
|
||||
done();
|
||||
});
|
||||
|
|
@ -46,7 +46,7 @@ describe("kwikemon", function() {
|
|||
|
||||
it("should not expire with a ttl of zero", function(done) {
|
||||
kwikemon.set('foo', 'bar', { ttl: 0 }, function(err) {
|
||||
kwikemon.fetchTTL('foo', function(err, ttl) {
|
||||
kwikemon.ttl('foo', function(err, ttl) {
|
||||
assert(ttl == -1);
|
||||
done();
|
||||
});
|
||||
|
|
@ -55,7 +55,7 @@ describe("kwikemon", function() {
|
|||
|
||||
it("should not expire when ttl is < 0", function(done) {
|
||||
kwikemon.set('foo', 'bar', { ttl: -1 }, function(err) {
|
||||
kwikemon.fetchTTL('foo', function(err, ttl) {
|
||||
kwikemon.ttl('foo', function(err, ttl) {
|
||||
assert(ttl == -1);
|
||||
done();
|
||||
});
|
||||
|
|
@ -92,12 +92,12 @@ describe("kwikemon", function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe("#fetch", function() {
|
||||
it("should fetch the last text monitored", function(done) {
|
||||
describe("#get", function() {
|
||||
it("should get the last text monitored", function(done) {
|
||||
async.series([
|
||||
kwikemon.set('foo', 'bar')
|
||||
, kwikemon.set('foo', 'marcellus')
|
||||
, kwikemon.fetch('foo')
|
||||
, kwikemon.get('foo')
|
||||
],
|
||||
function(err, results) {
|
||||
var mon = results[2];
|
||||
|
|
@ -107,18 +107,18 @@ describe("kwikemon", function() {
|
|||
);
|
||||
});
|
||||
|
||||
it("should fetch null for non-existent monitors", function(done) {
|
||||
kwikemon.fetch('non-existent', function(err, mon) {
|
||||
it("should get null for non-existent monitors", function(done) {
|
||||
kwikemon.get('non-existent', function(err, mon) {
|
||||
assert(mon == null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#fetchTTL", function() {
|
||||
it("should fetch the last TTL set", function(done) {
|
||||
describe("#ttl", function() {
|
||||
it("should get the last TTL set", function(done) {
|
||||
kwikemon.set('foo', 'bar', { ttl: 300 }, function(err) {
|
||||
kwikemon.fetchTTL('foo', function(err, ttl) {
|
||||
kwikemon.ttl('foo', function(err, ttl) {
|
||||
assert(ttl <= 300);
|
||||
done();
|
||||
});
|
||||
|
|
@ -126,20 +126,31 @@ describe("kwikemon", function() {
|
|||
});
|
||||
|
||||
it("should return -1 for non-existent monitors", function(done) {
|
||||
kwikemon.fetchTTL('non-existent', function(err, ttl) {
|
||||
kwikemon.ttl('non-existent', function(err, ttl) {
|
||||
assert(ttl == -1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should set a ttl if given one", function(done) {
|
||||
kwikemon.set('foo', 'bar', function(err) {
|
||||
kwikemon.ttl('foo', 100, function(err) {
|
||||
kwikemon.ttl('foo', function(err, ttl) {
|
||||
assert(ttl <= 100);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#fetchAll", function() {
|
||||
it("should fetch all monitors", function(done) {
|
||||
describe("#getAll", function() {
|
||||
it("should get all monitors", function(done) {
|
||||
async.series([
|
||||
kwikemon.set('a', '1')
|
||||
, kwikemon.set('b', '2')
|
||||
, kwikemon.set('c', '3')
|
||||
, kwikemon.fetchAll
|
||||
, kwikemon.getAll
|
||||
],
|
||||
function(err, results) {
|
||||
var monitors = results.pop()
|
||||
|
|
@ -187,12 +198,12 @@ describe("kwikemon", function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe("#removeAll", function() {
|
||||
describe("#clear", function() {
|
||||
it("should remove the named monitor", function(done) {
|
||||
async.series([
|
||||
kwikemon.set('foo', 'bar')
|
||||
, kwikemon.set('baz', 'quux')
|
||||
, kwikemon.removeAll
|
||||
, kwikemon.clear
|
||||
, kwikemon.exists('foo')
|
||||
, kwikemon.exists('baz')
|
||||
, kwikemon.count
|
||||
|
|
|
|||
3
version.js
Normal file
3
version.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// Copyright 2013 Sami Samhuri
|
||||
|
||||
module.exports = JSON.parse(require('fs').readFileSync(__dirname + '/package.json')).version;
|
||||
4
views/error.jade
Normal file
4
views/error.jade
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
extends layout
|
||||
block content
|
||||
p.
|
||||
error: #{err.message || err}
|
||||
13
views/layout.jade
Normal file
13
views/layout.jade
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
doctype 5
|
||||
html(lang="en")
|
||||
head
|
||||
title kwikemon
|
||||
link(rel="stylesheet", href="/css/style.css")
|
||||
body
|
||||
block nav
|
||||
block title
|
||||
h1= pageTitle || 'kwikemon'
|
||||
#content
|
||||
block content
|
||||
#footer
|
||||
a(href="https://github.com/samsonjs/kwikemon") kwikemon #{version}
|
||||
5
views/monitor.jade
Normal file
5
views/monitor.jade
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
extends layout
|
||||
block nav
|
||||
p: a(href="/") ← monitors
|
||||
block content
|
||||
p= monitor.text
|
||||
5
views/monitors.jade
Normal file
5
views/monitors.jade
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
extends layout
|
||||
block content
|
||||
ul#monitors
|
||||
- each monitor in monitors
|
||||
li.monitor: a(href="/#{monitor.name}") #{monitor.name}: #{monitor.text}
|
||||
Loading…
Reference in a new issue