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.
- data/Gemfile +6 -0
- data/Gemfile.lock +21 -0
- data/Procfile +1 -2
- data/VERSION +1 -1
- data/_spec/app_spec.rb +178 -0
- data/{spec → _spec}/cache_spec.rb +0 -0
- data/{spec → _spec}/combine_metric_spec.rb +0 -0
- data/{spec → _spec}/core_spec.rb +0 -0
- data/{spec → _spec}/count_metric_spec.rb +0 -0
- data/_spec/dashboard_spec.rb +67 -0
- data/_spec/event_spec.rb +46 -0
- data/{spec → _spec}/metric_spec.rb +0 -0
- data/{spec → _spec}/report_spec.rb +0 -0
- data/{spec → _spec}/sum_metric_spec.rb +0 -0
- data/_spec/widget_spec.rb +107 -0
- data/doc/import_dump.rb +26 -0
- data/em_runner.rb +33 -0
- data/fnordmetric.gemspec +59 -20
- data/haml/app.haml +26 -12
- data/lib/fnordmetric.rb +150 -15
- data/lib/fnordmetric/app.rb +70 -11
- data/lib/fnordmetric/cache.rb +4 -4
- data/lib/fnordmetric/context.rb +65 -0
- data/lib/fnordmetric/dashboard.rb +16 -12
- data/lib/fnordmetric/event.rb +65 -15
- data/lib/fnordmetric/gauge.rb +46 -0
- data/lib/fnordmetric/gauge_calculations.rb +43 -0
- data/lib/fnordmetric/gauge_modifiers.rb +43 -0
- data/lib/fnordmetric/inbound_stream.rb +66 -0
- data/lib/fnordmetric/logger.rb +38 -0
- data/lib/fnordmetric/namespace.rb +120 -0
- data/lib/fnordmetric/numbers_widget.rb +29 -11
- data/lib/fnordmetric/session.rb +131 -0
- data/lib/fnordmetric/standalone.rb +31 -0
- data/lib/fnordmetric/timeline_widget.rb +29 -9
- data/lib/fnordmetric/widget.rb +50 -45
- data/lib/fnordmetric/worker.rb +80 -0
- data/pub/fnordmetric/fnordmetric.css +76 -9
- data/pub/fnordmetric/fnordmetric.js +541 -42
- data/pub/raphael-min.js +8 -0
- data/pub/raphael-utils.js +221 -0
- data/readme.rdoc +172 -27
- data/server.rb +22 -0
- data/spec/app_spec.rb +359 -117
- data/spec/context_spec.rb +42 -0
- data/spec/dashboard_spec.rb +7 -47
- data/spec/event_spec.rb +114 -33
- data/spec/gauge_modifiers_spec.rb +276 -0
- data/spec/gauge_spec.rb +128 -0
- data/spec/namespace_spec.rb +104 -0
- data/spec/session_spec.rb +231 -0
- data/spec/spec_helper.rb +27 -4
- data/spec/widget_spec.rb +81 -75
- data/spec/worker_spec.rb +37 -0
- data/test_stream.sh +187 -0
- data/ulm_stats.rb +198 -0
- metadata +114 -35
- data/lib/fnordmetric/core.rb +0 -66
- data/lib/fnordmetric/engine.rb +0 -3
data/lib/fnordmetric/widget.rb
CHANGED
@@ -1,75 +1,80 @@
|
|
1
1
|
class FnordMetric::Widget
|
2
2
|
|
3
|
-
attr_accessor :
|
3
|
+
attr_accessor :gauges, :tick
|
4
4
|
|
5
|
-
def initialize(
|
6
|
-
@
|
7
|
-
|
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
|
-
@
|
16
|
+
@opts[:title]
|
12
17
|
end
|
13
18
|
|
14
|
-
def
|
15
|
-
[
|
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
|
21
|
-
|
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
|
25
|
-
|
37
|
+
def error!(msg)
|
38
|
+
FnordMetric.error!(msg)
|
26
39
|
end
|
27
40
|
|
28
|
-
def
|
29
|
-
|
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
|
-
|
34
|
-
|
35
|
-
}.compact
|
48
|
+
ensure_has_tick!
|
49
|
+
range.step(@tick)
|
36
50
|
end
|
37
51
|
|
38
52
|
def default_range(now=Time.now)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
56
|
-
|
60
|
+
def include_current?
|
61
|
+
!(@opts[:include_current] == false)
|
57
62
|
end
|
58
|
-
|
59
|
-
def
|
60
|
-
|
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
|
64
|
-
|
72
|
+
def render
|
73
|
+
data
|
65
74
|
end
|
66
75
|
|
67
|
-
def
|
68
|
-
|
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:#
|
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
|
-
|
4
|
+
.topbar{ height:43px; background:#24272c; display:none; }
|
5
5
|
|
6
|
-
#
|
7
|
-
|
8
|
-
#tabs
|
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:#
|
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{
|
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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
}
|
11
|
-
|
12
|
-
|
13
|
-
var
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
var
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
+
})();
|