dasht 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.agignore +3 -0
  3. data/Gemfile +14 -0
  4. data/Gemfile.lock +88 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.md +329 -0
  7. data/Rakefile +51 -0
  8. data/VERSION +1 -0
  9. data/assets/css/style.css +123 -0
  10. data/assets/images/background.jpg +0 -0
  11. data/assets/js/Chart.min.js +11 -0
  12. data/assets/js/dasht.js +141 -0
  13. data/assets/js/jquery-1.11.3.min.js +5 -0
  14. data/assets/js/masonry.pkgd.min.js +9 -0
  15. data/assets/js/mustache.min.js +1 -0
  16. data/assets/js/underscore-min.js +6 -0
  17. data/assets/plugins/chart.css +11 -0
  18. data/assets/plugins/chart.html +2 -0
  19. data/assets/plugins/chart.js +50 -0
  20. data/assets/plugins/map.css +17 -0
  21. data/assets/plugins/map.html +4 -0
  22. data/assets/plugins/map.js +125 -0
  23. data/assets/plugins/scroll.css +2 -0
  24. data/assets/plugins/scroll.html +2 -0
  25. data/assets/plugins/scroll.js +14 -0
  26. data/assets/plugins/value.css +18 -0
  27. data/assets/plugins/value.html +4 -0
  28. data/assets/plugins/value.js +26 -0
  29. data/examples/simple_heroku_dashboard.rb +34 -0
  30. data/lib/dasht.rb +25 -0
  31. data/lib/dasht/array_monkeypatching.rb +5 -0
  32. data/lib/dasht/base.rb +117 -0
  33. data/lib/dasht/board.rb +108 -0
  34. data/lib/dasht/collector.rb +33 -0
  35. data/lib/dasht/list.rb +93 -0
  36. data/lib/dasht/log_thread.rb +94 -0
  37. data/lib/dasht/metric.rb +82 -0
  38. data/lib/dasht/rack_app.rb +74 -0
  39. data/lib/dasht/reloader.rb +28 -0
  40. data/screenshot_1.png +0 -0
  41. data/test/helper.rb +34 -0
  42. data/test/test_list.rb +65 -0
  43. data/test/test_metric.rb +58 -0
  44. data/tests.rb +6 -0
  45. data/views/dashboard.erb +27 -0
  46. metadata +189 -0
@@ -0,0 +1,4 @@
1
+ {{#title}}
2
+ <div class="title">{{title}}</div>
3
+ {{/title}}
4
+ <div class="map"></div>
@@ -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,2 @@
1
+ @media (max-width: 640px) {
2
+ }
@@ -0,0 +1,2 @@
1
+ <div class="title">{{title}}</div>
2
+ <div class="metric" data-metric="{{metric}}" data-resolution="{{resolution}}" data-refresh="{{refresh}}" />
@@ -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,4 @@
1
+ {{#title}}
2
+ <div class="title">{{title}}</div>
3
+ {{/title}}
4
+ <div class="value fontsize-large" />
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ class Array
2
+ def dasht_sum
3
+ self.compact.inject(:+) || 0;
4
+ end
5
+ end
@@ -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
@@ -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