batsd-dash 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,2 +1,9 @@
1
1
  source "http://rubygems.org"
2
2
  gemspec
3
+
4
+ # for autorunning tests
5
+ group :test do
6
+ gem "guard"
7
+ gem "guard-minitest"
8
+ end
9
+
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'minitest' do
5
+ # with Minitest::Unit
6
+ watch(%r|^test/(.*)\/?test_(.*)\.rb|)
7
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "test" }
8
+ watch(%r|^test/test_helper\.rb|) { "test" }
9
+ end
10
+
data/README.md CHANGED
@@ -17,69 +17,62 @@ Here is a sample rackup file (`config.ru`):
17
17
 
18
18
  require 'batsd-dash'
19
19
 
20
- # set batsd server setting BatsdDash::ConnectionPool.settings = { host:'localhost', port: 8127, pool_size: 4 }
20
+ # set batsd server setting
21
+ BatsdDash::ConnectionPool.settings = { host:'localhost', port: 8127, pool_size: 8 }
21
22
 
22
- # run the app run BatsdDash::App
23
+ # run the app
24
+ run BatsdDash::App
23
25
 
24
- Rack is very powerful. You can password protect your batsd-dash instance by
25
- using `Rack::Auth::Basic` or `Rack::Auth::Digest::MD5`.
26
+ Rack is very powerful. You can password protect your batsd-dash instance
27
+ by using `Rack::Auth::Basic` or `Rack::Auth::Digest::MD5`.
26
28
 
27
- ## Usage
29
+ ## Viewing Graphs
28
30
 
