dasht 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.agignore +3 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +88 -0
- data/LICENSE.txt +20 -0
- data/README.md +329 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/assets/css/style.css +123 -0
- data/assets/images/background.jpg +0 -0
- data/assets/js/Chart.min.js +11 -0
- data/assets/js/dasht.js +141 -0
- data/assets/js/jquery-1.11.3.min.js +5 -0
- data/assets/js/masonry.pkgd.min.js +9 -0
- data/assets/js/mustache.min.js +1 -0
- data/assets/js/underscore-min.js +6 -0
- data/assets/plugins/chart.css +11 -0
- data/assets/plugins/chart.html +2 -0
- data/assets/plugins/chart.js +50 -0
- data/assets/plugins/map.css +17 -0
- data/assets/plugins/map.html +4 -0
- data/assets/plugins/map.js +125 -0
- data/assets/plugins/scroll.css +2 -0
- data/assets/plugins/scroll.html +2 -0
- data/assets/plugins/scroll.js +14 -0
- data/assets/plugins/value.css +18 -0
- data/assets/plugins/value.html +4 -0
- data/assets/plugins/value.js +26 -0
- data/examples/simple_heroku_dashboard.rb +34 -0
- data/lib/dasht.rb +25 -0
- data/lib/dasht/array_monkeypatching.rb +5 -0
- data/lib/dasht/base.rb +117 -0
- data/lib/dasht/board.rb +108 -0
- data/lib/dasht/collector.rb +33 -0
- data/lib/dasht/list.rb +93 -0
- data/lib/dasht/log_thread.rb +94 -0
- data/lib/dasht/metric.rb +82 -0
- data/lib/dasht/rack_app.rb +74 -0
- data/lib/dasht/reloader.rb +28 -0
- data/screenshot_1.png +0 -0
- data/test/helper.rb +34 -0
- data/test/test_list.rb +65 -0
- data/test/test_metric.rb +58 -0
- data/tests.rb +6 -0
- data/views/dashboard.erb +27 -0
- metadata +189 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
Dasht.map_geocoder_cache = {}
|
2
|
+
|
3
|
+
Dasht.map_plot_address = function(map, markers, geocoder, address) {
|
4
|
+
// Maybe pull the location from cache.
|
5
|
+
var location;
|
6
|
+
if (location = Dasht.map_geocoder_cache[address]) {
|
7
|
+
Dasht.map_plot_location(map, markers, address, location);
|
8
|
+
return;
|
9
|
+
}
|
10
|
+
|
11
|
+
geocoder.geocode({ "address": address }, function(results, status) {
|
12
|
+
// Don't plot if the lookup failed.
|
13
|
+
if (status != google.maps.GeocoderStatus.OK) return;
|
14
|
+
var location = results[0].geometry.location;
|
15
|
+
Dasht.map_geocoder_cache[address] = location;
|
16
|
+
Dasht.map_plot_location(map, markers, address, location);
|
17
|
+
});
|
18
|
+
}
|
19
|
+
|
20
|
+
Dasht.map_plot_ip = function(map, markers, ip) {
|
21
|
+
// Maybe pull the location from cache.
|
22
|
+
var location;
|
23
|
+
if (location = Dasht.map_geocoder_cache[ip]) {
|
24
|
+
Dasht.map_plot_location(map, markers, ip, location);
|
25
|
+
return;
|
26
|
+
}
|
27
|
+
|
28
|
+
// http://freegeoip.net/json/
|
29
|
+
jQuery.ajax({
|
30
|
+
url: 'http://104.236.251.84/json/' + ip,
|
31
|
+
type: 'POST',
|
32
|
+
dataType: 'jsonp',
|
33
|
+
success: function(response) {
|
34
|
+
var location = new google.maps.LatLng(response.latitude, response.longitude);
|
35
|
+
Dasht.map_geocoder_cache[ip] = location;
|
36
|
+
Dasht.map_plot_location(map, markers, ip, location);
|
37
|
+
},
|
38
|
+
error: function (xhr, ajaxOptions, thrownError) {
|
39
|
+
alert("Failed!");
|
40
|
+
}
|
41
|
+
});
|
42
|
+
}
|
43
|
+
|
44
|
+
Dasht.map_plot_location = function(map, markers, address_or_ip, location) {
|
45
|
+
var location_exists = _.any(_.values(markers), function(marker) {
|
46
|
+
return _.isEqual(marker.position, location);
|
47
|
+
});
|
48
|
+
if (location_exists) return;
|
49
|
+
|
50
|
+
// Drop a pin.
|
51
|
+
var marker = new google.maps.Marker({
|
52
|
+
map: map,
|
53
|
+
animation: google.maps.Animation.DROP,
|
54
|
+
position: location
|
55
|
+
});
|
56
|
+
|
57
|
+
// Keep track of markers.
|
58
|
+
markers[address_or_ip] = marker;
|
59
|
+
}
|
60
|
+
|
61
|
+
Dasht.map_init = function(el, options) {
|
62
|
+
// Initialize.
|
63
|
+
var old_data = undefined;
|
64
|
+
var styles = [
|
65
|
+
{
|
66
|
+
stylers: [
|
67
|
+
{ hue: "#ffffff" },
|
68
|
+
{ saturation: -100 },
|
69
|
+
{ lightness: 20 },
|
70
|
+
{ gamma: 0.5 }
|
71
|
+
]
|
72
|
+
},
|
73
|
+
{
|
74
|
+
featureType: "water",
|
75
|
+
stylers: [
|
76
|
+
{ hue: "#ffffff" },
|
77
|
+
{ saturation: 80 },
|
78
|
+
{ lightness: 100 },
|
79
|
+
{ gamma: 0.43 }
|
80
|
+
]
|
81
|
+
}
|
82
|
+
];
|
83
|
+
|
84
|
+
var mapOptions = {
|
85
|
+
zoom: 4,
|
86
|
+
center: new google.maps.LatLng(39.8282, -98.5795),
|
87
|
+
styles: styles,
|
88
|
+
disableDefaultUI: true
|
89
|
+
};
|
90
|
+
|
91
|
+
var map_el = $(el).find(".map").get()[0];
|
92
|
+
var num_entries = options["n"] || 10;
|
93
|
+
var map = new google.maps.Map(map_el, mapOptions);
|
94
|
+
var markers = {};
|
95
|
+
var geocoder = new google.maps.Geocoder();
|
96
|
+
var ip_regex = /\d+\.\d+\.\d+\.\d+/;
|
97
|
+
|
98
|
+
Dasht.fill_tile($(el).find(".map"));
|
99
|
+
|
100
|
+
// Update values.
|
101
|
+
setTimeout(function() {
|
102
|
+
Dasht.get_value(options, function(new_data) {
|
103
|
+
new_data = new_data[0];
|
104
|
+
if (_.isEqual(old_data, new_data)) return;
|
105
|
+
|
106
|
+
// Remove old markers.
|
107
|
+
var old_markers = _.difference(_.keys(markers), new_data);
|
108
|
+
_.each(old_markers, function(address) {
|
109
|
+
markers[address].setMap(null);
|
110
|
+
delete markers[address];
|
111
|
+
});
|
112
|
+
|
113
|
+
// Plot each marker.
|
114
|
+
_.each(new_data, function(item, index) {
|
115
|
+
if (item.search(ip_regex) >= 0) {
|
116
|
+
Dasht.map_plot_ip(map, markers, item);
|
117
|
+
} else {
|
118
|
+
Dasht.map_plot_address(map, markers, geocoder, item);
|
119
|
+
}
|
120
|
+
});
|
121
|
+
|
122
|
+
old_data = new_data;
|
123
|
+
});
|
124
|
+
}, 1000);
|
125
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Dasht.scroll_init = function(el, options) {
|
2
|
+
var old_value = undefined;
|
3
|
+
var num_entries = options["n"] || 10;
|
4
|
+
var metric = $(el).find(".metric");
|
5
|
+
$(el).on('update', function(event, value) {
|
6
|
+
value = value.slice(-1 * num_entries);
|
7
|
+
if (_.isEqual(old_value, value)) return;
|
8
|
+
metric.animate({ opacity: 0.8 }, 0, function() {
|
9
|
+
metric.html(value.join("<br />"));
|
10
|
+
metric.animate({ opacity: 1.0 }, 400);
|
11
|
+
});
|
12
|
+
old_value = value;
|
13
|
+
});
|
14
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
.value-tile .value {
|
2
|
+
font-family: 'Lato', sans-serif;
|
3
|
+
color: rgba(255, 255, 255, 0.9);
|
4
|
+
margin: auto;
|
5
|
+
font-size: 80px;
|
6
|
+
font-weight: bold;
|
7
|
+
margin: 20px 20px 0px 20px;
|
8
|
+
display: flex;
|
9
|
+
align-items: center;
|
10
|
+
}
|
11
|
+
|
12
|
+
@media (max-width: 640px) {
|
13
|
+
.value-tile .value {
|
14
|
+
margin-top: 20px;
|
15
|
+
margin-bottom: 20px;
|
16
|
+
font-size: 30px;
|
17
|
+
}
|
18
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
Dasht.value_update = function(el, value) {
|
2
|
+
$(el).css("opacity", 0.7);
|
3
|
+
$(el).html(value.toLocaleString());
|
4
|
+
$(el).animate({ "opacity": 1.0 });
|
5
|
+
}
|
6
|
+
|
7
|
+
Dasht.value_init = function(el, options) {
|
8
|
+
// Initialize.
|
9
|
+
var value = $(el).find(".value");
|
10
|
+
var value_el = value.get()[0];
|
11
|
+
var old_data = undefined;
|
12
|
+
|
13
|
+
// Set the value height to be tile height minus title height.
|
14
|
+
Dasht.fill_tile($(el).find(".title"), true, false);
|
15
|
+
Dasht.fill_tile(value);
|
16
|
+
|
17
|
+
// Update values.
|
18
|
+
setTimeout(function() {
|
19
|
+
Dasht.get_value(options, function(new_data) {
|
20
|
+
new_data = new_data[0];
|
21
|
+
if (_.isEqual(old_data, new_data)) return;
|
22
|
+
Dasht.value_update(value, new_data);
|
23
|
+
old_data = new_data;
|
24
|
+
});
|
25
|
+
}, 1000);
|
26
|
+
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'dasht'
|
2
|
+
|
3
|
+
application = ARGV[0]
|
4
|
+
|
5
|
+
dasht do |d|
|
6
|
+
# Consume Heroku logs.
|
7
|
+
d.start "heroku logs --tail --app #{application}" do |l|
|
8
|
+
# Track some metrics.
|
9
|
+
l.count :lines, /.+/
|
10
|
+
|
11
|
+
l.count :bytes, /.+/ do |match|
|
12
|
+
match[0].length
|
13
|
+
end
|
14
|
+
|
15
|
+
l.append :visitors, /Started GET .* for (\d+\.\d+\.\d+\.\d+) at/ do |matches|
|
16
|
+
matches[1]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
counter = 0
|
21
|
+
d.interval :counter do
|
22
|
+
sleep 1
|
23
|
+
counter += 1
|
24
|
+
end
|
25
|
+
|
26
|
+
# Publish a board.
|
27
|
+
d.board do |b|
|
28
|
+
b.value :counter, :title => "Counter"
|
29
|
+
b.value :lines, :title => "Number of Lines"
|
30
|
+
b.value :bytes, :title => "Number of Bytes"
|
31
|
+
b.chart :bytes, :title => "Chart of Bytes", :periods => 10
|
32
|
+
b.map :visitors, :title => "Visitors", :width => 12, :height => 9
|
33
|
+
end
|
34
|
+
end
|
data/lib/dasht.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'thread'
|
3
|
+
require 'erb'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require 'dasht/array_monkeypatching'
|
7
|
+
require 'dasht/reloader'
|
8
|
+
require 'dasht/board'
|
9
|
+
require 'dasht/list'
|
10
|
+
require 'dasht/metric'
|
11
|
+
require 'dasht/collector'
|
12
|
+
require 'dasht/rack_app'
|
13
|
+
require 'dasht/log_thread'
|
14
|
+
require 'dasht/base'
|
15
|
+
|
16
|
+
class DashtSingleton
|
17
|
+
def self.run(&block)
|
18
|
+
@@instance ||= Dasht::Base.new
|
19
|
+
@@instance.run(&block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def dasht(&block)
|
24
|
+
DashtSingleton.run(&block)
|
25
|
+
end
|
data/lib/dasht/base.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
module Dasht
|
2
|
+
class Base
|
3
|
+
attr_accessor :metrics
|
4
|
+
attr_accessor :rack_app
|
5
|
+
attr_accessor :reloader
|
6
|
+
attr_accessor :boards
|
7
|
+
|
8
|
+
# Settings.
|
9
|
+
attr_accessor :port
|
10
|
+
attr_accessor :background
|
11
|
+
attr_accessor :default_resolution
|
12
|
+
attr_accessor :default_refresh
|
13
|
+
attr_accessor :default_width
|
14
|
+
attr_accessor :default_height
|
15
|
+
attr_accessor :history
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@boards = {}
|
19
|
+
@log_threads = {}
|
20
|
+
@metrics = Metrics.new(self)
|
21
|
+
@reloader = Reloader.new(self)
|
22
|
+
@rack_app = RackApp.new(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def log(s)
|
26
|
+
if s.class < Exception
|
27
|
+
print "\n#{s}\n"
|
28
|
+
print s.backtrace.join("\n")
|
29
|
+
else
|
30
|
+
print "\r#{s}\n"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
### DATA INGESTION ###
|
35
|
+
|
36
|
+
def start(command, &block)
|
37
|
+
log_thread = @log_threads[command] = LogThread.new(self, command)
|
38
|
+
yield(log_thread) if block
|
39
|
+
log_thread
|
40
|
+
end
|
41
|
+
|
42
|
+
def tail(path)
|
43
|
+
start("tail -F -n 0 \"#{path}\"")
|
44
|
+
end
|
45
|
+
|
46
|
+
def interval(metric, &block)
|
47
|
+
Thread.new do
|
48
|
+
begin
|
49
|
+
while true
|
50
|
+
value = block.call
|
51
|
+
set(metric, value, :last) if value
|
52
|
+
end
|
53
|
+
rescue => e
|
54
|
+
log e
|
55
|
+
raise e
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
### DASHBOARD ###
|
61
|
+
|
62
|
+
def views_path
|
63
|
+
File.join(File.dirname(__FILE__), "..", "..", "views")
|
64
|
+
end
|
65
|
+
|
66
|
+
def system_plugins_path
|
67
|
+
File.join(File.dirname(__FILE__), "..", "..", "assets", "plugins")
|
68
|
+
end
|
69
|
+
|
70
|
+
def user_plugins_path
|
71
|
+
File.join(File.dirname($PROGRAM_NAME), "plugins")
|
72
|
+
end
|
73
|
+
|
74
|
+
def board(name = "default", &block)
|
75
|
+
name = name.to_s
|
76
|
+
board = @boards[name] = Board.new(self, name)
|
77
|
+
yield(board) if block
|
78
|
+
board
|
79
|
+
end
|
80
|
+
|
81
|
+
### RUN & RELOAD ###
|
82
|
+
|
83
|
+
def run(&block)
|
84
|
+
if @already_running
|
85
|
+
begin
|
86
|
+
reload(&block)
|
87
|
+
rescue => e
|
88
|
+
log e
|
89
|
+
end
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
@already_running = true
|
94
|
+
@reloader.run
|
95
|
+
|
96
|
+
block.call(self)
|
97
|
+
|
98
|
+
@log_threads.values.map(&:run)
|
99
|
+
@rack_app.run(port)
|
100
|
+
end
|
101
|
+
|
102
|
+
def reload(&block)
|
103
|
+
@boards = {}
|
104
|
+
@log_threads.values.map(&:terminate)
|
105
|
+
@log_threads = {}
|
106
|
+
|
107
|
+
begin
|
108
|
+
block.call(self)
|
109
|
+
rescue => e
|
110
|
+
log e
|
111
|
+
raise e
|
112
|
+
end
|
113
|
+
|
114
|
+
@log_threads.values.map(&:run)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/dasht/board.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
module Dasht
|
2
|
+
class Board
|
3
|
+
attr_accessor :parent
|
4
|
+
attr_accessor :name
|
5
|
+
attr_accessor :tiles
|
6
|
+
attr_accessor :background
|
7
|
+
attr_accessor :default_resolution
|
8
|
+
attr_accessor :default_refresh
|
9
|
+
attr_accessor :default_width
|
10
|
+
attr_accessor :default_height
|
11
|
+
|
12
|
+
def initialize(parent, name)
|
13
|
+
@parent = parent
|
14
|
+
@name = name
|
15
|
+
@tiles = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_html
|
19
|
+
# Load the erb.
|
20
|
+
path = File.join(parent.views_path, "dashboard.erb")
|
21
|
+
@erb = ERB.new(IO.read(path))
|
22
|
+
@erb.result(binding)
|
23
|
+
end
|
24
|
+
|
25
|
+
def emit_plugin_css
|
26
|
+
_emit_css(parent.system_plugins_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def emit_plugin_html
|
30
|
+
_emit_html(parent.system_plugins_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def emit_plugin_js
|
34
|
+
_emit_js(parent.system_plugins_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(method, *args, &block)
|
38
|
+
begin
|
39
|
+
metric = args.shift
|
40
|
+
options = args.pop || {}
|
41
|
+
@tiles << {
|
42
|
+
:type => method,
|
43
|
+
:metric => metric,
|
44
|
+
:resolution => self.default_resolution || parent.default_resolution || 60,
|
45
|
+
:refresh => self.default_refresh || parent.default_refresh || 5,
|
46
|
+
:width => self.default_width || parent.default_width || 3,
|
47
|
+
:height => self.default_height || parent.default_height || 3,
|
48
|
+
:extra_args => args
|
49
|
+
}.merge(options)
|
50
|
+
rescue => e
|
51
|
+
super(method, *args, &block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def emit_tile_js
|
58
|
+
s = "<script>\n"
|
59
|
+
s += "$(function() {\n";
|
60
|
+
@tiles.map do |options|
|
61
|
+
s += "Dasht.add_tile(#{options.to_json});\n"
|
62
|
+
end
|
63
|
+
s += "});"
|
64
|
+
s += "</script>\n"
|
65
|
+
s
|
66
|
+
end
|
67
|
+
|
68
|
+
def emit_board_js
|
69
|
+
s = "<script>"
|
70
|
+
if background = self.background || parent.background
|
71
|
+
s += "$('body').css('background', #{background.to_json});\n"
|
72
|
+
end
|
73
|
+
s += "</script>\n"
|
74
|
+
s
|
75
|
+
end
|
76
|
+
|
77
|
+
def _emit_css(plugin_path)
|
78
|
+
s = ""
|
79
|
+
Dir[File.join(plugin_path, "*.css")].each do |path|
|
80
|
+
name = File.basename(path)
|
81
|
+
s += "<link rel='stylesheet' type='text/css' href='/assets/plugins/#{name}'>\n"
|
82
|
+
end
|
83
|
+
return s
|
84
|
+
end
|
85
|
+
|
86
|
+
def _emit_html(plugin_path)
|
87
|
+
s = ""
|
88
|
+
Dir[File.join(plugin_path, "*.html")].each do |path|
|
89
|
+
name = File.basename(path).gsub(".html", "")
|
90
|
+
s += "<script id='#{name}-template' type='x-tmpl-mustache'>\n"
|
91
|
+
s += "<div class='tile #{name}-tile width-{{width}} height-{{height}}'>\n"
|
92
|
+
s += IO.read(path)
|
93
|
+
s += "</div>\n"
|
94
|
+
s += "</script>\n"
|
95
|
+
end
|
96
|
+
return s
|
97
|
+
end
|
98
|
+
|
99
|
+
def _emit_js(plugin_path)
|
100
|
+
s = ""
|
101
|
+
Dir[File.join(plugin_path, "*.js")].each do |path|
|
102
|
+
name = File.basename(path)
|
103
|
+
s += "<script src='/assets/plugins/#{name}'></script>\n"
|
104
|
+
end
|
105
|
+
s
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|