Skip to content

Instantly share code, notes, and snippets.

@eric
Created March 18, 2011 20:20
Show Gist options
  • Save eric/876762 to your computer and use it in GitHub Desktop.
Save eric/876762 to your computer and use it in GitHub Desktop.
A simple mechanism to log metrics to Redis

Redis Timeseries Metric

I've been playing around with trying to make it as simple as possible to start logging a few metrics with as little effort in setup as possible.

As I was working on Papertrail I came up with a couple interesting ideas, some of which were inspired by OpenTSDB.

Storing metrics

In this example we are pretending we're processing emails as background jobs and would like to track how many we've sent over time.

First, we initialize a metric:

@metric = RedisTimeseriesMetric.new('emails', %w(hostname))

The counter is called "emails" and has a single tag called "hostname".

When processing the emails in our background job, we would invoke:

@metric.incr(1, Time.now, :hostname => Socket.gethostname)

to increment the counter.

Retrieving metrics

To retrieve an overall count of emails that have been sent over the past day, we would use:

@metric.count(1.day.ago, Time.now)

To retrieve the count of emails that have been sent by a specific server:

@metric.count(1.day.ago, Time.now, :hostname => 'email1')

To retrieve a list of per hour email counts over the past day:

@metric.timeseries(1.day.ago, Time.now, :hostname => 'email1')

What if you wanted to graph a sparkline of the traffic? We could use jquery.sparkline.js:

<span class="sparkline" values="<%= @metric.timeseries(1.day.ago, Time.now).join(',') %>"></span>
<script type="text/javascript">$('sparkline').sparkline('html');</script>

Future

There are a number of interesting things that could be added to this:

  1. Time-based roll-ups to quickly get results based on daily measurements
  2. Ability to store counters that are read from other sources (like interface octets)
  3. Limit storage to so it doesn't grow endlessly
class RedisTimeseriesMetric
cattr_accessor :redis
self.redis ||= $redis
# Public: Initialize a new metric
#
# metric_name - The String metric name that is used as part of the
# redis key
# tags - An Array of Strings of required tags to be provided
# with all calls to `incr()`
# options - The Hash options used to provide an interval (default: {}):
# :interval - The Integer time interval to round metrics
# to (default: 1.hour)
#
# Examples
#
# @metric = RedisTimeseriesMetric.new('emails', %w(hostname))
#
def initialize(metric_name, *tags)
options = tags.pop if tags.last.is_a?(Hash)
@metric_name = metric_name
@tags = tags.flatten.sort
@interval = options[:interval] || 1.hour.to_i
end
# Public: Increment the metric
#
# val - The Integer of how much to increment the metric by (default: 1)
# time - The Time the increment happened in (default: Time.now)
# tags - A Hash of the tags and values to attribute the `val` to (optional)
#
# Examples
#
# @metric.incr(1, Time.now, :hostname => Socket.gethostname)
#
# Returns nothing.
def incr(val = 1, time = Time.now, tags = {})
# Round the time to our current rounding interval
time -= time % @interval
if !(missing_tags = @tags - tags.keys).empty?
raise "Required tags were not found: #{missing_tags * ', '}"
end
tags.each do |k, v|
redis.incrby "#{@metric_name}:#{k}=#{v}:#{time.to_i}", val
end
redis.incrby "#{@metric_name}:#{time.to_i}", val
end
# Public: Retrieve a sum of the counter for a given time range
#
# start_time - The Time to start retrieving metrics
# end_time - The Time to stop retrieving metrics
# tags - A Hash of the tags and values to gather (optional)
#
# Examples
#
# @metric.count(1.day.ago, Time.now)
# # => 29382
#
# @metric.count(1.day.ago, Time.now, :hostname => 'email1')
# # => 1827
#
# Returns the Integer sum of the metrics.
def count(start_time, end_time, tags = {})
raise "Cannot specify more than one tag" if tags.length > 1
keys, num_targets = *keylist(start_time, end_time, tags)
redis.mget(*keys).sum { |i| i.to_i }
end
# Public: Retrieve a timeseries of the counter for a given time range
#
# start_time - The Time to start retrieving metrics
# end_time - The Time to stop retrieving metrics
# tags - A Hash of the tags and values to gather (optional)
#
# Examples
#
# @metric.timeseries(1.day.ago, Time.now)
# # => [ 2, 7, 0, 82, 7, ... , 3 ]
#
# @metric.timeseries(1.day.ago, Time.now, :hostname => 'email1')
# # => [ 1, 3, 0, 23, 3, ..., 1 ]
#
# Returns the Array of Integers for each time unit for the metric.
def timeseries(start_time, end_time, tags = {})
raise "Cannot specify more than one tag" if tags.length > 1
keys, num_targets = *keylist(start_time, end_time, tags)
redis.mget(*keys).each_slice(num_targets).collect { |s| s.sum { |i| i.to_i } }
end
private
def keylist(start_time, end_time, tags = {})
raise "Cannot specify more than one tag" if tags.length > 1
times = timerange(start_time, end_time)
if tags.empty?
keys = times.collect do |time|
"#{@metric_name}:#{time}"
end
else
key, values = *tags.first
keys = times.collect do |time|
Array(values).collect do |value|
"#{@metric_name}:#{key}=#{value}:#{time}"
end
end
end
[ keys, tags.empty? ? 1 : Array(values).length ]
end
def timerange(start_time, end_time)
start_time, end_time = start_time.to_i, end_time.to_i
start_time -= start_time % @interval if start_time % @interval != 0
end_time -= end_time % @interval if end_time % @interval != 0
Range.new(start_time, end_time).step(@interval)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment