dasht 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|