riemann-dash 0.0.3

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/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: []