29
- ### Data API
31
+ Graphs are rendered using [nv.d3](http://nvd3.com/), a powerful graph
32
+ and visualization library.
30
33
 
31
- The application provides a simple JSON-based API for accessing data from the
32
- batds data server. There are 3 main routes provide, one for each datatype. These
33
- routes are `/counters`, `/timers` and `/gauges`. For example, the following
34
- request would access data for counter based metric:
35
-
36
- /counters?metric=a.b
37
-
38
- It's possible to access data for more than one metric within a single request.
39
- For example, the following request route will return data for both the `a.b`
40
- metric and the `c.d` metric:
34
+ Since rendering is all done on the client, we make use of hash based
35
+ navigation in order to reduce the amount of requests while still
36
+ maintaining 'linkability'.
41
37
 
42
- /counters?metrics[]=a.b&metrics[]=c.d
38
+ For example, to view a graph for the counter `a.b` statistic, you would need
39
+ to make the following request from your browser:
43
40
 
44
- The data API also accepts a `start` and `stop` unix timestamp parameter for
45
- accessing different ranges of data.
41
+ /graph#counters=a.b
46
42
 
47
- Note that, the data API will only respond with JSON if the `Accept` header to
48
- set to `application/json`!
43
+ The graph view will provide you with a date time picker to make selecting
44
+ different time ranges easier. Graphs are updated when you press the
45
+ 'View' button or when the URL is updated.
49
46
 
50
- ### Viewing Graphs
47
+ It's possible to view more than one metric at the same time. To do this,
48
+ visit the following route from your browser:
51
49
 
52
- Graphs are rendered using Flot, a JavaScript library which uses the canvas
53
- element to create graphs. Since rendering is all done on the client, we make use
54
- of hash based navigation in order to reduce the amount of requests and while
55
- maintaining 'linkability'.
50
+ /graph#counters=a.b,c.d
56
51
 
57
- For example, to view a graph for the `a.b` metric, you would make the following
58
- request from your browser:
52
+ You can also view different datatypes at the same time:
59
53
 
60
- /counters#metrics=a.b
54
+ /graph#counters=a.b&timers=x.y
61
55
 
62
- The graph view will provide you with a date time picker to make selecting
63
- different start and stop time ranges easier. Graphs are updated when you press
64
- the 'View' button.
56
+ __NOTE__: As of now, a single y-axis is used when datatypes are mixed.
57
+ Soon, we will add support for multiple axis when viewing mixed types.
65
58
 
66
- Much like the data API, it's possible to view more than one metric at the same
67
- time. To do this, visit the following route from your browser:
59
+ ## Data API
68
60
 
69
- /counters#metrics=a.b,c.d
61
+ The application provides a simple JSON-based API for accessing data from
62
+ the batds data server. The data API accepts similar parameters as the
63
+ graph view but uses traditional query strings instead:
70
64
 
71
- _TODO_ when no data or only a single point is available, the graph is a little
72
- strange looking. This is something we will improve upon. Additionally, we also
73
- plan to add some sort of tree-based widget for selecting different metrics to
74
- view.
65
+ /data?counters[]=a.b&counters[]=c.d&timers[]=x.y
75
66
 
76
- Feel free to submit pull requests with these features!
67
+ The data API also accepts a `start` and `stop` unix timestamp parameter
68
+ for accessing different ranges of data. Note that, the data API will
69
+ only respond with JSON if the `Accept` header to set to `application/json`!
77
70
 
78
- ### Zerofill
71
+ ## Graph and Render Options
79
72
 
80
- _TODO_ add details about zerofill.
73
+ 1. Zerofill:
74
+ __TODO__ Add details about zerofill
81
75
 
82
- _TODO_ Setup client to accept pass along no-zerofill options.
83
76
 
84
77
  ## Development
85
78
 
@@ -92,3 +85,7 @@ files, ensure you recompile the CSS. This is done by running:
92
85
 
93
86
  Additionally, it is highly recommended you use thin for development since this
94
87
  app uses EventMachine.
88
+
89
+ ## About
90
+
91
+ This is project is maintained and developed by the people behind [BreakBase](http://breakbase.com) ([@mikeycgto](https://twitter.com/mikeycgto) and [@btoconnor](https://twitter.com/btoconnor))
data/batsd-dash.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
12
12
  s.homepage = "https://github.com/mikeycgto/batsd-dash"
13
13
 
14
14
  s.summary = %q{batsd-dash}
15
- s.description = %q{batsd-dash - graphs and stuff from batds. yay.}
15
+ s.description = %q{batsd-dash - graphs and stuff from batsd. yay!}
16
16
 
17
17
  s.rubyforge_project = "batsd-dash"
18
18
 
@@ -2,42 +2,51 @@
2
2
  module BatsdDash
3
3
  module GraphHelper
4
4
  ##
5
- # This method works directly against values
6
- def collect_for_graph(values, opts = {})
7
- values.tap do |pts|
8
- # remap the values
9
- pts.map! { |pt| [pt['timestamp'].to_i * 1000, pt['value'].to_f] }
10
-
11
- # apply zerofill
12
- zero_fill!(pts, opts[:range], opts[:interval]) unless pts.empty? || !opts[:zero_fill]
13
- end
14
- end
5
+ # This method works directly against values. It will tranform all
6
+ # datapoint to an array where the first element is a milisecond
7
+ # timestamp and the second is a float value data point.
8
+ def values_for_graph(values, opts = {})
9
+ return values if values.empty?
15
10
 
16
- ##
17
- # The data better be normalized to the interval otherwise
18
- # this method may get pissed
19
- def zero_fill!(values, range, step)
20
- return values if step.zero?
11
+ values.tap do |pts|
12
+ step = opts[:interval] * 1000
13
+ range = opts[:range]
21
14
 
22
- # convert to milisec
23
- step *= 1000
15
+ # transform the first point
16
+ transform_point_at!(0, values)
24
17
 
25
- values.tap do |data|
26
18
  # start from the first timestamp
27
- time = data.first.first + step
19
+ time = values.first.first + step
28
20
  index = 0
29
21
 
30
- while obj = data[index += 1]
22
+ # loop through values to transform and zerofill
23
+ while index < values.size - 1
24
+ obj = transform_point_at!(index += 1, values)
25
+
31
26
  if obj.first <= time
32
27
  time += step
33
28
  next
34
29
  end
35
30
 
36
- data.insert(index, [time, 0])
37
-
31
+ # need to insert zerofilled point (if zerofill is enabled)
32
+ values.insert(index, [time, 0]) if opts[:zero_fill]
38
33
  time += step
39
34
  end
40
35
  end
41
36
  end
37
+
38
+ ##
39
+ # Transform a point at a given index
40
+ def transform_point_at!(index, values)
41
+ data_pt = values[index]
42
+
43
+ # we've already transformed this index (must be zerofill)
44
+ return data_pt unless Hash === data_pt
45
+
46
+ pt_time = data_pt['timestamp'].to_i * 1000
47
+ pt_value = data_pt['value'].to_f
48
+
49
+ values[index] = [pt_time, pt_value]
50
+ end
42
51
  end
43
52
  end
@@ -1,11 +1,17 @@
1
1
  # helpers for processing params and validating input
2
2
  module BatsdDash
3
3
  module ParamsHelper
4
- def parse_metrics
5
- metrics = params[:metrics]
6
- metrics = [metrics] unless Array === metrics
4
+ def parse_statistics
5
+ Hash.new { |hash,key| hash[key] = [] }.tap do |stats|
6
+ %w[ counters gauges timers ].each do |datatype|
7
+ list = params[datatype]
7
8
 
8
- metrics.tap { |list| list.reject! { |m| m.nil? || m.empty? } }
9
+ list = [list] unless Array === list
10
+ list.reject! { |m| m.nil? || m.empty? }
11
+
12
+ stats[datatype] = list unless list.empty?
13
+ end
14
+ end
9
15
  end
10
16
 
11
17
  def parse_time_range
@@ -16,7 +22,7 @@ module BatsdDash
16
22
 
17
23
  # 1 hr range
18
24
  # TODO make this setting?
19
- [ now - 3600 + 1, now ]
25
+ [ now - 1800, now ]
20
26
 
21
27
  else
22
28
  [start.to_i, stop.to_i].tap do |range|
@@ -1,15 +1,31 @@
1
- @import "compass/utilities/general/clearfix";
2
1
  @import "compass/css3";
2
+
3
+ @import "compass/utilities/general/clearfix";
3
4
  @import "compass/reset";
4
5
 
6
+ $green: #0F0;
7
+ $graphHeight:400px;
8
+ $graphWidth:880px;
9
+
10
+ @mixin absolute-position($height, $width){
11
+ height:$height;
12
+ width:$width;
13
+
14
+ position:absolute;
15
+ top: ($graphHeight / 2) - ($height / 2);
16
+ left: ($graphWidth / 2) - ($width / 2);
17
+ }
18
+
19
+
5
20
  html, body { height:100%; min-height:100%; background-color:#222; }
6
- h1, h2 { color:#0F0; font-family:"Courier New",Courier,monospace; }
21
+
22
+ h1, h2, strong, em { color:$green; font-family:"Courier New",Courier,monospace; }
7
23
  a { text-decoration:none; color:inherit; }
8
24
 
9
25
  .wrap { width:1000px; margin:0 auto; }
10
26
 
11
27
  #header {
12
- height:60px; border:1px solid #0F0; border-width:0 0 1px 0; margin:0 0 20px;
28
+ height:60px; border:1px solid $green; border-width:0 0 1px 0; margin:0 0 20px;
13
29
 
14
30
  background-color: #000000;
15
31
  @include filter-gradient(#000000, #222, vertical);
@@ -28,7 +44,6 @@ a { text-decoration:none; color:inherit; }
28
44
  }
29
45
  }
30
46
 
31
-
32
47
  #content {
33
48
  color:#FFF; width:1000px; margin:0 auto; font-size:18px;
34
49
 
@@ -40,9 +55,23 @@ a { text-decoration:none; color:inherit; }
40
55
  }
41
56
 
42
57
  .graph {
43
- width:900px; height:400px; margin:0 auto;
58
+ height:$graphHeight;
59
+ width:$graphWidth;
60
+
61
+ position:relative;
62
+ margin:0 auto;
63
+
64
+ em {
65
+ @include absolute-position(30px, 300px);
44
66
 
45
- h2 { text-align:center; margin:180px; }
67
+ padding:2px 0;
68
+ font-size:26px;
69
+ text-align:center;
70
+
71
+ -webkit-animation-name:blink;
72
+ -webkit-animation-duration:1s;
73
+ -webkit-animation-iteration-count:infinite;
74
+ }
46
75
  }
47
76
 
48
77
  .inputs {
@@ -53,4 +82,68 @@ a { text-decoration:none; color:inherit; }
53
82
  }
54
83
  }
55
84
 
85
+ #loading {
86
+ @include absolute-position(100px, 280px);
87
+
88
+ display:none;
89
+
90
+ strong {
91
+ float:right;
92
+ font-size:26px;
93
+ margin:36px 0 0;
94
+ }
95
+
96
+ #spinner {
97
+ position:relative;
98
+ float:left;
99
+ width:100px;
100
+ height:100px;
101
+
102
+ -webkit-animation-name: rotateSpinner;
103
+ -webkit-animation-duration:2s;
104
+ -webkit-animation-iteration-count:infinite;
105
+ -webkit-animation-timing-function:linear;
106
+
107
+ div {
108
+ width:10px;
109
+ height:30px;
110
+ background:$green;
111
+
112
+ position: absolute;
113
+ top:35px;
114
+ left:45px;
115
+ }
116
+
117
+ $i:1;
118
+ $rotate:0deg;
119
+ $opacity:0.12;
120
+
121
+ @while ($i <= 8) {
122
+ .bar#{$i} {
123
+ @include simple-transform(1, $rotate, 0px, -40px);
124
+
125
+ opacity:$opacity;
126
+ }
127
+
128
+ @if ($i % 2 == 0){
129
+ $opacity:$opacity + 0.13;
130
+ } @else {
131
+ $opacity:$opacity + 0.12;
132
+ }
133
+
134
+ $rotate: $rotate + 45deg;
135
+ $i: $i + 1;
136
+ }
137
+ }
138
+ }
139
+
140
+ @-webkit-keyframes rotateSpinner {
141
+ from { -webkit-transform:scale(0.5) rotate(0deg); }
142
+ to { -webkit-transform:scale(0.5) rotate(360deg); }
143
+ }
56
144
 
