Compare commits

..

No commits in common. "master" and "v0.0.2" have entirely different histories.

20 changed files with 241 additions and 746 deletions

View file

@ -11,27 +11,28 @@ npm install -g kwikemon
## Usage ## Usage
$ kwikemond & $ kwikemond &
$ curl -s localhost/nginx_status | grep Active | kwikemon write nginx-connections $ curl -s localhost/nginx_status | grep Active | kwikemon nginx-connections
$ curl localhost:1111/nignx-connections $ curl localhost:1111/nignx-connections
Active connections: 316 Active connections: 316
$ kwikemon set foo bar $ kwikemon foo bar
$ curl localhost:1111/ $ curl localhost:1111/
foo: bar foo: bar
nginx-connections: Active connections: 316 nginx-connections: Active connections: 316
Here's how it works: Here's how it works:
- call `kwikemon set thing status` to set the text for the monitor named "thing" - call `kwikemon 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 - fire up the server, `kwikemond`, that serves up these monitors in a big list or individually
Alternatively: Alternatively:
- continuously pipe data to `kwikemon write <name of thing you are watching>` on stdin - continuously pipe data to `kwikemon <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> - 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 `kwikemon help`. To see everything `kwikemon` can do run it without arguments.
$ kwikemon help # or with -h or --help
$ kwikemon
This is very much a work in progress. This is very much a work in progress.
@ -43,12 +44,12 @@ You can use kwikemon as a library.
var kwikemon = require('kiwkemon'); var kwikemon = require('kiwkemon');
kwikemon.set('foo', 'bar', function(err) { kwikemon.set('foo', 'bar', function(err) {
kwikemon.get('foo', function(err, text) { kwikemon.fetch('foo', function(err, text) {
console.log('foo = ' + text); console.log('foo = ' + text);
}); });
}); });
kwikemon.getAll(function(err, monitors) { kwikemon.fetchAll(function(err, monitors) {
Object.keys(monitors).forEach(function (name) { Object.keys(monitors).forEach(function (name) {
console.log(name + ' = ' + monitors[name]); console.log(name + ' = ' + monitors[name]);
}); });
@ -69,14 +70,28 @@ A monitor named `nginx` stores its data in the hash `kwikemon:monitor:nginx`. Ha
are: are:
- text - text
- expire
- created - created
- modified - modified
- updates - updates
The list of all monitors is a set stored at `kwikemon:monitors`. The list of all monitors is a set stored at `kwikemon:monitors`.
#### Set #### 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
exists = redis.exists("kwikemon:monitor:nginx") exists = redis.exists("kwikemon:monitor:nginx")
if exists: if exists:
@ -96,48 +111,11 @@ The list of all monitors is a set stored at `kwikemon:monitors`.
# optional # optional
redis.expire("kwikemon:monitor:nginx", <ttl>) redis.expire("kwikemon:monitor:nginx", <ttl>)
#### Get #### Delete
redis.hgetall("kwikemon:monitor:nginx")
#### Remove
redis.del("kwikemon:monitor:nginx") redis.del("kwikemon:monitor:nginx")
redis.srem("kwikemon:monitors", "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 ## License

116
app.js
View file

@ -1,116 +0,0 @@
// 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
});
});
}

View file

