Skip to content

Commit

Permalink
Basic UI to show timeseries data.
Browse files Browse the repository at this point in the history
d3 bar graphs plus typical resque table UI for performed and enqueued
jobs.
Include d3 in gem.
  • Loading branch information
lukeasrodgers committed Nov 11, 2016
1 parent bd95e19 commit f29d90b
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 3 deletions.
8 changes: 8 additions & 0 deletions examples/sinatra/Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ task :enqueue_failure do
Resque.enqueue(FailingJob, {})
end
end

desc "Enqueue a random sample of jobs"
task :enqueue_random_sample do
job_klasses = Object.constants.select {|s| s.to_s.match(/Job\z/)}
job_klasses.each do |klass|
(rand(5) + 1).times { Resque.enqueue(Object.const_get(klass), {}) }
end
end
41 changes: 39 additions & 2 deletions lib/resque-job-stats/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Plugins
module JobStats
module Server
VIEW_PATH = File.join(File.dirname(__FILE__), 'server', 'views')
PUBLIC_PATH = File.join(File.dirname(__FILE__), 'server', 'public')

def job_stats_to_display
@job_stats_to_display ||= Resque::Plugins::JobStats::Statistic::DEFAULT_STATS
Expand Down Expand Up @@ -48,13 +49,26 @@ def display_stat(stat, stat_name, format)
def check_or_cross_stat(value)
value ? "✓" : "✗"
end

def render_public_file(filename)
file = File.join(PUBLIC_PATH, filename)
cache_control :public, :max_age => 86400
send_file file
rescue Errno::ENOENT
status 404
end
end

class << self
def registered(app)
app.get '/job_stats' do
@jobs = Resque::Plugins::JobStats::Statistic.find_all(self.class.job_stats_to_display).sort
erb(File.read(File.join(VIEW_PATH, 'job_stats.erb')))
key = params.keys.first
if key == 'js'
render_public_file(params[key])
else
@jobs = Resque::Plugins::JobStats::Statistic.find_all(self.class.job_stats_to_display).sort
erb(File.read(File.join(VIEW_PATH, 'job_stats.erb')))
end
end
# We have little choice in using this funky name - Resque
# already has a "Stats" tab, and it doesn't like
Expand All @@ -76,6 +90,29 @@ def registered(app)
erb(File.read(File.join(VIEW_PATH, 'job_histories.erb')))
end

app.get '/job_stats/timeseries/:job_class' do
@job_class = Resque::Plugins::JobStats.measured_jobs.find { |j| j.to_s == params[:job_class] }
pass unless @job_class

@interval = params[:interval] == 'minute' ? 'minute' : 'hour'
other_interval_label = params[:interval] == 'minute' ? 'hour' : 'minute'
@toggle_interval_url = "#{request.path_info}?interval=#{other_interval_label}"

if @job_class.respond_to?("queued_per_#{@interval}")
@enqueued = @job_class.public_send("queued_per_#{@interval}").to_a
else
@enqueued = nil
end

if @job_class.respond_to?("performed_per_#{@interval}")
@performed = @job_class.public_send("performed_per_#{@interval}").to_a
else
@performed = nil
end

erb(File.read(File.join(VIEW_PATH, 'job_timeseries.erb')))
end

app.helpers(Helpers)
end
end
Expand Down
8 changes: 8 additions & 0 deletions lib/resque-job-stats/server/public/d3.v4.min.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions lib/resque-job-stats/server/views/job_stats.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
job.job_class.is_a?(Resque::Plugins::JobStats::History) %>
<a href='<%= url "/job_history/#{job.name}" %>'>[history]</a>
<% end %>
<% if job.job_class <= Resque::Plugins::JobStats::Timeseries::Enqueued ||
job.job_class.is_a?(Resque::Plugins::JobStats::Timeseries::Enqueued) ||
job.job_class <= Resque::Plugins::JobStats::Timeseries::Performed ||
job.job_class.is_a?(Resque::Plugins::JobStats::Timeseries::Performed) %>
<a href='<%= url "/job_stats/timeseries/#{job.name}" %>'>[timeseries]</a>
<% end %>
</td>
<%= display_stat(job, :jobs_enqueued, :number_display) %>
<%= display_stat(job, :jobs_performed, :number_display) %>
Expand Down
179 changes: 179 additions & 0 deletions lib/resque-job-stats/server/views/job_timeseries.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<h1>Resque Job Timeseries</h1>

<p class="intro">
This page displays timeseries data for jobs.
</p>

<h2><code><%= @job_class %></code></h2>
<% if @interval == 'hour' %>
<p>View by: <strong style="font-weight: bold">hour</strong> | <a href="<%= @toggle_interval_url %>">minute</a></p>
<% else %>
<p>View by: <a href="<%= @toggle_interval_url %>">hour</a> | <strong style="font-weight: bold">minute</strong></p>
<% end %>

<svg style="margin: 1em 0;" id="performed-graph"></svg>
<svg style="margin: 1em 0;" id="enqueued-graph"></svg>