145
+ @-webkit-keyframes blink {
146
+ 0% { opacity:1.0; }
147
+ 50% { opacity:0.25; }
148
+ 100% { opacity:1.0; }
149
+ }
@@ -1,3 +1,3 @@
1
1
  module BatsdDash
2
- VERSION = '0.2.1'
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/batsd-dash.rb CHANGED
@@ -42,41 +42,44 @@ module BatsdDash
42
42
  end
43
43
  end
44
44
 
45
- %w[ counters timers gauges ].each do |datatype|
46
- # this route renders the template (with codes for the graph)
47
- get "/#{datatype}", :provides => :html do
48
- haml :view
49
- end
45
+ # this route renders the template (with codes for the graph)
46
+ get "/graph", :provides => :html do
47
+ haml :view
48
+ end
50
49
 
51
- # actual data API route
52
- get "/#{datatype}", :provides => :json do
53
- metrics = parse_metrics
54
- range = parse_time_range
50
+ # actual data API route
51
+ get "/data", :provides => :json do
52
+ statistics = parse_statistics
53
+ range = parse_time_range
55
54
 
56
- return render_error('Invalid time range') unless range
57
- return render_error('Invalid metrics') if metrics.empty?
55
+ return render_error('Invalid time range') unless range
56
+ return render_error('Invalid metrics') if statistics.empty?
58
57
 
