fnordmetric 0.3.2 → 0.5.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.
Files changed (59) hide show
  1. data/Gemfile +6 -0
  2. data/Gemfile.lock +21 -0
  3. data/Procfile +1 -2
  4. data/VERSION +1 -1
  5. data/_spec/app_spec.rb +178 -0
  6. data/{spec → _spec}/cache_spec.rb +0 -0
  7. data/{spec → _spec}/combine_metric_spec.rb +0 -0
  8. data/{spec → _spec}/core_spec.rb +0 -0
  9. data/{spec → _spec}/count_metric_spec.rb +0 -0
  10. data/_spec/dashboard_spec.rb +67 -0
  11. data/_spec/event_spec.rb +46 -0
  12. data/{spec → _spec}/metric_spec.rb +0 -0
  13. data/{spec → _spec}/report_spec.rb +0 -0
  14. data/{spec → _spec}/sum_metric_spec.rb +0 -0
  15. data/_spec/widget_spec.rb +107 -0
  16. data/doc/import_dump.rb +26 -0
  17. data/em_runner.rb +33 -0
  18. data/fnordmetric.gemspec +59 -20
  19. data/haml/app.haml +26 -12
  20. data/lib/fnordmetric.rb +150 -15
  21. data/lib/fnordmetric/app.rb +70 -11
  22. data/lib/fnordmetric/cache.rb +4 -4
  23. data/lib/fnordmetric/context.rb +65 -0
  24. data/lib/fnordmetric/dashboard.rb +16 -12
  25. data/lib/fnordmetric/event.rb +65 -15
  26. data/lib/fnordmetric/gauge.rb +46 -0
  27. data/lib/fnordmetric/gauge_calculations.rb +43 -0
  28. data/lib/fnordmetric/gauge_modifiers.rb +43 -0
  29. data/lib/fnordmetric/inbound_stream.rb +66 -0
  30. data/lib/fnordmetric/logger.rb +38 -0
  31. data/lib/fnordmetric/namespace.rb +120 -0
  32. data/lib/fnordmetric/numbers_widget.rb +29 -11
  33. data/lib/fnordmetric/session.rb +131 -0
  34. data/lib/fnordmetric/standalone.rb +31 -0
  35. data/lib/fnordmetric/timeline_widget.rb +29 -9
  36. data/lib/fnordmetric/widget.rb +50 -45
  37. data/lib/fnordmetric/worker.rb +80 -0
  38. data/pub/fnordmetric/fnordmetric.css +76 -9
  39. data/pub/fnordmetric/fnordmetric.js +541 -42
  40. data/pub/raphael-min.js +8 -0
  41. data/pub/raphael-utils.js +221 -0
  42. data/readme.rdoc +172 -27
  43. data/server.rb +22 -0
  44. data/spec/app_spec.rb +359 -117
  45. data/spec/context_spec.rb +42 -0
  46. data/spec/dashboard_spec.rb +7 -47
  47. data/spec/event_spec.rb +114 -33
  48. data/spec/gauge_modifiers_spec.rb +276 -0
  49. data/spec/gauge_spec.rb +128 -0
  50. data/spec/namespace_spec.rb +104 -0
  51. data/spec/session_spec.rb +231 -0
  52. data/spec/spec_helper.rb +27 -4
  53. data/spec/widget_spec.rb +81 -75
  54. data/spec/worker_spec.rb +37 -0
  55. data/test_stream.sh +187 -0
  56. data/ulm_stats.rb +198 -0
  57. metadata +114 -35
  58. data/lib/fnordmetric/core.rb +0 -66
  59. data/lib/fnordmetric/engine.rb +0 -3
@@ -1,75 +1,80 @@
1
1
  class FnordMetric::Widget
2
2
 
3
- attr_accessor :report
3
+ attr_accessor :gauges, :tick
4
4
 
5
- def initialize(options={})
6
- @options = options
7
- add_report(@options[:report]) if @options[:report]
5
+ def initialize(opts={})
6
+ @opts = opts
7
+
8
+ unless opts.has_key?(:title)
9
+ error! "widget can't be initialized without a title"
10
+ end
11
+
12
+ add_gauges(opts.delete(:gauges))
8
13
  end
9
14
 
10
15
  def title
11
- @options[:title]
16
+ @opts[:title]
12
17
  end
13
18
 
14
- def metrics
15
- [@options[:metrics]].flatten.map{ |m|
16
- m.is_a?(FnordMetric::Metric) ? m : FnordMetric.metrics.fetch(m)
17
- }
19
+ def token
20
+ title.to_s.gsub(/[\W]/, '').downcase
18
21
  end
19
22
 
