pencil 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,106 @@
1
+ require "models/base"
2
+ require "models/graph"
3
+ require "models/host"
4
+ require "set"
5
+
6
+ module Dash::Models
7
+ class Dashboard < Base
8
+ attr_accessor :graphs
9
+ attr_accessor :graph_opts
10
+
11
+ def initialize(name, params={})
12
+ super
13
+
14
+ @graphs = []
15
+ @graph_opts = {}
16
+ params["graphs"].each do |name|
17
+ # graphs map to option hashes
18
+ if name.instance_of?(Hash)
19
+ g = Graph.find(name.keys.first) # should only be one key
20
+ @graph_opts[g] = name[name.keys.first]||{}
21
+ else
22
+ raise "Bad format for graph (must be a hash)"
23
+ end
24
+
25
+ @graphs << g if g
26
+ end
27
+
28
+ end
29
+
30
+ def clusters
31
+ clusters = Set.new
32
+ @graphs.each { |g| clusters += get_valid_hosts(g)[1] }
33
+ clusters.sort
34
+ end
35
+
36
+ def get_all_hosts(cluster=nil)
37
+ hosts = Set.new
38
+ clusters = Set.new
39
+ @graphs.each do |g|
40
+ h, c = get_valid_hosts(g, cluster)
41
+ hosts += h
42
+ clusters += c
43
+ end
44
+ return hosts, clusters
45
+ end
46
+
47
+ def get_valid_hosts(graph, cluster=nil)
48
+ clusters = Set.new
49
+ if cluster
50
+ hosts = Host.find_by_cluster(cluster)
51
+ else
52
+ hosts = Host.all
53
+ hosts.each { |h| clusters << h.cluster }
54
+ end
55
+
56
+ # filter by what matches the graph definition
57
+ hosts = hosts.select { |h| h.multi_match(graph["hosts"]) }
58
+
59
+ # filter if we have a dashboard-level 'hosts' filter
60
+ if @params["hosts"]
61
+ hosts = hosts.select { |h| h.multi_match(@params["hosts"]) }
62
+ end
63
+
64
+ hosts.each { |h| clusters << h.cluster }
65
+
66
+ return hosts, clusters
67
+ end
68
+
69
+ def render_cluster_graph(graph, clusters, opts={})
70
+ # FIXME: edge case where the dash filter does not filter to a subset of
71
+ # the hosts filter
72
+
73
+ hosts = get_host_wildcards(graph)
74
+ opts[:sum] = :cluster unless opts[:zoom]
75
+ graph_url = graph.render_url(hosts.to_a, clusters, opts)
76
+ return graph_url
77
+ end
78
+
79
+ def get_host_wildcards(graph)
80
+ return graph_opts[graph]["hosts"] || @params["hosts"] || graph["hosts"]
81
+ end
82
+
83
+ def render_global_graph(graph, opts={})
84
+ hosts = get_host_wildcards(graph)
85
+ _, clusters = get_valid_hosts(graph)
86
+
87
+ next_url = ""
88
+ type = opts[:zoom] ? :cluster : :global
89
+ options = opts.merge({:sum => type})
90
+ graph_url = graph.render_url(hosts, clusters, options)
91
+ return graph_url
92
+ end
93
+
94
+ def self.find_by_graph(graph)
95
+ ret = []
96
+ Dashboard.each do |name, dash|
97
+
98
+ if dash["graphs"].map { |x| x.keys.first }.member?(graph.name)
99
+ ret << dash
100
+ end
101
+ end
102
+
103
+ return ret
104
+ end
105
+ end # Dash::Models::Dashboard
106
+ end # Dash::Models
@@ -0,0 +1,304 @@
1
+ require "models/base"
2
+ require "uri"
3
+
4
+ module Dash::Models
5
+ class Graph < Base
6
+ def initialize(name, params={})
7
+ super
8
+
9
+ @params["hosts"] ||= ["*"]
10
+ @params["title"] ||= name
11
+
12
+ if not @params["targets"]
13
+ raise ArgumentError, "graph #{name} needs a 'targets' map"
14
+ end
15
+ end
16
+
17
+ # fixme parameters in general
18
+ def width(opts={})
19
+ opts["width"] || @params[:url_opts][:width]
20
+ end
21
+
22
+ # translate STR into graphite-speak for applying FUNC to STR
23
+ # graphite functions take zero or one argument
24
+ # pass passes STR through, instead of raising an error if FUNC isn't
25
+ # recognized
26
+ def translate(func, str, arg=nil, pass=false)
27
+ # puts "calling translate"
28
+ # puts "func => #{func}"
29
+ # puts "str => #{str}"
30
+ # puts "arg => #{arg}"
31
+ # procs and lambdas don't support default arguments in 1.8, so I have to
32
+ # do this
33
+ z = lambda { |*body| "#{func}(#{body[0]||str})" }
34
+ y = "#{str}, #{arg}"
35
+ x = lambda { z.call(y) }
36
+
37
+ return \
38
+ case func.to_s
39
+ # comb
40
+ when "sumSeries", "averageSeries", "minSeries", "maxSeries", "group"
41
+ z.call
42
+ # transform
43
+ when "scale", "offset"
44
+ # perhaps .to_f
45
+ x.call
46
+ when "derivative", "integral"
47
+ z.call
48
+ when "nonNegativeDerivative"
49
+ z.call("#{str}#{', ' + arg if arg}")
50
+ when "log", "timeShift", "summarize", "hitcount",
51
+ # calculate
52
+ "movingAverage", "stdev", "asPercent"
53
+ x.call
54
+ when "diffSeries", "ratio"
55
+ z.call
56
+ # filters
57
+ when "mostDeviant"
58
+ z.call("#{arg}, #{str}")
59
+ when "highestCurrent", "lowestCurrent", "nPercentile", "currentAbove",
60
+ "currentBelow", "highestAverage", "lowestAverage", "averageAbove",
61
+ "averageBelow", "maximumAbove", "maximumBelow"
62
+ x.call
63
+ when "sortByMaxima", "minimalist"
64
+ z.call
65
+ when "limit", "exclude"
66
+ x.call
67
+ when "key", "alias"
68
+ "alias(#{str}, \"#{arg}\")"
69
+ when "cumulative", "drawAsInfinite"
70
+ z.call
71
+ when "lineWidth"
72
+ x.call
73
+ when "dashed", "keepLastValue"
74
+ z.call
75
+ when "substr", "threshold"
76
+ x.call
77
+ when "color"
78
+ str #color is handled elsewhere
79
+ else
80
+ raise "BAD FUNC #{func}" unless pass
81
+ str
82
+ end
83
+ end
84
+
85
+ # inner means we're dealing with a complex key; @params will be applied
86
+ # later on
87
+ def handle_metric(name, opts, inner=false)
88
+ ret = name.dup
89
+ if inner
90
+ @params.each do |k, v|
91
+ ret = translate(k, ret, v, true)
92
+ end
93
+ end
94
+ (opts||{}).each do |k, v|
95
+ #puts "#{k} => #{v}"
96
+ ret = translate(k, ret, v)
97
+ end
98
+ ret
99
+ end
100
+
101
+ def render_url(hosts, clusters, opts={})
102
+ opts = {
103
+ :sum => nil,
104
+ :title => @params["title"],
105
+ }.merge(opts)
106
+
107
+ if ! [:global, :cluster, nil].member?(opts[:sum])
108
+ raise ArgumentError, "render graph #{name}: invalid :sum - #{opts[:sum]}"
109
+ end
110
+
111
+ sym_hash = {}
112
+ (opts[:dynamic_url_opts]||[]).each do |k,v|
113
+ sym_hash[k.to_sym] = v
114
+ end
115
+
116
+ # fixme key checking may be necessary
117
+ url_opts = {
118
+ :title => opts[:title],
119
+ }.merge(@params[:url_opts]).merge(sym_hash)
120
+
121
+ url_opts[:from] = url_opts.delete(:stime) || ""
122
+ url_opts[:until] = url_opts.delete(:etime) || ""
123
+ url_opts.delete(:start)
124
+ url_opts.delete(:duration)
125
+
126
+ graphite_opts = [ "vtitle", "yMin", "yMax", "lineWidth", "areaMode",
127
+ "template", "lineMode", "bgcolor", "graphOnly", "hideAxes", "hideGrid",
128
+ "hideLegend", "fgcolor", "fontSize", "fontName", "fontItalic",
129
+ "fontBold" ]
130
+
131
+ @params.each do |k, v|
132
+ if graphite_opts.member?(k)
133
+ url_opts[k.to_sym] = v
134
+ end
135
+ end
136
+
137
+ target = []
138
+ colors = []
139
+ #fixme code duplication
140
+ if opts[:sum] == :global
141
+ @params["targets"].each do |stat_name, opts|
142
+ z = opts.dup
143
+ # opts['key'] ||= stat_name
144
+ z[:key] ||= stat_name
145
+ #######################
146
+ if stat_name.instance_of?(Array)
147
+ metric = stat_name.map do |m|
148
+ mm = compose_metric(m.keys.first, clusters.to_a.join(","),
149
+ hosts.to_a.join(","))
150
+ handle_metric(mm, m[m.keys.first], true)
151
+ end.join(",")
152
+ else
153
+ metric = "#{stat_name}.{#{clusters.to_a.join(',')}}" +
154
+ ".{#{hosts.to_a.join(',')}}"
155
+ metric = handle_metric(metric, {}, true)
156
+ end
157
+ #######################
158
+ z[:key] = "global #{z[:key]}"
159
+ target << handle_metric("sumSeries(#{metric})", z)
160
+ colors << next_color(colors, z[:color])
161
+ end # @params["targets"].each
162
+ elsif opts[:sum] == :cluster # one line per cluster/metric
163
+ clusters.each do |cluster|
164
+ @params["targets"].each do |stat_name, opts|
165
+ z = opts.dup
166
+ metrics = []
167
+ hosts.each do |host|
168
+ #######################
169
+ if stat_name.instance_of?(Array)
170
+ metrics << stat_name.map do |m|
171
+ mm = compose_metric(m.keys.first, cluster, host)
172
+ handle_metric(mm, m[m.keys.first], true)
173
+ end.join(",")
174
+ else
175
+ metrics << handle_metric(compose_metric(stat_name, cluster, host), {}, true)
176
+ end
177
+ #######################
178
+ end # hosts.each
179
+
180
+ z[:key] = "#{cluster} #{z[:key]}"
181
+ target << handle_metric("sumSeries(#{metrics.join(',')})", z)
182
+ colors << next_color(colors, z[:color])
183
+ end # metrics.each
184
+ end # clusters.each
185
+ else # one line per {metric,host,colo}
186
+ @params["targets"].each do |stat_name, opts|
187
+ clusters.each do |cluster|
188
+ hosts.each do |host|
189
+ label = "#{host} #{opts[:key]}"
190
+ #################
191
+ if stat_name.instance_of?(Array)
192
+ metric = stat_name.map do |m|
193
+ mm = compose_metric(m.keys.first, cluster, host)
194
+ handle_metric(mm, m[m.keys.first], true)
195
+ end.join(",")
196
+ else
197
+ metric = handle_metric(compose_metric(stat_name, cluster, host), {}, true)
198
+ end
199
+ #################
200
+
201
+ if label =~ /\*/
202
+ # for this particular type of graph, don't display a legend
203
+ url_opts[:hideLegend] = true
204
+ z = opts.dup
205
+ # fixme proper labeling... maybe
206
+ # With wildcards let graphite construct the legend (or not).
207
+ # Since we're handling wildcards we don't know how many
208
+ # hosts will match, so just put in the default color list.
209
+ # technically we do know, so this can be fixed
210
+ z.delete(:key)
211
+ target << handle_metric(metric, z)
212
+ colors.concat(@params[:default_colors]) if colors.empty?
213
+ else
214
+ z = opts.dup
215
+ z[:key] = "#{host}/#{cluster} #{opts[:key]}"
216
+ target << handle_metric(metric, z)
217
+ colors << next_color(colors, opts[:color])
218
+ end
219
+ end
220
+ end
221
+ end # @params["targets"].each
222
+ end # if opts[:sum]
223
+
224
+ url_opts[:target] = target
225
+ url_opts[:colorList] = colors.join(",")
226
+
227
+ url = URI.join(@params[:graphite_url], "/render/?").to_s
228
+ url_parts = []
229
+ url_opts.each do |k, v|
230
+ [v].flatten.each do |v|
231
+ url_parts << "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}"
232
+ end
233
+ end
234
+ url += url_parts.join("&")
235
+ return url
236
+ end
237
+
238
+ # return an array of all metrics matching the specifications in
239
+ # @params["targets"]
240
+ # metrics are arrays of fields (once delimited by periods)
241
+ def expand
242
+ url = URI.join(@params[:graphite_url], "/metrics/expand/?query=").to_s
243
+ metrics = []
244
+
245
+ @params["targets"].each do |metric|
246
+ if metric.first.instance_of?(Array)
247
+ metric.first.each do |m|
248
+ composed = compose_metric(m.first.first, "*", "*")
249
+ query = open("#{url}#{composed}").read
250
+ metrics << JSON.parse(query)["results"]
251
+ end
252
+ else
253
+ composed = compose_metric(metric.first.first, "*", "*")
254
+ query = open("#{url}#{composed}").read
255
+ metrics << JSON.parse(query)["results"]
256
+ end
257
+ end
258
+
259
+ return metrics.flatten.map { |x| x.split(".") }
260
+ end
261
+
262
+ def hosts_clusters
263
+ metrics = expand
264
+ clusters = Set.new
265
+
266
+ # field -1 is the host name, and -2 is its cluster
267
+ hosts = metrics.map do |x|
268
+ Host.new(x[-1], @params.merge({ "cluster" => x[-2] }))
269
+ end.uniq
270
+
271
+ # filter by what matches the graph definition
272
+ hosts = hosts.select { |h| h.multi_match(@params["hosts"]) }
273
+ hosts.each { |h| clusters << h.cluster }
274
+
275
+ return hosts, clusters
276
+ end
277
+
278
+ private
279
+ def next_color(colors, preferred_color=nil)
280
+ default_colors = @params[:default_colors].clone
281
+
282
+ if preferred_color and !colors.member?(preferred_color)
283
+ return preferred_color
284
+ end
285
+
286
+ if preferred_color and ! default_colors.member?(preferred_color)
287
+ default_colors << preferred_color
288
+ end
289
+
290
+ weights = Hash.new { |h, k| h[k] = 0 }
291
+ colors.each do |c|
292
+ weights[c] += 1
293
+ end
294
+
295
+ i = 0
296
+ loop do
297
+ default_colors.each do |c|
298
+ return c if weights[c] == i
299
+ end
300
+ i += 1
301
+ end
302
+ end
303
+ end # Dash::Models::Graph
304
+ end # Dash::Models
@@ -0,0 +1,82 @@
1
+ require "models/base"
2
+ require "models/graph"
3
+
4
+ module Dash::Models
5
+ class Host < Base
6
+ attr_accessor :graphs
7
+
8
+ def initialize(name, params={})
9
+ super
10
+
11
+ @graphs = []
12
+ Graph.each do |graph_name, graph|
13
+ graph["hosts"].each do |h|
14
+ if match(h)
15
+ @graphs << graph
16
+ break
17
+ end
18
+ end # graph["hosts"].each
19
+ end # Graph.each
20
+ end
21
+
22
+ def cluster
23
+ return @params["cluster"]
24
+ end
25
+
26
+ def key
27
+ "#{@cluster}#{@name}"
28
+ end
29
+
30
+ def eql?(other)
31
+ key == other.key
32
+ end
33
+
34
+ def ==(other)
35
+ key == other.key
36
+ end
37
+
38
+ def <=>(other)
39
+ if @params[:host_sort] == "builtin"
40
+ return key <=> other.key
41
+ elsif @params[:host_sort] == "numeric"
42
+ regex = /\d+/
43
+ match = @name.match(regex)
44
+ match2 = other.name.match(regex)
45
+ if match.pre_match != match2.pre_match
46
+ return match.pre_match <=> match2.pre_match
47
+ else
48
+ return match[0].to_i <=> match2[0].to_i
49
+ end
50
+ else
51
+ # http://www.bofh.org.uk/2007/12/16/comprehensible-sorting-in-ruby
52
+ sensible = lambda do |k|
53
+ k.to_s.split(
54
+ /((?:(?:^|\s)[-+])?(?:\.\d+|\d+(?:\.\d+?(?:[eE]\d+)?(?:$|(?![eE\.])))?))/ms
55
+ ).map { |v| Float(v) rescue v.downcase }
56
+ end
57
+ return sensible.call(self) <=> sensible.call(other)
58
+ end
59
+ end
60
+
61
+ def hash
62
+ key.hash
63
+ end
64
+
65
+ def self.find_by_name_and_cluster(name, cluster)
66
+ Host.each do |host_name, host|
67
+ next unless host_name = name
68
+ return host if host.cluster == cluster
69
+ end
70
+ return nil
71
+ end
72
+
73
+ def self.find_by_cluster(cluster)
74
+ ret = []
75
+ Host.each do |name, host|
76
+ ret << host if host.cluster == cluster
77
+ end
78
+ return ret
79
+ end
80
+
81
+ end # Dash::Models::Host
82
+ end # Dash::Models
data/lib/models.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "models/dashboard"
2
+ require "models/graph"
3
+ require "models/host"
data/lib/namespace.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Dash
2
+ module Models
3
+ end
4
+ end
Binary file
@@ -0,0 +1,97 @@
1
+ h2 {
2
+ color: #ADFF2F;
3
+ }
4
+
5
+ h3 {
6
+ color: #32cd32;
7
+ }
8
+
9
+ .graphtitle {
10
+ text-shadow: 1px 1px 1px #333333;
11
+ }
12
+
13
+ .dashlinks {
14
+ color: green;
15
+ font-size: small;
16
+ }
17
+
18
+ a {
19
+ color: #15317E;
20
+ }
21
+
22
+ a:visited {
23
+ color: #15317E;
24
+ }
25
+
26
+ .graphsection {
27
+ border: dotted 2px #333333;
28
+ width: 500;
29
+ margin-left: 5px;
30
+ margin-top: 10px;
31
+ }
32
+
33
+ div#wrap {
34
+ width: 100%;
35
+ margin-top: 10px;
36
+ margin-bottom: 10px;
37
+ margin-left: auto;
38
+ margin-right: auto;
39
+ padding: 0px;
40
+ }
41
+
42
+ div#header {
43
+ padding: 15px;
44
+ margin: 0px;
45
+ margin-left: 23%;
46
+ }
47
+
48
+ div#nav {
49
+ width: 20%;
50
+ padding: 10px;
51
+ margin-top: 1px;
52
+ float: left;
53
+ color: green;
54
+ }
55
+
56
+ div#main {
57
+ margin-left: 23%;
58
+ margin-top: 1px;
59
+ padding: 10px;
60
+ color: #4CBB17;
61
+ }
62
+
63
+ div#footer {
64
+ padding: 15px;
65
+ margin: 0px;
66
+ border-top: thin solid #808080;
67
+ }
68
+
69
+ table {
70
+ color: green;
71
+ }
72
+
73
+ input {
74
+ border: solid 1px #15317E;
75
+ color: green;
76
+ background-color: #1f1f1f;
77
+ font-size: small;
78
+ }
79
+
80
+ .invisible {
81
+ display: none;
82
+ }
83
+
84
+ select {
85
+ border: solid 1px #15317E;
86
+ color: green;
87
+ background-color: #1f1f1f;
88
+ }
89
+
90
+ .select2 {
91
+ color: #4CBB17;
92
+ font-size: large;
93
+ }
94
+
95
+ .error {
96
+ color: red;
97
+ }
@@ -0,0 +1,23 @@
1
+ <h2>List of dashboards for <%= cluster_selector %></h2>
2
+
3
+ <% hosts = Host.all.select { |h| h.cluster == @cluster }
4
+ boards = @dashboards.select { |d| d.clusters.member?(@cluster) }
5
+ seen_hosts = Set.new %>
6
+
7
+ <% boards.each do |b| %>
8
+ <h2><a href="/dash/<%= "#{@cluster}/#{append_query_string(b.name)}" %>"><%= b.name %></a>
9
+ <% dash_hosts = b.get_all_hosts(@cluster)[0]
10
+ seen_hosts += dash_hosts %>
11
+ <%= hosts_selector(dash_hosts) %>
12
+ </h2>
13
+ <%= b.graphs.collect do |g|
14
+ href = append_query_string("/dash/#{@cluster}/#{b.name}/#{g}")
15
+ "<li><a href=\"#{href}\">#{g}</a><br></li>"
16
+ end %>
17
+ <br>
18
+ <% end %>
19
+ <h3>Other Hosts (not associated with a dashboard)</h3>
20
+ <%= (hosts.to_set - seen_hosts).sort.collect do |h|
21
+ href = append_query_string("/host/#{@cluster}/#{h}")
22
+ "<li><a href=\"#{href}\">#{h}</a></li>"
23
+ end %>
@@ -0,0 +1,36 @@
1
+ <% hosts = @dash.get_valid_hosts(@zoom, @cluster)[0].sort %>
2
+
3
+ <%= cluster_switcher %>
4
+ <%= graph_switcher %>
5
+ <%= graph_uplink %>
6
+ <br>
7
+ <br>
8
+ <a href="#by_host">by_host</a>
9
+ <%= hosts.collect { |h| "<a href=\"##{h}\">#{h}</a>" }.join(" ") %>
10
+
11
+ <div class="graphsection" style="width:<%= @zoom.width(merge_opts) %>;">
12
+ <span class="graphtitle"><%= @zoom.name %> / <%= @cluster %> :: summary</span>
13
+ <div class="graph">
14
+ <img src="<%= @dash.render_cluster_graph(@zoom, @cluster, :dynamic_url_opts => merge_opts) %>">
15
+ </div>
16
+ </div>
17
+
18
+ <div class="graphsection" style="width:<%= @zoom.width(merge_opts) %>;">
19
+ <a name="by_host">
20
+ <span class="graphtitle"><%= @zoom.name %> / <%= @cluster %> :: by host</span>
21
+ <div class="graph">
22
+ <img src="<%= @dash.render_cluster_graph(@zoom, @cluster, :zoom => true, :dynamic_url_opts => merge_opts) %>">
23
+ </div>
24
+ </div>
25
+
26
+ <% hosts.each do |host| %>
27
+ <div class="graphsection" style="width:<%= @zoom.width(merge_opts) %>;">
28
+ <a name="<%= host %>">
29
+ <% image_url, zoom_url = cluster_zoom_graph(@zoom, @cluster, host, "#{@zoom.name} / #{@cluster} / #{host}") %>
30
+ <span class="graphtitle"><%= @zoom.name %> / <%= @cluster %> / <%= host %></span>
31
+ <span class="dashlinks">(<a href="<%= zoom_url %>">host</a>)</span>
32
+ <div class="graph">
33
+ <a href="<%= zoom_url %>"><img src="<%= image_url %>"></a>
34
+ </div>
35
+ </div>
36
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <%= cluster_switcher %>
2
+ <%= dash_switcher %>
3
+ <%= dash_uplink %>
4
+ <br>
5
+ <br>
6
+ <%= @dash.graphs.collect { |g| "<a href=\"##{g.name}\">#{g.name}</a>" }.join(" ") %>
7
+
8
+ <% @dash.graphs.each do |g| %>
9
+ <div class="graphsection" style="width:<%= g.width(merge_opts) %>;">
10
+ <a name="<%= g.name %>">
11
+ <% image_url, zoom_url = cluster_graph(g, @cluster, "#{g.name} / #{@cluster}") %>
12
+ <span class="graphtitle"><%= g.name %></span>
13
+ <span class="dashlinks">(<a href="<%= zoom_url %>">zoom</a>)</span>
14
+ <div class="graph">
15
+ <a href="<%= zoom_url %>"><img src="<%= image_url %>"></a>
16
+ </div>
17
+ </div>
18
+ <% end %>