<% if @performed %>
<h3 style="font-size: 1.2em; margin: 2em 0 1em;">Performed</h3>
<table>
<tr>
<th>Time</th>
<th>Count</th>
</tr>
<% @performed.each do |k, v| %>
<tr>
<td><span class="time"><%= k %></span></td>
<td><%= v == 0 ? '-' : v %></td>
</tr>
<% end %>
</table>
<% end %>
<% if @enqueued %>
<h3 style="font-size: 1.2em; margin: 2em 0 1em;">Enqueued</h3>
<table>
<tr>
<th>Time</th>
<th>Count</th>
</tr>
<% @enqueued.each do |k, v| %>
<tr>
<td><span class="time"><%= k %></span></td>
<td><%= v == 0 ? '-' : v %></td>
</tr>
<% end %>
</table>
<% end %>

<script>
var enqueued = <%= @enqueued.to_json %>;
var performed = <%= @performed.to_json %>;
var interval = "<%= @interval %>";

$(document).ready(function() {
'use strict';

var parseDate = d3.isoParse;

var TimeseriesGraph = function(rawData, title, interval) {
this.graphTitle = title;
this.padding = {top: 20, right: 20, bottom: 20, left: 40};
this.margin = {top: 30, right: 20, bottom: 10, left: 50};
this.width = 600 - this.margin.left - this.margin.right - this.padding.left - this.padding.right;
this.height = 270 - this.margin.top - this.margin.bottom - this.padding.top - this.padding.bottom;
this.outerWidth = this.width + this.margin.left + this.margin.right + this.padding.left + this.padding.right;
this.outerHeight = this.height + this.margin.top + this.margin.bottom + this.padding.top + this.padding.bottom;

this.interval = interval;

this.data = rawData.map(function(tuple) {
return {
time: parseDate(tuple[0]),
count: tuple[1]
};
});

var xExtent = d3.extent(this.data, function(d) { return d.time; });
var domain = this.data.map(function(d) { return d.time; });

if (this.interval === 'hour') {
var paddingFn = d3.timeHour;
}
else {
var paddingFn = d3.timeMinute;
}

var xMin = xExtent[0];
var xMax = xExtent[1];
this.xExtentPadded = [
paddingFn.offset(xMin, -1),
paddingFn.offset(xMax, 1)
];

// Pad x domain so that bars don't go over the rightmost edge.
this.paddedData = domain.concat(this.xExtentPadded);
};

TimeseriesGraph.prototype.drawGraph = function(selector) {
var self = this;
$(selector).empty();
var x = d3.scaleTime().range([0, this.width]);
var y = d3.scaleLinear().range([this.height, 0]);

var ordinalXScale = d3.scaleBand().domain(this.paddedData).range([0, this.width]);

var svg = d3.select(selector)
.attr("width", this.outerWidth)
.attr("height", this.outerHeight);
var g = svg.append("g")
.attr("transform", "translate(" + this.padding.left + "," + this.padding.top + ")");

var yMax = d3.max(this.data, function(d) { return d.count; });

x.domain(this.xExtentPadded);
if (yMax > 0) {
y.domain([0, yMax]);
}
else {
y.domain([0, 5]);
}

var xAxis = g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + this.height + ")")
.call(d3.axisBottom(x));

var yAxis = g.append("g").attr("class", "axis axis--y");

if (yMax < 10 && yMax > 0) {
yAxis.call(d3.axisLeft(y).ticks(yMax, 'd'));
}
else if (yMax === 0) {
yAxis.call(d3.axisLeft(y).ticks(5, 'd'));
}
else {
yAxis.call(d3.axisLeft(y).ticks(8, 'd'));
}

svg.append("text")
.attr('text-anchor', 'middle')
.attr('transform', 'translate('+ (this.width / 2 + this.padding.left) +','+ this.padding.top / 2 +')')
.text(this.graphTitle);

svg.append("text")
.attr('text-anchor', 'middle')
.attr("transform", "translate(10,"+(this.height/2+this.padding.top)+")rotate(-90)")
.text("Jobs");

svg.append("text")
.attr('text-anchor', 'middle')
.attr("transform", "translate("+ (this.width / 2 + this.padding.left) +","+ (this.outerHeight - this.margin.bottom) +")")
.text("Time");

var barSpacing = 1;
g.selectAll(".bar")
.data(this.data)
.enter().append("rect")
.attr('fill', '#555')
.attr("class", "bar")
.attr("x", function(d) { return x(d.time); })
.attr("y", function(d) { return y(d.count); })
.attr("width", ordinalXScale.bandwidth() - barSpacing)
.attr("height", function(d) { return self.height - y(d.count); });
};

if (performed) {
var performedGraph = new TimeseriesGraph(performed, 'Performed', interval);
performedGraph.drawGraph('#performed-graph');
}

if (enqueued) {
var enqueuedGraph = new TimeseriesGraph(enqueued, 'Enqueued', interval);
enqueuedGraph.drawGraph('#enqueued-graph');
}

});

</script>
<script src="<%= url_path("/job_stats?js=d3.v4.min.js") %>"></script>
2 changes: 1 addition & 1 deletion resque-job-stats.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Gem::Specification.new do |s|

files = `git ls-files`.split("\n") rescue []
files &= (
Dir['lib/**/*.{rb,erb}'] +
Dir['lib/**/*.{rb,erb,js}'] +
Dir['*.md'])

s.files = files
Expand Down

0 comments on commit f29d90b

Please sign in to comment.