redis-timeseries-values
Version:
Manage timeseries data storage in Redis with ease
201 lines (166 loc) • 6.71 kB
JavaScript
var TimeSeries = module.exports = function(redis, keyBase, granularities) {
this.redis = redis;
this.keyBase = keyBase || 'stats';
this.pendingMulti = redis.multi();
this.granularities = granularities || {
'1second' : { ttl: this.minutes(5), duration: 1 },
'1minute' : { ttl: this.hours(1) , duration: this.minutes(1) },
'5minutes' : { ttl: this.days(1) , duration: this.minutes(5) },
'10minutes': { ttl: this.days(1) , duration: this.minutes(10) },
'1hour' : { ttl: this.days(7) , duration: this.hours(1) },
'1day' : { ttl: this.weeks(52) , duration: this.days(1) }
};
};
/** Helper functions that compute various durations in secs */
TimeSeries.prototype.seconds = function(i) { return i; };
TimeSeries.prototype.minutes = function(i) { return i*60; };
TimeSeries.prototype.hours = function(i) { return i*this.minutes(60); };
TimeSeries.prototype.days = function(i) { return i*this.hours(24); };
TimeSeries.prototype.weeks = function(i) { return i*this.days(7); };
TimeSeries.prototype.months = function(i) { return i*(this.weeks(4)+this.days(2)); };
/**
* Record a hit for the specified stats key
* This method is chainable:
* --> var ts = new TimeSeries(redis)
* .recordHit("messages")
* .recordHit("purchases", ts)
* .recordHit("purchases", ts, 3)
* ...
* .exec([callback]);
*
* `timestamp` should be in seconds, and defaults to current time.
* `increment` should be an integer, and defaults to 1
*/
TimeSeries.prototype.recordHit = function(key, timestamp, increment) {
var self = this;
Object.keys(this.granularities).forEach(function(gran) {
var properties = self.granularities[gran],
keyTimestamp = getRoundedTime(properties.ttl, timestamp),
tmpKey = [self.keyBase, key, gran, keyTimestamp].join(':'),
hitTimestamp = getRoundedTime(properties.duration, timestamp);
self.pendingMulti.hincrby(tmpKey, hitTimestamp, Math.floor(increment || 1));
self.pendingMulti.expireat(tmpKey, keyTimestamp + 2 * properties.ttl);
});
return this;
};
/*
* Removing hits
*
* This decrements the counters for the
* stats keys you provide
*
* "timestamp" defaults to the current time
* "decrement" defaults to -1
*
* ts.removeHit('your_stats_key')
* .removeHit('another_stats_key', timestamp)
* .removeHit('another_stats_key', timestamp2, decrement)
* …
* .exec();
*/
TimeSeries.prototype.removeHit = function(key, timestamp, decrement) {
return this.recordHit(key, timestamp, -(decrement || 1));
};
/**
* Execute the current pending redis multi
*/
TimeSeries.prototype.exec = function(callback) {
// Reset pendingMulti before proceeding to
// avoid concurrent modifications
var current = this.pendingMulti;
this.pendingMulti = this.redis.multi();
current.exec(callback);
};
/**
* getHits("messages", "10minutes", 3, cb)
* --> "messages" hits during the last 3 '10minutes' chunks
*/
TimeSeries.prototype.getHits = function(key, gran, count, callback) {
var properties = this.granularities[gran],
currentTime = getCurrentTime();
if (typeof properties === "undefined") {
return callback(new Error("Unsupported granularity: "+gran));
}
if (count > properties.ttl / properties.duration) {
return callback(new Error("Count: "+count+" exceeds the maximum stored slots for granularity: "+gran));
}
var from = getRoundedTime(properties.duration, currentTime - count*properties.duration),
to = getRoundedTime(properties.duration, currentTime);
for(var ts=from, multi=this.redis.multi(); ts<=to; ts+=properties.duration) {
var keyTimestamp = getRoundedTime(properties.ttl, ts),
tmpKey = [this.keyBase, key, gran, keyTimestamp].join(':');
multi.hget(tmpKey, ts);
}
multi.exec(function(err, results) {
if (err) {
return callback(err);
}
for(var ts=from, i=0, data=[]; ts<=to; ts+=properties.duration, i+=1) {
data.push([ts, results[i] ? parseInt(results[i], 10) : 0]);
}
return callback(null, data.slice(Math.max(data.length - count, 0)));
});
};
/**
* Record a value for the specified stats key
* This method is chainable:
* --> var ts = new TimeSeries(redis)
* .recordValue("speed", ts, 42)
* .recordValue("rpm", ts, 7800)
* ...
* .exec([callback]);
*
* `timestamp` should be in seconds, and defaults to current time.
* `value` should be an integer, and defaults to 0
*/
TimeSeries.prototype.recordValue = function(key, timestamp, value) {
var self = this;
Object.keys(this.granularities).forEach(function(gran) {
var properties = self.granularities[gran],
keyTimestamp = getRoundedTime(properties.ttl, timestamp),
tmpKey = [self.keyBase, key, gran, keyTimestamp].join(':'),
hitTimestamp = getRoundedTime(properties.duration, timestamp);
self.pendingMulti.hset(tmpKey, hitTimestamp, Math.floor(value || 0));
self.pendingMulti.expireat(tmpKey, keyTimestamp + 2 * properties.ttl);
});
return this;
};
/**
* getValues("speed", "1minute", 60, cb)
* --> "speed" values for the last hour's 1 minute readings
*/
TimeSeries.prototype.getValues = function(key, gran, count, callback) {
var properties = this.granularities[gran],
currentTime = getCurrentTime();
if (typeof properties === "undefined") {
return callback(new Error("Unsupported granularity: "+gran));
}
if (count > properties.ttl / properties.duration) {
return callback(new Error("Count: "+count+" exceeds the maximum stored slots for granularity: "+gran));
}
var from = getRoundedTime(properties.duration, currentTime - count*properties.duration),
to = getRoundedTime(properties.duration, currentTime);
for(var ts=from, multi=this.redis.multi(); ts<=to; ts+=properties.duration) {
var keyTimestamp = getRoundedTime(properties.ttl, ts),
tmpKey = [this.keyBase, key, gran, keyTimestamp].join(':');
multi.hget(tmpKey, ts);
}
multi.exec(function(err, results) {
if (err) {
return callback(err);
}
for(var ts=from, i=0, data=[]; ts<=to; ts+=properties.duration, i+=1) {
data.push([ts, results[i] ? parseInt(results[i], 10) : null]);
}
return callback(null, data.slice(Math.max(data.length - count, 0)));
});
};
// Get current timestamp in seconds
var getCurrentTime = function() {
return Math.floor(Date.now() / 1000);
};
// Round timestamp to the 'precision' interval (in seconds)
var getRoundedTime = function(precision, time) {
time = time || getCurrentTime();
return Math.floor(time / precision) * precision;
};