batsd-dash 0.2.1 → 0.3.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 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