@ -2,166 +2,120 @@
var kwikemon = require('../kwikemon.js'); var kwikemon = require('../kwikemon.js');
var commands = {}; function usage() {
function defineCommand(name, handler, args) { console.log("usage: kwikemon [options]");
commands[name] = { handler: handler, args: args }; console.log(" kwikemon <name> [text]");
} console.log();
defineCommand('clear', clear); console.log("options:");
defineCommand('count', count); console.log(" -c, --clear remove all monitors");
defineCommand('get', get, 'name'); console.log(" -f, --fetch <name> show the text of the named monitor");
defineCommand('help', usage); console.log(" -n, --count count monitors");
defineCommand('list', list); console.log(" -h, --help show what you're reading now");
defineCommand('remove', remove, 'name'); console.log(" -l, --list show all monitors");
defineCommand('set', set, 'name text'); console.log(" -r, --remove <name> remove the named monitor");
defineCommand('sweep', sweep); console.log(" -s, --sweep clean up expired & deleted monitors");
defineCommand('ttl', ttl, 'name [ttl]'); console.log(" -t, --ttl <name> show the TTL of the named monitor");
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);
}
} }
function clear() { var opt = process.argv[2]
kwikemon.clear(function(err) { , name = process.argv[2]
process.exit(0); , text = process.argv[3]
}); ;
} if (opt && opt[0] == '-') {
switch (opt) {
function count() { case '-c':
kwikemon.count(function(err, n) { case '--clear':
console.log(n); kwikemon.removeAll(function(err) {
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); process.exit(0);
} });
}); break;
}
function get(name) { case '-f':
if (name) { case '--fetch':
kwikemon.get(name, function(err, mon) { var name = process.argv[3];
kwikemon.fetch(name, function(err, mon) {
if (mon) { if (mon) {
console.log(mon.text); console.log(mon.text);
process.exit(0); process.exit(0);
} }
else { else {
console.log('no monitor named', name); console.log('error: no monitor named', name);
process.exit(1); process.exit(1);
} }
}); });
} break;
else {
console.log('get requires a name');
process.exit(1);
}
}
function list() { case '-l':
kwikemon.getAll(function(err, monitors) { case '--list':
Object.keys(monitors).forEach(function(name) { kwikemon.fetchAll(function(err, monitors) {
console.log(name + ':', monitors[name].text); 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); process.exit(0);
}); });
} break;
else {
console.log('set requires a name and some text')
process.exit(1);
}
}
function sweep() { case '-n':
kwikemon.sweep(function(err) { case '--count':
process.exit(0); kwikemon.count(function(err, n) {
}); console.log(n);
} process.exit(0);
});
break;
function ttl(name, ttl) { case '-r':
if (ttl) return expire(name, Number(ttl)); case '--remove':
if (name) { name = process.argv[3];
kwikemon.ttl(name, function(err, ttl) { 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) {
if (typeof ttl == 'number') { if (typeof ttl == 'number') {
console.log(ttl); console.log(ttl);
process.exit(0); process.exit(0);
} }
else { else {
console.log('no monitor named', name); console.log('error: no monitor named', name);
process.exit(1); process.exit(1);
} }
}); });
} break;
else {
console.log('ttl requires a name'); default:
process.exit(1); usage();
process.exit(name == '-h' || name == '--help' ? 0 : 1);
} }
} }
else if (name && text) {
function write(name) { kwikemon.set(name, text, function() {
if (name) { process.exit(0);
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) {
if (require.main == module) { process.stdin.pipe(kwikemon.createWriter(name));
main(); process.stdin.on('end', function() {
} process.exit(0);
});
}
else {
usage();
process.exit(1);
}

View file

@ -1,9 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
var port = process.argv[2] || 1111 var server = require('../server.js')
, host = process.argv[3] || '127.0.0.1' , port = process.argv[2]
, app = require('../app.js') , host = process.argv[3]
; ;
app.listen(port, host); server.start(port, host);
console.log('kwikemond listening on ' + host + ':' + port);

View file

@ -8,47 +8,29 @@ module.exports = {
// read // read
, exists: callbackOptional(exists) , exists: callbackOptional(exists)
, get: callbackOptional(get) , fetch: callbackOptional(fetch)
, ttl: callbackOptional(ttl) , fetchTTL: callbackOptional(fetchTTL)
, count: count
, list: list , list: list
, getAll: getAll , fetchAll: fetchAll
, count: count
// remove // remove
, remove: callbackOptional(remove) , remove: callbackOptional(remove)
, clear: clear , removeAll: removeAll
, sweep: sweep , sweep: sweep
// change redis client // change redis client
, redis: redis , redis: setRedis
}; };
var async = require('async') var async = require('async')
, fs = require('fs') , redis = require('redis').createClient()
, Redis = require('redis')
, redisClient
, toml = require('toml')
, LineEmitter = require('./line_emitter.js') , LineEmitter = require('./line_emitter.js')
; ;
function redis(newRedis) { function setRedis(newRedis) {
if (newRedis){ if (redis) redis.end();
if (redisClient) redisClient.end(); redis = newRedis;
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. // Make the callback argument of a function optional.
@ -57,15 +39,15 @@ function redis(newRedis) {
// that accepts the callback is returned, with the // that accepts the callback is returned, with the
// rest of the arguments fixed (like bind). // rest of the arguments fixed (like bind).
// //
// function get(id, cb) { db.get(id, cb); } // function fetch(id, cb) { db.fetch(id, cb); }
// get = callbackOptional(get); // fetch = callbackOptional(fetch);
// //
// function print(err, x) { if (err) throw err; console.log(x); } // function print(err, x) { if (err) throw err; console.log(x); }
// //
// get(1, print); // fetch(1, print);
// //
// var get1 = get(1); // var fetch1 = fetch(1);
// get1(print); // fetch1(print);
function callbackOptional(fn, ctx) { function callbackOptional(fn, ctx) {
return function() { return function() {
var args = Array.prototype.slice.call(arguments); var args = Array.prototype.slice.call(arguments);
@ -87,7 +69,7 @@ function k(name) {
} }
function exists(name, cb) { function exists(name, cb) {
redis().exists(k(name), function(err, exists) { redis.exists(k(name), function(err, exists) {
if (err) return cb(err); if (err) return cb(err);
cb(null, exists == 1); cb(null, exists == 1);
}); });
@ -106,11 +88,10 @@ function set(name, text, options, cb) {
; ;
exists(name, function(err, exists) { exists(name, function(err, exists) {
var fields = { var fields = {
name: name text: text
, text: text
, modified: Date.now() , modified: Date.now()
} }
, multi = redis().multi() , multi = redis.multi()
; ;
if (!exists) { if (!exists) {
fields.created = Date.now(); fields.created = Date.now();
@ -137,37 +118,16 @@ function writer(name) {
return le; return le;
} }
function get(name, cb) { function fetch(name, cb) {
redis().hgetall(k(name), cb); redis.hgetall(k(name), cb);
} }
function expire(name, ttl, cb) { function fetchTTL(name, cb) {
exists(name, function(err, exists) { redis.ttl(k(name), cb);
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) { function count(cb) {
sweep(function(err) { redis.scard('kwikemon:monitors', cb);
if (err) return cb(err);
redis().scard('kwikemon:monitors', cb);
})
} }
function sweep(cb) { function sweep(cb) {
@ -178,7 +138,7 @@ function sweep(cb) {
if (i == n) cb(); if (i == n) cb();
} }
; ;
redis().smembers('kwikemon:monitors', function(err, names) { redis.smembers('kwikemon:monitors', function(err, names) {
if (err) return cb(err); if (err) return cb(err);
n = names.length; n = names.length;
if (n == 0) return cb(); if (n == 0) return cb();
@ -200,24 +160,24 @@ function sweep(cb) {
function list(cb) { function list(cb) {
sweep(function(err) { sweep(function(err) {
if (err) return cb(err); if (err) return cb(err);
redis().smembers('kwikemon:monitors', cb); redis.smembers('kwikemon:monitors', cb);
}); });
} }
function getAll(cb) { function fetchAll(cb) {
var monitors = {}; var monitors = {};
list(function(err, names) { list(function(err, names) {
if (err) return cb(err); if (err) return cb(err);
var geters = names.sort().map(function(name) { var fetchers = names.sort().map(function(name) {
return function(done) { return function(done) {
get(name, function(err, text) { fetch(name, function(err, text) {
if (err) return done(err); if (err) return done(err);
monitors[name] = text; monitors[name] = text;
done(); done();
}); });
}; };
}); });
async.parallel(geters, function(err, _) { async.parallel(fetchers, function(err, _) {
if (err) return cb(err); if (err) return cb(err);
cb(null, monitors) cb(null, monitors)
}); });
@ -225,16 +185,16 @@ function getAll(cb) {
} }
function remove(name, cb) { function remove(name, cb) {
redis().multi() redis.multi()
.del(k(name)) .del(k(name))
.srem('kwikemon:monitors', name) .srem('kwikemon:monitors', name)
.exec(cb); .exec(cb);
} }
function clear(cb) { function removeAll(cb) {
redis().smembers('kwikemon:monitors', function(err, names) { redis.smembers('kwikemon:monitors', function(err, names) {
if (err) return cb(err); if (err) return cb(err);
var multi = redis().multi(); var multi = redis.multi();
names.forEach(function(name) { names.forEach(function(name) {
multi.del(k(name)); multi.del(k(name));
multi.srem('kwikemon:monitors', name); multi.srem('kwikemon:monitors', name);

View file

@ -1,5 +1,5 @@
{ "name": "kwikemon" { "name": "kwikemon"
, "version": "0.0.6" , "version": "0.0.2"
, "description": "monitor one-off things on your servers" , "description": "monitor one-off things on your servers"
, "author": "Sami Samhuri <sami@samhuri.net>" , "author": "Sami Samhuri <sami@samhuri.net>"
, "license": "MIT" , "license": "MIT"
@ -11,12 +11,9 @@
, "kwikemond": "./bin/kwikemond" , "kwikemond": "./bin/kwikemond"
} }
, "dependencies": { , "dependencies": {
"async": "0.2.x" "paramify": "0.0.x"
, "express": "3.0.x"
, "jade": "0.31.x"
, "negotiator": "0.2.x"
, "redis": "0.8.x" , "redis": "0.8.x"
, "toml": "0.4.x" , "async": "0.2.x"
} }
, "devDependencies": { , "devDependencies": {
"mocha": "1.10.x" "mocha": "1.10.x"
@ -25,4 +22,4 @@
, "engines": { , "engines": {
"node": ">=0.10" "node": ">=0.10"
} }
} }

View file

@ -1,15 +0,0 @@
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;
}

View file

@ -1,5 +0,0 @@
source 'https://rubygems.org'
gem 'hashie', '~> 2.0.5'
gem 'redis', '~> 3.0.4'
gem 'toml', '~> 0.0.4'

View file

@ -1,18 +0,0 @@
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)

View file

@ -1,17 +0,0 @@
$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

View file

@ -1,139 +0,0 @@
# 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

View file

@ -1,104 +0,0 @@
# 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

View file

@ -1,3 +0,0 @@
module Kwikemon
VERSION = '0.0.9'
end

65
server.js Normal file
View file

@ -0,0 +1,65 @@
// 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
View file

@ -12,14 +12,14 @@ before(function(done) {
done(); done();
}); });
}); });
beforeEach(kwikemon.clear); beforeEach(kwikemon.removeAll);
after(kwikemon.clear); after(kwikemon.removeAll);
describe("kwikemon", function() { describe("kwikemon", function() {
describe("#set", function() { describe("#set", function() {
it("should set text", function(done) { it("should set text", function(done) {
kwikemon.set('foo', 'bar', function(err) { kwikemon.set('foo', 'bar', function(err) {
kwikemon.get('foo', function(err, mon) { kwikemon.fetch('foo', function(err, mon) {
assert(mon.text == 'bar'); assert(mon.text == 'bar');
done(); done();
}); });
@ -28,7 +28,7 @@ describe("kwikemon", function() {
it("should overwrite text", function(done) { it("should overwrite text", function(done) {
kwikemon.set('foo', 'baz', function(err) { kwikemon.set('foo', 'baz', function(err) {
kwikemon.get('foo', function(err, mon) { kwikemon.fetch('foo', function(err, mon) {
assert(mon.text == 'baz'); assert(mon.text == 'baz');
done(); done();
}); });
@ -37,7 +37,7 @@ describe("kwikemon", function() {
it("should set custom ttls", function(done) { it("should set custom ttls", function(done) {
kwikemon.set('foo', 'bar', { ttl: 1 }, function(err) { kwikemon.set('foo', 'bar', { ttl: 1 }, function(err) {
kwikemon.ttl('foo', function(err, ttl) { kwikemon.fetchTTL('foo', function(err, ttl) {
assert(ttl <= 1); assert(ttl <= 1);
done(); done();
}); });
@ -46,7 +46,7 @@ describe("kwikemon", function() {
it("should not expire with a ttl of zero", function(done) { it("should not expire with a ttl of zero", function(done) {
kwikemon.set('foo', 'bar', { ttl: 0 }, function(err) { kwikemon.set('foo', 'bar', { ttl: 0 }, function(err) {
kwikemon.ttl('foo', function(err, ttl) { kwikemon.fetchTTL('foo', function(err, ttl) {
assert(ttl == -1); assert(ttl == -1);
done(); done();
}); });
@ -55,7 +55,7 @@ describe("kwikemon", function() {
it("should not expire when ttl is < 0", function(done) { it("should not expire when ttl is < 0", function(done) {
kwikemon.set('foo', 'bar', { ttl: -1 }, function(err) { kwikemon.set('foo', 'bar', { ttl: -1 }, function(err) {
kwikemon.ttl('foo', function(err, ttl) { kwikemon.fetchTTL('foo', function(err, ttl) {
assert(ttl == -1); assert(ttl == -1);
done(); done();
}); });
@ -92,12 +92,12 @@ describe("kwikemon", function() {
}); });
}); });
describe("#get", function() { describe("#fetch", function() {
it("should get the last text monitored", function(done) { it("should fetch the last text monitored", function(done) {
async.series([ async.series([
kwikemon.set('foo', 'bar') kwikemon.set('foo', 'bar')
, kwikemon.set('foo', 'marcellus') , kwikemon.set('foo', 'marcellus')
, kwikemon.get('foo') , kwikemon.fetch('foo')
], ],
function(err, results) { function(err, results) {
var mon = results[2]; var mon = results[2];
@ -107,18 +107,18 @@ describe("kwikemon", function() {
); );
}); });
it("should get null for non-existent monitors", function(done) { it("should fetch null for non-existent monitors", function(done) {
kwikemon.get('non-existent', function(err, mon) { kwikemon.fetch('non-existent', function(err, mon) {
assert(mon == null); assert(mon == null);
done(); done();
}); });
}); });
}); });
describe("#ttl", function() { describe("#fetchTTL", function() {
it("should get the last TTL set", function(done) { it("should fetch the last TTL set", function(done) {
kwikemon.set('foo', 'bar', { ttl: 300 }, function(err) { kwikemon.set('foo', 'bar', { ttl: 300 }, function(err) {
kwikemon.ttl('foo', function(err, ttl) { kwikemon.fetchTTL('foo', function(err, ttl) {
assert(ttl <= 300); assert(ttl <= 300);
done(); done();
}); });
@ -126,31 +126,20 @@ describe("kwikemon", function() {
}); });
it("should return -1 for non-existent monitors", function(done) { it("should return -1 for non-existent monitors", function(done) {
kwikemon.ttl('non-existent', function(err, ttl) { kwikemon.fetchTTL('non-existent', function(err, ttl) {
assert(ttl == -1); assert(ttl == -1);
done(); 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("#getAll", function() { describe("#fetchAll", function() {
it("should get all monitors", function(done) { it("should fetch all monitors", function(done) {
async.series([ async.series([
kwikemon.set('a', '1') kwikemon.set('a', '1')
, kwikemon.set('b', '2') , kwikemon.set('b', '2')
, kwikemon.set('c', '3') , kwikemon.set('c', '3')
, kwikemon.getAll , kwikemon.fetchAll
], ],
function(err, results) { function(err, results) {
var monitors = results.pop() var monitors = results.pop()
@ -198,12 +187,12 @@ describe("kwikemon", function() {
}); });
}); });
describe("#clear", function() { describe("#removeAll", function() {
it("should remove the named monitor", function(done) { it("should remove the named monitor", function(done) {
async.series([ async.series([
kwikemon.set('foo', 'bar') kwikemon.set('foo', 'bar')
, kwikemon.set('baz', 'quux') , kwikemon.set('baz', 'quux')
, kwikemon.clear , kwikemon.removeAll
, kwikemon.exists('foo') , kwikemon.exists('foo')
, kwikemon.exists('baz') , kwikemon.exists('baz')
, kwikemon.count , kwikemon.count

View file

@ -1,3 +0,0 @@
// Copyright 2013 Sami Samhuri
module.exports = JSON.parse(require('fs').readFileSync(__dirname + '/package.json')).version;

View file

@ -1,4 +0,0 @@
extends layout
block content
p.
error: #{err.message || err}

View file

@ -1,13 +0,0 @@
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}

View file

@ -1,5 +0,0 @@
extends layout
block nav
p: a(href="/") &larr; monitors
block content
p= monitor.text

View file

@ -1,5 +0,0 @@
extends layout
block content
ul#monitors
- each monitor in monitors
li.monitor: a(href="/#{monitor.name}") #{monitor.name}: #{monitor.text}