20
- def tick
21
- @options[:tick] || 1.day
23
+ def add_gauges(gauges)
24
+ if gauges.blank? && has_tick?
25
+ error! "initializing a widget without gauges is void"
26
+ else
27
+ @gauges = gauges
28
+ end
29
+
30
+ if (ticks = gauges.map{ |g| g.tick }).uniq.length == 1
31
+ @tick = ticks.first
32
+ else
33
+ error! "you can't add gauges with different ticks to the same widget"
34
+ end
22
35
  end
23
36
 
24
- def range
25
- @options[:range] || default_range
37
+ def error!(msg)
38
+ FnordMetric.error!(msg)
26
39
  end
27
40
 
28
- def range_to_i
29
- (range.first.to_i..range.last.to_i)
41
+ def range
42
+ ensure_has_tick!
43
+ #@opts[:range] || default_range # FIXME: allow custom ranges, but assure that the range-start is 'on a tick'
44
+ default_range
30
45
  end
31
46
 
32
47
  def ticks
33
- range_to_i.step(tick.to_i).map{ |ts|
34
- (Time.at(ts)..Time.at(ts+tick.to_i))
35
- }.compact
48
+ ensure_has_tick!
49
+ range.step(@tick)
36
50
  end
37
51
 
38
52
  def default_range(now=Time.now)
39
- if tick.to_i == 1.day.to_i
40
- now = now+1.day
41
- te = Time.utc(now.year, now.month, now.day)
42
- (te-30.days)..(te-1.second)
43
- elsif tick.to_i == 1.hour.to_i
44
- te = Time.utc(now.year, now.month, now.day, now.hour)
45
- (te-24.hours)..(te-1.second)
46
- else
47
- (now-(tick*30))..now
48
- end
49
- end
50
-
51
- def add_report(report)
52
- @report = report
53
+ ensure_has_tick!
54
+ te = gauges.first.tick_at(now.to_i)
55
+ te += @tick if include_current?
56
+ rs = @tick == 1.hour.to_i ? 24 : 30
57
+ (te-(@tick*rs)..te)
53
58
  end
54
59
 
55
- def data
56
- { :title => @options[:title] }
60
+ def include_current?
61
+ !(@opts[:include_current] == false)
57
62
  end
58
-
59
- def data_json
60
- data.to_json.gsub('"', '\'')
63
+
64
+ def data
65
+ {
66
+ :title => @opts[:title],
67
+ :width => @opts[:width] || 100,
68
+ :klass => self.class.name.split("::").last
69
+ }
61
70
  end
62
71
 
63
- def include_current?
64
- !(@options[:current] == false)
72
+ def render
73
+ data
65
74
  end
66
75
 
