riemann-dash 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011 Kyle Kingsbury
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,51 @@
1
+ Riemann-Dash
2
+ ============
3
+
4
+ An extensible Sinatra dashboard for Riemann. Connects to Riemann over the
5
+ network and shows events matching the queries you configure.
6
+
7
+ Get started
8
+ ==========
9
+
10
+ ``` bash
11
+ gem install riemann-dash
12
+ riemann-dash
13
+ ```
14
+
15
+ Riemann-dash will connect to a local Riemann server on port 5555, and display a
16
+ basic dashboard of all events in that server's index.
17
+
18
+ Configuring
19
+ ===========
20
+
21
+ Riemann-dash takes an optional config file, which you can specify as the first
22
+ command-line argument. If none is given, it looks for a file in the local
23
+ directory: config.rb. That file can override any configuration options on the
24
+ Dash class (hence all Sinatra configuration) as well as the Riemann client
25
+ options, etc.
26
+
27
+ ``` ruby
28
+ set :port, 6000 # HTTP server on port 6000
29
+ config[:client][:host] = 'my.ustate.server'
30
+ ```
31
+
32
+ You'll probably want a more specific dashboard:
33
+
34
+ ``` ruby
35
+ config[:view] = 'my/custom/view'
36
+ ```
37
+
38
+ Then you can write your own index.erb (and other views too, if you like). I've
39
+ provided an default stylesheet, layout, and dashboard in
40
+ lib/riemann/dash/views--as well as an extensive set of functions for laying out
41
+ events from a given query: see lib/riemann/dash/helper/renderer.rb.
42
+
43
+ A long history with cacti, nagios, and the like has convinced me that a.) web
44
+ configuration of dashboards is inevitably slower than just writing the code and
45
+ b.) you're almost certainly going to want to need more functions than I can
46
+ give you. My goal is to give you the tools to make it easier and get out of
47
+ your way.
48
+
49
+ An example config.rb, additional controllers, views, and public directory are
50
+ all in doc/dash. Should give you ideas for extending the dashboard for your own
51
+ needs.
data/bin/riemann-dash ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
4
+ require 'riemann/dash'
5
+
6
+ Riemann::Dash.load ARGV.first
7
+ Riemann::Dash.run!
@@ -0,0 +1,5 @@
1
+ class Riemann::Dash
2
+ get '/css' do
3
+ scss :css, :layout => false
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Riemann::Dash
2
+ get '/' do
3
+ erb :index
4
+ end
5
+ end
@@ -0,0 +1,258 @@
1
+ module Riemann
2
+ class Dash
3
+ helpers do
4
+ include ::Rack::Utils
5
+
6
+ alias_method :h, :escape_html
7
+
8
+ # Returns a scalar factor from 0.2 to 1, where 0.2 is "on the order of
9
+ # age_scale ago", and 1 is "very recent"
10
+ def age_fraction(time)
11
+ return 1 if time.nil?
12
+
13
+ x = 1 - ((Time.now.to_f - time) / Dash.config[:age_scale])
14
+ if x < 0.2
15
+ 0.2
16
+ elsif x > 1
17
+ 1
18
+ else
19
+ x
20
+ end
21
+ end
22
+
23
+ # Finds the longest common prefix of a list of strings.
24
+ # i.e. 'abc, 'ab', 'abdf' => 'ab'
25
+ def longest_common_prefix(strings, prefix = '')
26
+ return strings.first if strings.size <= 1
27
+
28
+ first = strings[0][0,1] or return prefix
29
+ tails = strings[1..-1].inject([strings[0][1..-1]]) do |tails, string|
30
+ if string[0,1] != first
31
+ return prefix
32
+ else
33
+ tails << string[1..-1]
34
+ end
35
+ end
36
+
37
+ longest_common_prefix(tails, prefix + first)
38
+ end
39
+
40
+ # An overview of states
41
+ def state_list(states)
42
+ ul(states.map { |s| state_short s })
43
+ end
44
+
45
+ def state_grid(states = Dash.client.query)
46
+ h2('States by Host') +
47
+ table(
48
+ *Event.partition(states, :host).map do |host, states|
49
+ tr(
50
+ th(host, class: 'host'),
51
+ *Event.sort(states, :service).map do |state|
52
+ state_short state
53
+ end
54
+ )
55
+ end
56
+ )
57
+ end
58
+
59
+ # Renders a state as the given HTML tag with a % width corresponding to
60
+ # metric / max.
61
+ def state_bar(s, opts = {})
62
+ opts = {tag: 'div', max: 1}.merge opts
63
+
64
+ return '' unless s
65
+ x = s.metric
66
+
67
+ # Text
68
+ text = case x
69
+ when Float
70
+ '%.2f' % x
71
+ when Integer
72
+ x.to_s
73
+ else
74
+ '?'
75
+ end
76
+
77
+ # Size
78
+ size = begin
79
+ (x || 0) * 100 / opts[:max]
80
+ rescue ZeroDivisionError
81
+ 0
82
+ end
83
+ size = "%.2f" % size
84
+
85
+ tag opts[:tag], h(text),
86
+ :class => "state #{s.state}",
87
+ style: "opacity: #{age_fraction s.time}; width: #{size}%",
88
+ title: s.description
89
+ end
90
+
91
+ # Renders a set of states in a chart. Each row is a given host, each
92
+ # service is a column. Each state is shown as a bar with an inferred
93
+ # maximum for the entire service, so you can readily compare multiple
94
+ # hosts.
95
+ #
96
+ # Takes a a set of states and options:
97
+ # title: the title of the chart. Inferred to be the longest common
98
+ # prefix of all services.
99
+ # maxima: maps each service to the maximum value used to display its
100
+ # bar.
101
+ # service_names: maps each service to a friendly name. Default service
102
+ # names have common prefixes removed.
103
+ # hosts: an array of hosts for rows. Default is every host present in
104
+ # states, sorted.
105
+ # transpose: Hosts go across, services go down. Enables :global_maxima.
106
+ # global_maximum: Compute default maxima for services globally,
107
+ # instead of a different maximum for each service.
108
+ def state_chart(states, opts = {})
109
+ o = {
110
+ :maxima => {},
111
+ :service_names => {}
112
+ }.merge opts
113
+ if o[:transpose] and not o.include?(:global_maximum)
114
+ o[:global_maximum] = true
115
+ end
116
+
117
+ # Get all services
118
+ services = states.map { |s| s.service }.compact.uniq.sort
119
+
120
+ # Figure out what name to use for each service.
121
+ prefix = longest_common_prefix services
122
+ service_names = services.inject({}) do |names, service|
123
+ names[service] = service[prefix.length..-1]
124
+ names
125
+ end.merge o[:service_names]
126
+
127
+ # Compute maximum for each service
128
+ maxima = if o[:global_maximum]
129
+ max = states.map(&:metric).compact.max
130
+ services.inject({}) do |m, s|
131
+ m[s] = max
132
+ m
133
+ end.merge o[:maxima]
134
+ else
135
+ states.inject(Hash.new(0)) do |m, s|
136
+ if s.metric
137
+ m[s.service] = [s.metric, m[s.service]].max
138
+ end
139
+ m
140
+ end.merge o[:maxima]
141
+ end
142
+
143
+ # Compute union of all hosts for these states, if no
144
+ # list of hosts explicitly given.
145
+ hosts = o[:hosts] || states.map do |state|
146
+ state.host
147
+ end
148
+ hosts = hosts.uniq.sort { |a, b|
149
+ if !a
150
+ -1
151
+ elsif !b
152
+ 1
153
+ else
154
+ a <=> b
155
+ end
156
+ }
157
+
158
+ # Construct index
159
+ by = states.inject({}) do |index, s|
160
+ index[[s.host, s.service]] = s
161
+ index
162
+ end
163
+
164
+ # Title
165
+ title = o[:title] || prefix.capitalize rescue 'Unknown'
166
+
167
+ if o[:transpose]
168
+ h2(title) +
169
+ table(
170
+ tr(
171
+ th,
172
+ *hosts.map do |host|
173
+ th host
174
+ end
175
+ ),
176
+ *services.map do |service|
177
+ tr(
178
+ th(service_names[service]),
179
+ *hosts.map do |host|
180
+ s = by[[host, service]]
181
+ td(
182
+ s ? state_bar(s, max: maxima[service]) : nil
183
+ )
184
+ end
185
+ )
186
+ end,
187
+ :class => 'chart'
188
+ )
189
+ else
190
+ h2(title) +
191
+ table(
192
+ tr(
193
+ th,
194
+ *services.map do |service|
195
+ th service_names[service]
196
+ end
197
+ ),
198
+ *hosts.map do |host|
199
+ tr(
200
+ th(host),
201
+ *services.map do |service|
202
+ s = by[[host, service]]
203
+ td(
204
+ s ? state_bar(s, max: maxima[service]) : nil
205
+ )
206
+ end
207
+ )
208
+ end,
209
+ :class => 'chart'
210
+ )
211
+ end
212
+ end
213
+
214
+ # Renders a state as a short tag.
215
+ def state_short(s, opts={tag: 'li'})
216
+ if s
217
+ "<#{opts[:tag]} class=\"state #{s.state}\" style=\"opacity: #{age_fraction s.time}\" title=\"#{h s.description}\">#{h s.host} #{h s.service}</#{opts[:tag]}>"
218
+ else
219
+ "<#{opts[:tag]} class=\"service\"></#{opts[:tag]}>"
220
+ end
221
+ end
222
+
223
+ # Renders a time to an HTML tag.
224
+ def time(unix)
225
+ t = Time.at(unix)
226
+ "<time datetime=\"#{t.iso8601}\">#{t.strftime(Dash.config[:strftime])}</time>"
227
+ end
228
+
229
+ # Renders an HTML tag
230
+ def tag(tag, *a)
231
+ if Hash === a.last
232
+ opts = a.pop
233
+ else
234
+ opts = {}
235
+ end
236
+
237
+ attrs = opts.map do |k,v|
238
+ "#{k}=\"#{h v}\""
239
+ end.join ' '
240
+
241
+ content = if block_given?
242
+ a << yield
243
+ else
244
+ a
245
+ end.flatten.join("\n")
246
+
247
+ s = "<#{tag} #{attrs}>#{content}</#{tag}>"
248
+ end
249
+
250
+ # Specific tag aliases
251
+ %w(div span h1 h2 h3 h4 h5 h6 ul ol li table th tr td u i b).each do |tag|
252
+ class_eval "def #{tag}(*a, &block)
253
+ tag #{tag.inspect}, *a, &block
254
+ end"
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,16 @@
1
+ class Riemann::Dash::Static
2
+ def initialize(app, options = {})
3
+ @app = app
4
+ @root = options[:root] or raise ArgumentError, "no root"
5
+ @file_server = ::Rack::File.new(@root)
6
+ end
7
+
8
+ def call(env)
9
+ r = @file_server.call env
10
+ if r[0] == 404
11
+ @app.call env
12
+ else
13
+ r
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ module Riemann; end
2
+ class Riemann::Dash
3
+ VERSION = '0.0.3'
4
+ end
@@ -0,0 +1,39 @@
1
+ html,table {
2
+ font-family: Helvetica Nueue, Helvetica, sans;
3
+ font-size: 8pt;
4
+ }
5
+ h1 {
6
+ margin-bottom: 0.2em;
7
+ }
8
+ h2 {
9
+ margin-top: 0;
10
+ margin-bottom: 0.1em;
11
+ }
12
+
13
+ .box {
14
+ float: left;
15
+ margin: 4px;
16
+ }
17
+
18
+ .ok {
19
+ background: #B8F1BC;
20
+ }
21
+ .warning {
22
+ background: #F7D18E;
23
+ }
24
+ .critical {
25
+ background: #FF3C43;
26
+ }
27
+
28
+ .chart {
29
+ width: 140px;
30
+ border: 1px solid #ccc;
31
+ }
32
+ .chart td {
33
+ min-width: 40px;
34
+ overflow: hidden;
35
+ }
36
+ .chart th {
37
+ width: 1px;
38
+ text-align: left;
39
+ }
@@ -0,0 +1,8 @@
1
+ <h2>Problems</h2>
2
+ <div class="box"><%= state_list query('state != "ok"') %></div>
3
+
4
+ <div class="box">
5
+ <%= state_chart query('service = "cpu" or service = "memory" or service =~ "disk%" or service = "load"'), title: "Health" %>
6
+ </div>
7
+
8
+ <div class="box"><%= state_chart query('true'), title: "Everything" %></div>
@@ -0,0 +1,21 @@
1
+ <html>
2
+ <head>
3
+ <title>Dashboard</title>
4
+ <link rel="stylesheet" type="text/css" href="/css" />
5
+
6
+ <script type="text/javascript">
7
+ setTimeout(function() {
8
+ location.reload(true);
9
+ }, 10000);
10
+ </script>
11
+ </head>
12
+
13
+ <body onload="refresh();">
14
+ <div>
15
+ <h1>Dashboard</h1>
16
+ <span style="position: absolute; top: 4px; right: 4px;">(as of <%= time Time.now.to_i %>)</span>
17
+ </div>
18
+
19
+ <%= yield %>
20
+ </body>
21
+ </html>
@@ -0,0 +1,126 @@
1
+ require 'riemann/client'
2
+ require 'sinatra/base'
3
+
4
+ module Riemann
5
+ class Dash < Sinatra::Base
6
+ # A little dashboard sinatra application.
7
+
8
+ require 'yaml'
9
+ require 'find'
10
+ require 'erubis'
11
+ require 'sass'
12
+
13
+ def self.config
14
+ @config ||= {
15
+ client: {},
16
+ age_scale: 60 * 30,
17
+ state_order: {
18
+ 'critical' => 3,
19
+ 'warning' => 2,
20
+ 'ok' => 1
21
+ },
22
+ strftime: '%H:%M:%S',
23
+ controllers: [File.join(File.dirname(__FILE__), 'dash', 'controller')],
24
+ helpers: [File.join(File.dirname(__FILE__), 'dash', 'helper')],
25
+ views: File.join(File.dirname(__FILE__), 'dash', 'views')
26
+ }
27
+ end
28
+
29
+ def self.client
30
+ @client ||= Riemann::Client.new(config[:client])
31
+ end
32
+
33
+ def self.load(filename)
34
+ unless load_config(filename || 'config.rb')
35
+ # Configuration failed; load a default view.
36
+ puts "No configuration loaded; using defaults."
37
+ end
38
+
39
+ config[:controllers].each { |d| load_controllers d }
40
+ config[:helpers].each { |d| load_helpers d }
41
+ set :views, File.expand_path(config[:views])
42
+ end
43
+
44
+ # Executes the configuration file.
45
+ def self.load_config(filename)
46
+ begin
47
+ instance_eval File.read(filename)
48
+ true
49
+ rescue Errno::ENOENT
50
+ false
51
+ end
52
+ end
53
+
54
+ # Load controllers.
55
+ # Controllers can be regular old one-file-per-class, but if you prefer a little
56
+ # more modularity, this method will allow you to define all controller methods
57
+ # in their own files. For example, get "/posts/*/edit" can live in
58
+ # controller/posts/_/edit.rb. The sorting system provided here requires
59
+ # files in the correct order to handle wildcards appropriately.
60
+ def self.load_controllers(dir)
61
+ rbs = []
62
+ Find.find(
63
+ File.expand_path(dir)
64
+ ) do |path|
65
+ rbs << path if path =~ /\.rb$/
66
+ end
67
+
68
+ # Sort paths with _ last, becase those are wildcards.
69
+ rbs.sort! do |a, b|
70
+ as = a.split File::SEPARATOR
71
+ bs = b.split File::SEPARATOR
72
+
73
+ # Compare common subpaths
74
+ l = [as.size, bs.size].min
75
+ catch :x do
76
+ (0...l).each do |i|
77
+ a, b = as[i], bs[i]
78
+ if a[/^_/] and not b[/^_/]
79
+ throw :x, 1
80
+ elsif b[/^_/] and not a[/^_/]
81
+ throw :x, -1
82
+ elsif ord = (a <=> b) and ord != 0
83
+ throw :x, ord
84
+ end
85
+ end
86
+
87
+ # All subpaths are identical; sort longest first
88
+ if as.size > bs.size
89
+ throw :x, -1
90
+ elsif as.size < bs.size
91
+ throw :x, -1
92
+ else
93
+ throw :x, 0
94
+ end
95
+ end
96
+ end
97
+
98
+ rbs.each do |r|
99
+ require r
100
+ end
101
+ end
102
+
103
+ # Load helpers
104
+ def self.load_helpers(dir)
105
+ Find.find(
106
+ File.expand_path(dir)
107
+ ) do |path|
108
+ require path if path =~ /\.rb$/
109
+ end
110
+ end
111
+
112
+ # Add an additional public directory.
113
+ def self.public_dir(dir)
114
+ require 'riemann/dash/rack/static'
115
+ use Riemann::Dash::Static, :root => dir
116
+ end
117
+
118
+ def client
119
+ self.class.client
120
+ end
121
+
122
+ def query(*a)
123
+ self.class.client.query(*a).events || []
124
+ end
125
+ end
126
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: riemann-dash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kyle Kingsbury
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: riemann-client
16
+ requirement: &9900960 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.0.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *9900960
25
+ - !ruby/object:Gem::Dependency
26
+ name: erubis
27
+ requirement: &9900240 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 2.7.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *9900240
36
+ - !ruby/object:Gem::Dependency
37
+ name: sinatra
38
+ requirement: &9899640 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 1.3.2
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *9899640
47
+ - !ruby/object:Gem::Dependency
48
+ name: sass
49
+ requirement: &9898820 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 3.1.14
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *9898820
58
+ - !ruby/object:Gem::Dependency
59
+ name: thin
60
+ requirement: &9898320 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: 1.3.1
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *9898320
69
+ description:
70
+ email: aphyr@aphyr.com
71
+ executables:
72
+ - riemann-dash
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/riemann/dash.rb
77
+ - lib/riemann/dash/version.rb
78
+ - lib/riemann/dash/rack/static.rb
79
+ - lib/riemann/dash/views/index.erubis
80
+ - lib/riemann/dash/views/layout.erubis
81
+ - lib/riemann/dash/views/css.scss
82
+ - lib/riemann/dash/controller/index.rb
83
+ - lib/riemann/dash/controller/css.rb
84
+ - lib/riemann/dash/helper/renderer.rb
85
+ - bin/riemann-dash
86
+ - LICENSE
87
+ - README.markdown
88
+ homepage: https://github.com/aphyr/riemann-dash
89
+ licenses: []
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: 1.9.1
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project: riemann-dash
108
+ rubygems_version: 1.8.10
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: HTTP dashboard for the distributed event system Riemann.
112
+ test_files: []