59
- results = { range: range.dup.map! { |n| n * 1000 }, metrics: [] }
60
- collect_opts = { zero_fill: !params[:no_zero_fill], range: results[:range] }
58
+ results = []
59
+ options = {
60
+ range: range.dup.map { |n| n * 1000 },
61
+ zero_fill: !params[:no_zero_fill]
62
+ }
61
63
 
64
+ statistics.each do |datatype, metrics|
62
65
  metrics.each do |metric|
63
66
  statistic = "#{datatype}:#{metric}"
64
67
  deferrable = connection_pool.async_values(statistic, range)
65
68
 
66
69
  deferrable.errback { |e| return render_error(e.message) }
67
70
  deferrable.callback do |json|
68
- values = json[statistic]
71
+ options[:interval] ||= json['interval']
72
+
73
+ points = json[statistic] || []
74
+ values = values_for_graph(points, options)
69
75
 
70
- # merge in interval if its not already; interval is always same
71
- collect_opts.merge!(interval: json['interval'] || 0) unless collect_opts.has_key?(:interval)
72
- # process values for graphing and add to results
73
- results[:metrics] << { label: metric, data: collect_for_graph(values, collect_opts) }
76
+ results << { key: metric, type: datatype[0..-2], values: values }
74
77
  end
75
78
  end
76
-
77
- cache_control :no_cache, :no_store
78
- render_json results
79
79
  end
80
+
81
+ cache_control :no_cache, :no_store
82
+ render_json range: options[:range], interval: options[:interval], results: results
80
83
  end
81
84
  end
82
85
  end