67
- def render(elem_id)
68
- %Q{
69
- <script type='text/javascript'>
70
- FnordMetric.render('#{elem_id}', #{data_json});
71
- </script>
72
- }
76
+ def ensure_has_tick!
77
+ error! "widget does not have_tick" unless has_tick?
73
78
  end
74
79
 
75
80
  end
@@ -0,0 +1,80 @@
1
+ class FnordMetric::Worker
2
+
3
+ def initialize(namespaces, opts)
4
+ @namespaces = {}
5
+ @opts = opts
6
+ configure(namespaces)
7
+ end
8
+
9
+ def ready!
10
+ @redis = EM::Hiredis.connect(@opts[:redis_uri])
11
+ tick
12
+ end
13
+
14
+ def configure(namespaces)
15
+ namespaces.each do |key, block|
16
+ @namespaces[key] = FnordMetric::Namespace.new(key, @opts.clone)
17
+ @namespaces[key].instance_eval(&block)
18
+ end
19
+ end
20
+
21
+ def tick
22
+ @redis.blpop(queue_key, 0).callback do |list, event_id|
23
+ @redis.get(event_key(event_id)).callback do |event_data|
24
+ process_event(event_id, event_data) if event_data
25
+ FnordMetric.log("oops, lost an event :(") unless event_data
26
+ EM.next_tick(&method(:tick))
27
+ @redis.hincrby(stats_key, :events_processed, 1)
28
+ end
29
+ end
30
+ end
31
+
32
+ def process_event(event_id, event_data)
33
+ EM.defer do
34
+ parse_json(event_data).tap do |event|
35
+ event[:_time] ||= Time.now.to_i
36
+ event[:_eid] = event_id
37
+ announce_event(event)
38
+ publish_event(event)
39
+ expire_event(event_id)
40
+ end
41
+ end
42
+ end
43
+
44
+ def pubsub_key
45
+ [@opts[:redis_prefix], 'announce'].join("-")
46
+ end
47
+
48
+ def queue_key
49
+ [@opts[:redis_prefix], 'queue'].join("-")
50
+ end
51
+
52
+ def event_key(event_id)
53
+ [@opts[:redis_prefix], 'event', event_id].join("-")
54
+ end
55
+
56
+ def stats_key
57
+ [@opts[:redis_prefix], 'stats'].join("-")
58
+ end
59
+
60
+ def announce_event(event)
61
+ namespace(event[:_namespace]).ready!(@redis).announce(event)
62
+ end
63
+
64
+ def expire_event(event_id)
65
+ @redis.expire(event_key(event_id), @opts[:event_data_ttl])
66
+ end
67
+
68
+ def publish_event(event)
69
+ @redis.publish(pubsub_key, event[:_eid])
70
+ end
71
+
72
+ def namespace(key)
73
+ (@namespaces[key] || @namespaces.first.last).clone
74
+ end
75
+
76
+ def parse_json(data)
77
+ Yajl::Parser.new(:symbolize_keys => true).parse(data)
78
+ end
79
+
80
+ end
@@ -1,21 +1,32 @@
1
1
  body, html{ height:100%; padding:0px;}
2
- body{ background:#fff; color:#333; margin:0; padding:0; overflow-y:scroll; font: 12px/20px "Helvetica Neue", Helvetica, Arial, sans-serif; }
2
+ body{ background:#3b3e45; color:#333; margin:0; padding:0; overflow-y:scroll; font: 12px/20px "Helvetica Neue", Helvetica, Arial, sans-serif; }
3
3
 
4
- #wrap{ margin:0 40px; }
4
+ .topbar{ height:43px; background:#24272c; display:none; }
5
5
 
6
- #tabs{ width:200px; position:fixed; height:100%; margin-top:70px; }
7
- #tabs ul{ list-style-type:none; padding:0; margin:0; }
8
- #tabs ul li{ height:34px; line-height:35px; border-bottom:1px dotted #ececec; cursor:pointer; color:#666; font-size:13px; }
6
+ #wrap{ margin:0 20px; }
7
+
8
+ #tabs{ width:150px; position:fixed; height:100%; margin-top:70px; }
9
+ #tabs ul{ list-style-type:none; padding:0; margin:0; width:156px;}
10
+ #tabs ul li{ height:34px; line-height:35px; cursor:pointer; color:#ccc; font-size:13px; border-radius:3px; margin-bottom:5px; }
9
11
  #tabs ul li:after{ content:'›'; display:block; float:right; margin-right:15px; color:#ccc; font-size:16px; line-height:35px; }
10
12
  #tabs ul li .picto{ margin-top:10px; margin-right:7px; }
11
- #tabs ul li:hover, #tabs ul li:hover:after{ color:#000; }
13
+ #tabs ul li:hover, #tabs ul li:hover:after{ color:#fff; }
12
14
  #tabs ul li:hover .picto{ opacity:1; }
13
15
 
14
16
  .picto{ display:block; height:14px; width:14px; float:left; background:url('/fnordmetric/sprite.png') no-repeat 14px 14px; opacity:0.7; }
15
17
  .picto.piechart{ background-position:-42px -173px; width:9px; margin-right:5px; }
16
18
 
17
- #viewport{ min-height:800px; float:left; margin-left:220px; padding-top:20px; border-right:1px solid #e0e0e0; border-left:1px solid #e0e0e0; }
19
+ #viewport{ float:left; margin-left:150px; margin-top:30px; border-radius:3px; min-width:790px; }
20
+ #viewport .viewport_inner{ margin:6px; background:#fff; min-height:1200px; border-radius:2px; }
21
+
22
+ #viewport, #tabs ul li:hover, #tabs ul li.active{ background:#24272c; box-shadow: inset 0px 1px 2px 1px rgba(0, 0, 0, 0.4); }
18
23
 
24
+ .widget{ min-height:100px; border-right:1px solid #ececec; float:left; }
25
+ .widget.full_width{ border-right:none; }
26
+ .widget .inner{ margin:20px; }
27
+ .widget .headbar{ margin-bottom:30px; }
28
+
29
+ /*
19
30
  .headbar{ height:36px; background:#f2f2f2; border-bottom:1px solid #e2e2e2; }
20
31
  .headbar h2{ margin:8px; line-height:21px; float:left; height:20px; font-size:14px; }
21
32
  .headbar .datepicker{ background:#fff; border:1px solid #999; height:20px; padding:0 7px; float:right; margin:8px -1px; min-width:100px; font-size:11px; font-style:italic; }
@@ -42,8 +53,8 @@ body{ background:#fff; color:#333; margin:0; padding:0; overflow-y:scroll; font:
42
53
  }
43
54
 
44
55
  .headbar .button:hover, .headbar.button.active{background:#ddd;border-bottom-color:#999;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, .05)}
45
-
46
- .numbers_container, .number{ float:left; border-right:1px solid #ececec;}
56
+ */
57
+ .numbers_container, .number{ float:left; border-right:1px solid #ececec; }
47
58
  .number{ margin-left:12px; padding-right:20px; margin-right:10px; }
48
59
  .number .value{ color:#333; font-size:30px; display:block; margin-bottom:5px; }
49
60
  .number .desc{ color:#999; font-size:12px; }
@@ -51,3 +62,59 @@ body{ background:#fff; color:#333; margin:0; padding:0; overflow-y:scroll; font:
51
62
  .numbers_container{ padding-right:0px; width:33.2%; }
52
63
 
53
64
  .numbers_container .title{ padding:4px 10px 1px 10px; color:#333; font-size:13px; display:block; background:#f2f2f2; border-bottom:1px solid #e2e2e2; margin-bottom:15px; }
65
+
66
+
67
+ .headbar {
68
+ background-color: #F4F4F4;
69
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#f4f4f4), to(#e9e9e9));
70
+ background-image: -webkit-linear-gradient(top, #f4f4f4, #e9e9e9);
71
+ background-image: -moz-linear-gradient(top, #f4f4f4, #e9e9e9);
72
+ background-image: -ms-linear-gradient(top, #f4f4f4, #e9e9e9);
73
+ background-image: -o-linear-gradient(top, #f4f4f4, #e9e9e9);
74
+ background-image: linear-gradient(top, #f4f4f4, #e9e9e9);
75
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#f4f4f4', EndColorStr='#e9e9e9');
76
+ padding: 0 15px;
77
+ border-bottom: 1px solid #C9C9C9;
78
+ border-top: 1px solid #F9F9F9;
79
+ height: 28px;
80
+ font-size:13px;
81
+ line-height:29px;
82
+ text-shadow: 1px 0px 2px rgba(255, 255, 255, 1);
83
+ -moz-text-shadow: 1px 0px 2px rgba(255,255,255,1);
84
+ -webkit-text-shadow: 1px 0px 2px rgba(255,255,255,1);
85
+ }
86
+
87
+
88
+ ul.session_list{ list-style-type:none; margin:0; padding:9px 16px 0 11px; }
89
+ ul.session_list li{ color:#0A0A0A; margin-bottom:10px; height:18px; overflow:hidden; line-height:18px; padding:4px; }
90
+ ul.session_list li:hover{ background:#eee; cursor:pointer; }
91
+ ul.session_list li .picture{ height:18px; width:18px; float:left; background:#333; overflow:hidden; }
92
+ ul.session_list li .name{ float:left; width:120px; overflow:hidden; margin-left:10px; font-size:12px; }
93
+ ul.session_list li .time{ float:right; width:40px; overflow:hidden; text-align:right; font-size:10px; }
94
+
95
+ .sessions_feed{ min-width:300px; min-height:100px; float:left; }
96
+ .sessions_feed ul.feed_inner{ margin:5px 15px; min-height:100px; padding:0px; }
97
+ .sessions_feed ul.feed_inner li{ list-style-type:none; border-bottom:1px solid #e2e2e2; min-height:54px; }
98
+ .sessions_feed ul.feed_inner li .message{ font-size:12px; line-height:19px; padding-top:9px; display:block; }
99
+ .sessions_feed ul.feed_inner li .properties{ margin-left:50px; font-size:10px; display:block; color:#555; }
100
+ .sessions_feed ul.feed_inner li .time{ font-size:10px; line-height:20px; padding-top:19px; padding-right:10px; display:block; color:#999; float:right; font-style:italic; }
101
+ .sessions_feed ul.feed_inner li .picture{ height:40px; overflow:hidden; width:40px; float:left; background:#333; margin:7px 10px 0 0; }
102
+ .sessions_sidebar{ min-height:1200px; float:right; width:250px; border-left:1px solid #C7C9CC; }
103
+ .events_sidebar{ min-height:1200px; float:left; width:200px; border-right:1px solid #C7C9CC; }
104
+
105
+ ul.event_type_list{ margin:10px; padding:0; }
106
+ ul.event_type_list li{ list-style-type:none; color:#0A0A0A; margin-bottom:8px; height:18px; overflow:hidden; line-height:18px; padding:4px; }
107
+ ul.event_type_list li input{ margin-right:7px; }
108
+ ul.event_type_list li:hover{ background:#eee; cursor:pointer; }
109
+ ul.event_type_list li .history{ float:right; color:#999; font-size:10px; }
110
+ ul.event_type_list li .history:hover{ color:#333; text-decoration:underline; }
111
+
112
+ .clearfix:after {
113
+ content: ".";
114
+ display: block;
115
+ clear: both;
116
+ visibility: hidden;
117
+ line-height: 0;
118
+ height: 0;
119
+ }
120
+
@@ -1,44 +1,543 @@
1
- FnordMetric = {
2
- d: document,
3
- p: '/fnordmetric/',
4
- id: function(id){return FnordMetric.d.getElementById(id)},
5
- tag: function(element){return FnordMetric.d.getElementsByTagName(element)},
6
- ce: function(element){return FnordMetric.d.createElement(element)},
7
-
8
- init: function(){
9
- if(widget_config.path_prefix){ FnordMetric.p = widget_config.path_prefix; }
10
- },
11
-
12
- js: function(url, callback){
13
- var s = FnordMetric.ce('script');
14
- s.type = "text/javascript";
15
- s.onload = callback;
16
- FnordMetric.init();
17
- s.src = FnordMetric.p+url;
18
- FnordMetric.tag('head')[0].appendChild(s);
19
- },
20
-
21
- css: function(url, callback){
22
- var s = FnordMetric.ce('link');
23
- s.type = "text/css";
24
- s.rel = 'stylesheet';
25
- s.href = FnordMetric.p+url;
26
- s.onload = callback;
27
- FnordMetric.tag('head')[0].appendChild(s);
28
- },
29
-
30
- render: function(elem, widget_config){
31
- var f = FnordMetric.ce('iframe');
32
- f.style.width = '100%'; f.style.height = widget_config.widget_height+'px';
33
- f.frameBorder = 'none'; f.scrolling = 'no';
34
- FnordMetric.id(elem).appendChild(f);
35
- var s = f.contentDocument.createElement('script')
36
- s.type = "text/javascript";
37
- s.src = FnordMetric.p+'/fnordmetric/fnordmetric.js';
38
- widget_config.path_prefix = FnordMetric.p;
39
- s.onload = function(){ f.contentWindow.FnordMetric.js(widget_config.widget_url); }
40
- f.contentWindow.widget_config = widget_config;
41
- f.contentDocument.getElementsByTagName('head')[0].appendChild(s);
1
+ var FnordMetric = (function(){
2
+
3
+ var canvasElem = false;
4
+
5
+ var currentNamespace = false;
6
+ var currentView = false;
7
+
8
+ function decPrint(val){
9
+ return (val < 10 ? '0'+val : val);
10
+ }
11
+
12
+ function formatTimeOfDay(_time){
13
+ var time = new Date();
14
+ time.setTime(_time*1000);
15
+ return decPrint(time.getHours()) + ':' +
16
+ decPrint(time.getMinutes()) + ':' +
17
+ decPrint(time.getSeconds());
18
+ }
19
+
20
+ function formatTimeSince(time){
21
+ var now = new Date().getTime()/1000;
22
+ var since = now - time;
23
+ if(since < 60){
24
+ return parseInt(since) + 's';
25
+ } else if(since<3600){
26
+ return parseInt(since/60) + 'm';
27
+ } else if(since<(3600*24)){
28
+ return parseInt(since/3600) + 'h';
29
+ } else {
30
+ return ">1d"
31
+ }
32
+ }
33
+
34
+ var numbersWidget = function(opts){
35
+
36
+ function render(){
37
+ //console.log(opts);
38
+ }
39
+
40
+ return {
41
+ render: render
42
+ };
43
+
44
+ };
45
+
46
+ var timelineWidget = function(opts){
47
+
48
+ function render(){
49
+
50
+ var labels = opts.labels;
51
+ var series = opts.series;
52
+
53
+ var elem_id = "fm_graph_"+parseInt(Math.random()*99999);
54
+ var elem_inner = $('.inner', opts.elem);
55
+ elem_inner.append($('<div id="'+elem_id+'"></div>'));
56
+
57
+ var width = elem_inner.width();
58
+ var height = 240;
59
+ var canvas = Raphael(elem_id, width, height+30);
60
+ var xtick = width / (labels.length-1);
61
+
62
+ var label_mod = Math.ceil((labels.length/10));
63
+
64
+ if(opts.independent_y_axis){
65
+ var max = false;
66
+ } else {
67
+ var amax = [];
68
+ $(series).each(function(n,_series){
69
+ amax.push(Math.max.apply(Math, _series.data));
70
+ });
71
+ var max = Math.max.apply(Math, amax);
72
+ }
73
+
74
+ $(series).each(function(n,_series){
75
+
76
+ //var path_string = "M0,"+height;
77
+ var path_string = "";
78
+ var _max = max;
79
+
80
+ if(!_max){ _max = Math.max.apply(Math, _series.data); }
81
+
82
+ _max = _max * 1.1;
83
+
84
+ $(_series.data).each(function(i,v){
85
+
86
+ var p_x = (i*xtick);
87
+ var p_y = (height-((v/_max)*height));
88
+
89
+ path_string += ( ( i == 0 ? "M" : "L" ) + p_x + ',' + p_y );
90
+
91
+ if(i%label_mod==0){
92
+ canvas.text(p_x, height+10, labels[i]).attr({
93
+ font: '10px Helvetica, Arial',
94
+ fill: "#777"
95
+ });
96
+ }
97
+
98
+ canvas.circle(p_x, p_y, 4).attr({
99
+ fill: _series.color,
100
+ stroke: '#fff',
101
+ "stroke-width": 1,
102
+ }).toBack();
103
+
104
+
105
+ var htrgt = canvas.rect(p_x - 20, p_y - 20, 40, 40).attr({
106
+ stroke: "none",
107
+ fill: "#fff",
108
+ opacity: 0
109
+ }).toFront();
110
+
111
+ (function(htrgt){
112
+
113
+ var t_y = p_y + 9;
114
+ var ttt = canvas.text(p_x, t_y+10, v).attr({
115
+ font: '12px Helvetica, Arial',
116
+ fill: "#fff",
117
+ opacity: 0
118
+ });
119
+
120
+ var tttb = ttt.getBBox();
121
+ var ttw = tttb.width+20;
122
+ var tt = canvas.rect(p_x-(ttw/2), t_y, ttw, 22, 5).attr({
123
+ stroke: "none",
124
+ fill: "#000",
125
+ opacity: 0
126
+ }).toBack();
127
+
128
+
129
+ $(htrgt[0]).hover(function(){
130
+ tt.animate({ opacity: 0.8 }, 300);
131
+ ttt.animate({ opacity: 0.8 }, 300);
132
+ }, function(){
133
+ tt.animate({ opacity: 0 }, 300);
134
+ ttt.animate({ opacity: 0 }, 300);
135
+ });
136
+
137
+ })(htrgt);
138
+
139
+ });
140
+
141
+ if(_max>0){
142
+
143
+ canvas.path(path_string).attr({
144
+ stroke: _series.color,
145
+ "stroke-width": 3,
146
+ "stroke-linejoin": 'round'
147
+ }).toBack();
148
+
149
+ path_string += "L"+width+","+height+" L0,"+height+" Z";
150
+
151
+ canvas.path(path_string).attr({
152
+ stroke: "none",
153
+ fill: _series.color,
154
+ opacity: 0.1
155
+ }).toBack();
156
+
157
+ }
158
+
159
+
160
+ });
161
+
162
+ canvas.drawGrid(0, 0, width, height, 1, 6, "#ececec");
163
+
164
+ }
165
+
166
+ return {
167
+ render: render
168
+ };
169
+
170
+ };
171
+
172
+ var sessionView = (function(){
173
+
174
+ var listElem = $('<ul class="session_list"></ul>');
175
+ var feedInnerElem = $('<ul class="feed_inner"></ul>');
176
+ var typeListElem = $('<ul class="event_type_list"></ul>');
177
+ var filterElem = $('<div class="events_sidebar"></div>').html(
178
+ $('<div class="headbar"></div>').html('Event Types')
179
+ ).append(typeListElem);
180
+ var feedElem = $('<div class="sessions_feed"></div>').html(
181
+ $('<div class="headbar"></div>').html('Event Feed')
182
+ ).append(feedInnerElem);
183
+ var sideElem = $('<div class="sessions_sidebar"></div>').html(
184
+ $('<div class="headbar"></div>').html('Active Users')
185
+ ).append(listElem);
186
+
187
+ var eventsPolledUntil = false;
188
+ var eventsFilter = [];
189
+ var sessionData = {};
190
+ var pollRunning = true;
191
+
192
+ function load(elem){
193
+ eventsPolledUntil = parseInt(new Date().getTime()/10000);
194
+ elem.html('')
195
+ .append(filterElem)
196
+ .append(feedElem)
197
+ .append(sideElem);
198
+ startPoll();
199
+ loadEventTypes();
200
+ };
201
+
202
+ function resize(_width, _height){
203
+ $('.sessions_feed').width(_width-452);
204
+ };
205
+
206
+ function startPoll(){
207
+ (doSessionPoll())();
208
+ (doEventsPoll())();
209
+ sessionView.session_poll = window.setInterval(doSessionPoll(), 1000);
210
+ };
211
+
212
+ function stopPoll(){
213
+ pollRunning = false;
214
+ window.clearInterval(sessionView.session_poll);
215
+ }
216
+
217
+ function doSessionPoll(){
218
+ return (function(){
219
+ $.ajax({
220
+ url: '/'+currentNamespace+'/sessions',
221
+ success: callbackSessionPoll()
222
+ });
223
+ });
224
+ };
225
+
226
+ function loadEventHistory(event_type){
227
+ feedInnerElem.html('');
228
+ $.ajax({
229
+ url: '/'+currentNamespace+'/events?type='+event_type,
230
+ success: function(_data, _status){
231
+ var data = JSON.parse(_data).events;
232
+ for(var n=data.length; n >= 0; n--){
233
+ if(data[n]){ renderEvent(data[n]); }
234
+ }
235
+ }
236
+ });
237
+ }
238
+
239
+ function callbackSessionPoll(){
240
+ return (function(_data, _status){
241
+ $.each(JSON.parse(_data).sessions, function(i,v){
242
+ updateSession(v);
243
+ });
244
+ sortSessions();
245
+ });
246
+ };
247
+
248
+ function loadEventTypes(){
249
+ $.ajax({
250
+ url: '/'+currentNamespace+'/event_types',
251
+ success: function(_data){
252
+ var data = JSON.parse(_data);
253
+ $(data.types).each(function(i,v){
254
+ if(v.slice(0,5)!='_set_'){ addEventType(v,v); }
255
+ });
256
+ }
257
+ });
258
+ };
259
+
260
+ function addEventType(type, display){
261
+ typeListElem.append(
262
+ $('<li class="event_type"></li>').append(
263
+ $('<span class="history"></span>').html('history')
264
+ .click(function(){
265
+ $('.event_type_list .event_type input').attr('checked', false);
266
+ $('input', $(this).parent()).attr('checked', true);
267
+ updateEventFilter(); loadEventHistory(type);
268
+ })
269
+ ).append(
270
+ $('<input type="checkbox" />').attr('checked', true)
271
+ .click(function(){ updateEventFilter(); })
272
+ ).append(
273
+ $('<span></span>').html(display)
274
+ ).attr('rel', type)
275
+ );
276
+ }
277
+
278
+ function updateEventFilter(){
279
+ var _unchecked_types = [];
280
+ $('ul.event_type_list li.event_type').each(function(i,v){
281
+ if(!$('input', v).attr('checked')){
282
+ _unchecked_types.push($(v).attr('rel'));
283
+ }
284
+ });
285
+ eventsFilter = _unchecked_types;
286
+ }
287
+
288
+ function doEventsPoll(){
289
+ return (function(){
290
+ $.ajax({
291
+ url: '/'+currentNamespace+'/events?since='+eventsPolledUntil,
292
+ success: callbackEventsPoll()
293
+ });
294
+ });
295
+ };
296
+
297
+ function callbackEventsPoll(){
298
+ return (function(_data, _status){
299
+ var data = JSON.parse(_data)
300
+ var events = data.events;
301
+ var timout = 1000;
302
+ var maxevents = 200;
303
+ if(events.length > 0){
304
+ timeout = 200;
305
+ eventsPolledUntil = parseInt(events[0]._time)-1;
306
+ }
307
+ for(var n=events.length-1; n >= 0; n--){
308
+ var v = events[n];
309
+ if(eventsFilter.indexOf(v._type) == -1){
310
+ if(parseInt(v._time)<=eventsPolledUntil){
311
+ renderEvent(v);
312
+ }
313
+ }
314
+ };
315
+ var elems = $("p", feedInnerElem);
316
+ for(var n=maxevents; n < elems.length; n++){
317
+ $(elems[n]).remove();
318
+ }
319
+ if(pollRunning){
320
+ window.setTimeout(doEventsPoll(), timout);
321
+ }
322
+ });
323
+ };
324
+
325
+ function updateSession(session_data){
326
+ sessionData[session_data.session_key] = session_data;
327
+ renderSession(session_data);
328
+ }
329
+
330
+ function sortSessions(){
331
+ console.log("fixme: sort and splice to 100");
332
+ }
333
+
334
+ function renderSession(session_data){
335
+
336
+ var session_name = session_data["_name"];
337
+ var session_time = formatTimeSince(session_data["_updated_at"]);
338
+ var session_elem = $('li[data-session='+session_data["session_key"]+']:first');
339
+
340
+ if(session_elem.length>0){
341
+
342
+ if(session_data["_picture"] && (session_data["_picture"].length > 1)){
343
+ $('.picture img', session_elem).attr('src', session_data["_picture"])
344
+ }
345
+
346
+ if(session_name){
347
+ $('.name', session_elem).html(session_name);
348
+ }
349
+
350
+ $('.time', session_elem).html(session_time);
351
+
352
+ } else {
353
+
354
+ var session_picture = $('<img width="18" />');
355
+
356
+ if(!session_name){
357
+ session_name = session_data["session_key"].substr(0,15)
358
+ };
359
+
360
+ if(session_data["_picture"]){
361
+ session_picture.attr('src', session_data["_picture"]);
362
+ };
363
+
364
+ listElem.append(
365
+ $('<li class="session"></li>').append(
366
+ $('<div class="picture"></div>').html(session_picture)
367
+ ).append(
368
+ $('<span class="name"></span>').html(session_name)
369
+ ).append(
370
+ $('<span class="time"></span>').html(session_time)
371
+ ).attr('data-session', session_data["session_key"])
372
+ );
373
+
374
+ }
375
+ };
376
+
377
+ function renderEvent(event_data){
378
+ var event_time = $('<span class="time"></span>');
379
+ var event_message = $('<span class="message"></span>');
380
+ var event_props = $('<span class="properties"></span>');
381
+ var event_picture = $('<div class="picture"></picture>');
382
+
383
+ var event_type = event_data._type;
384
+
385
+ if(!event_type){ return true; }
386
+
387
+ if(event_data._message){
388
+ event_message.html(event_data._message);
389
+ } else if(event_type=="_pageview"){
390
+ event_message.html("Pageview: " + event_data.url);
391
+ } else if(event_type.substr(0,5) == '_set_'){
392
+ return true; /* dont render */
393
+ } else {
394
+ event_message.html(event_type);
395
+ }
396
+
397
+ event_time.html(formatTimeOfDay(event_data._time));
398
+
399
+ if(event_data._session_key && event_data._session_key.length > 0){
400
+ if(session_data=sessionData[event_data._session_key]){
401
+ if(session_data._name){
402
+ event_props.append(
403
+ $('<strong></strong>').html(session_data._name)
404
+ );
405
+ }
406
+ if(session_data._picture){
407
+ event_picture.append(
408
+ $('<img width="40" />').attr('src', session_data._picture)
409
+ )
410
+ }
411
+ }
412
+ }
413
+
414
+ feedInnerElem.prepend(
415
+ $('<li class="feed_event"></li>')
416
+ .append(event_time)
417
+ .append(event_picture)
418
+ .append(event_message)
419
+ .append(event_props)
420
+ );
421
+ }
422
+
423
+ function close(){
424
+ stopPoll();
425
+ };
426
+
427
+ return {
428
+ load: load,
429
+ resize: resize,
430
+ close: close
431
+ };
432
+
433
+ });
434
+
435
+
436
+ var dashboardView = (function(dashboard_name){
437
+
438
+ var widgets = [];
439
+ var viewport = null;
440
+
441
+ function load(_viewport){
442
+ viewport = _viewport.html('');
443
+ /*alert('yay, new dashboard view loaded: ' + dashboard_name);*/
444
+ $.ajax({
445
+ url: '/'+currentNamespace+'/dashboard/'+dashboard_name,
446
+ success: function(resp, status){
447
+ var conf = JSON.parse(resp);
448
+ renderWidgets(conf.widgets);
449
+ }
450
+ });
451
+ };
452
+
453
+ function renderWidgets(_widgets){
454
+ for(wkey in _widgets){
455
+ var widget = _widgets[wkey];
456
+ widget["elem"] = $('<div class="widget"></div>').append(
457
+ $('<div class="headbar"></div>').html(widget.title)
458
+ ).append(
459
+ $('<div class="inner"></div>')
460
+ );
461
+ widgets[wkey] = widget;
462
+ viewport.append(widget.elem);
463
+ resizeWidget(wkey);
464
+ renderWidget(wkey);
465
+ };
466
+ resize();
467
+ };
468
+
469
+ function renderWidget(wkey){
470
+ var widget = widgets[wkey];
471
+ /* argh... */
472
+ if(widget.klass=='TimelineWidget'){ timelineWidget(widget).render(); }
473
+ if(widget.klass=='NumbersWidget'){ numbersWidget(widget).render(); }
474
+ };
475
+
476
+ function resizeWidget(wkey){
477
+ var widget = widgets[wkey];
478
+ var wwperc = widgets[wkey].width;
479
+ if(!wwperc){ wwperc = 100; }
480
+ var wwidth = viewport.width() * (wwperc/100.0);
481
+ if(wwperc==100){
482
+ widgets[wkey].elem.addClass('full_width');
483
+ } else { wwidth -= 1; }
484
+ widget.elem.width(wwidth);
485
+ }
486
+
487
+ function resize(){
488
+ for(wkey in widgets){
489
+ resizeWidget(wkey);
490
+ };
491
+ };
492
+
493
+ function close(){
494
+
495
+ };
496
+
497
+ return {
498
+ load: load,
499
+ resize: resize,
500
+ close: close
501
+ };
502
+
503
+ });
504
+
505
+
506
+ function renderDashboard(_dash){
507
+ loadView(dashboardView(_dash));
508
+ };
509
+
510
+ function renderSessionView(){
511
+ loadView(sessionView());
42
512
  }
43
513
 
44
- };
514
+ function loadView(_view){
515
+ if(currentView){ currentView.close(); }
516
+ canvasElem.html('loading!');
517
+ currentView = _view;
518
+ currentView.load(canvasElem);
519
+ resizeView();
520
+ };
521
+
522
+ function resizeView(){
523
+ currentView.resize(
524
+ canvasElem.innerWidth(),
525
+ canvasElem.innerHeight()
526
+ );
527
+ };
528
+
529
+ function init(_namespace, _canvasElem){
530
+ canvasElem = _canvasElem;
531
+ currentNamespace = _namespace;
532
+ loadView(sessionView());
533
+ };
534
+
535
+ return {
536
+ p: '/fnordmetric/',
537
+ renderDashboard: renderDashboard,
538
+ renderSessionView: renderSessionView,
539
+ resizeView: resizeView,
540
+ init: init
541
+ };
542
+
543
+ })();