perus 0.1.11 → 0.1.12

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 599b70600ece749fcd4c4f45ac930f4b795cff09
4
- data.tar.gz: ace224657f580232d911a0965426af0696fb5225
3
+ metadata.gz: 820c3e7205d8bb9dcceb2fd9d52ceeb76db9e39d
4
+ data.tar.gz: 2ae3ddcda6b97e2c21c01d1fadef7dda294a7c62
5
5
  SHA512:
6
- metadata.gz: 22e9fb25b86654bdc57454173ba6b29cd444bd15c40785551a535feb54178ba394759e418ae39a4758e95e9c0d25b3dc2e5535d33e34996606c5c6ab2c2e6ef9
7
- data.tar.gz: 88c08f91f40ac189bfc48932f60bf4e6489ec8a7147da0ae00eee8ff10f7d36fe2afe38baef5a9595aff25430b482813f70302e20c2cd6b4620167742ac27ba5
6
+ metadata.gz: 968cf198e14eec35f27faf8b8f6e6a090524c1e6b5b9540197090449d570464dbcf2b113d4165fb3da4d895a37bd4439959b3a61a934c05dcbbcf44aa55ce961
7
+ data.tar.gz: 31080180df8b4553f3b6cf2cc5073b830fdea47a95874cd10a784fbd35214297c8683bf571f150e2304ecc426987e67945c74529233d87d6e67fb0c0df07d517
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ require 'perus'
3
+ require 'optparse'
4
+ require 'irb'
5
+
6
+ options_path = Perus::Server::DEFAULT_SERVER_OPTIONS_PATH
7
+
8
+ OptionParser.new do |opts|
9
+ opts.banner = "Usage: perus-console [options]"
10
+
11
+ opts.on('-c', '--config PATH', "Path to config file (default: #{Perus::Server::DEFAULT_SERVER_OPTIONS_PATH})") do |c|
12
+ options_path = c
13
+ end
14
+
15
+ opts.on('-h', '--help', 'Prints this help') do
16
+ puts opts
17
+ exit
18
+ end
19
+ end.parse!
20
+
21
+ include Perus::Server
22
+ Server.load_options(options_path)
23
+ DB.start
24
+ IRB.start
@@ -16,6 +16,7 @@ module Perus
16
16
  require './server/admin'
17
17
  require './server/app'
18
18
  require './server/db'
19
+ require './server/stats'
19
20
  require './server/server'
20
21
  end
21
22
  end
@@ -102,6 +102,11 @@ module Perus::Server
102
102
  redirect "#{url_prefix}admin/configs/#{params['config_id']}"
103
103
  end
104
104
 
105
+ get '/admin/stats' do
106
+ @stats = Stats.new
107
+ erb :stats
108
+ end
109
+
105
110
 
106
111
  #----------------------
107
112
  # API
@@ -142,30 +147,33 @@ module Perus::Server
142
147
 
143
148
  # receive data from a client
144
149
  post '/systems/:id/ping' do
145
- timestamp = Time.now.to_i
150
+ Server.ping_queue.post do
151
+ timestamp = Time.now.to_i
146
152
 
147
- # update the system with its last known ip and update time
148
- system = System.with_pk!(params['id'])
149
- system.last_updated = timestamp
153
+ # update the system with its last known ip and update time
154
+ system = System.with_pk!(params['id'])
155
+ system.last_updated = timestamp
150
156
 
151
- if request.ip == '127.0.0.1' && ENV['RACK_ENV'] == 'production'
152
- system.ip = request.env['HTTP_X_FORWARDED_FOR']
153
- else
154
- system.ip = request.ip
155
- end
157
+ if request.ip == '127.0.0.1' && ENV['RACK_ENV'] == 'production'
158
+ system.ip = request.env['HTTP_X_FORWARDED_FOR']
159
+ else
160
+ system.ip = request.ip
161
+ end
162
+
163
+ # errors is either nil or a hash of the format - module: [err, ...]
164
+ system.save_metric_errors(params, timestamp)
156
165
 
157
- # errors is either nil or a hash of the format - module: [err, ...]
158
- system.save_metric_errors(params, timestamp)
166
+ # add each new value, a later process cleans up old values
167
+ system.save_values(params, timestamp)
159
168
 
160
- # add each new value, a later process cleans up old values
161
- system.save_values(params, timestamp)
169
+ # save action return values and prevent them from running again
170
+ system.save_actions(params, timestamp)
162
171
 
163
- # save action return values and prevent them from running again
164
- system.save_actions(params, timestamp)
172
+ # ip, last updated, uploads and metrics are now updated. these are
173
+ # stored on the system.
174
+ system.save
175
+ end
165
176
 
166
- # ip, last updated, uploads and metrics are now updated. these are
167
- # stored on the system.
168
- system.save
169
177
  content_type :json
170
178
  {success: true}.to_json
171
179
  end
@@ -220,6 +228,19 @@ module Perus::Server
220
228
  redirect "#{url_prefix}groups/#{params['id']}/systems"
221
229
  end
222
230
 
231
+ # delete completed actions in a group
232
+ delete '/groups/:id/systems/actions' do
233
+ group = Group.with_pk!(params['id'])
234
+ group.systems.each do |system|
235
+ system.actions.each do |action|
236
+ next if action.timestamp.nil?
237
+ action.destroy
238
+ end
239
+ end
240
+
241
+ redirect "#{url_prefix}groups/#{params['id']}/systems"
242
+ end
243
+
223
244
  # create an action for all systems
224
245
  post '/systems/actions' do
225
246
  System.each do |system|
@@ -229,6 +250,16 @@ module Perus::Server
229
250
  redirect "#{url_prefix}systems"
230
251
  end
231
252
 
253
+ # delete all completed actions
254
+ delete '/systems/actions' do
255
+ Action.each do |action|
256
+ next if action.timestamp.nil?
257
+ action.destroy
258
+ end
259
+
260
+ redirect "#{url_prefix}systems"
261
+ end
262
+
232
263
  # delete an action. deletion also clears any uploaded files.
233
264
  delete '/systems/:system_id/actions/:id' do
234
265
  action = Action.with_pk!(params['id'])
@@ -4,6 +4,8 @@ require 'concurrent'
4
4
 
5
5
  module Perus::Server
6
6
  module DB
7
+ MAX_VACUUM_ATTEMPTS = 5
8
+
7
9
  def self.db
8
10
  @db
9
11
  end
@@ -42,7 +44,31 @@ module Perus::Server
42
44
  # restructures the db so system records in the values index should
43
45
  # be sequentially stored
44
46
  vacuum_task = Concurrent::TimerTask.new do
45
- @db.execute('vacuum')
47
+ attempts = 0
48
+ complete = false
49
+
50
+ while !complete && attempts < MAX_VACUUM_ATTEMPTS
51
+ begin
52
+ puts "Vacuuming, attempt #{attempts + 1}"
53
+ start = Time.now
54
+ @db.execute('vacuum')
55
+ Stats.vacuumed!(Time.now - start)
56
+ complete = true
57
+ puts "Vacuuming complete"
58
+
59
+ rescue
60
+ attempts += 1
61
+ if attempts < MAX_VACUUM_ATTEMPTS
62
+ puts "Vacuum failed, will reattempt after short sleep"
63
+ sleep(5)
64
+ end
65
+ end
66
+ end
67
+
68
+ if !complete
69
+ puts "Vacuum failed more than MAX_VACUUM_ATTEMPTS"
70
+ Stats.vacuumed!('failed')
71
+ end
46
72
  end
47
73
 
48
74
  # fire every 12 hours
@@ -54,7 +80,13 @@ module Perus::Server
54
80
  # are removed from a system, the accompanying metric record is also
55
81
  # removed.
56
82
  cleanup_task = Concurrent::TimerTask.new do
57
- Perus::Server::DB.cleanup
83
+ begin
84
+ start = Time.now
85
+ Perus::Server::DB.cleanup
86
+ Stats.cleaned!(Time.now - start)
87
+ rescue
88
+ Stats.cleaned!('failed')
89
+ end
58
90
  end
59
91
 
60
92
  # fire every hour
@@ -66,10 +98,16 @@ module Perus::Server
66
98
  # cached so lookups can be done against the db, rather than running
67
99
  # each alert for each system on a page load.
68
100
  cache_alerts_task = Concurrent::TimerTask.new do
69
- Perus::Server::Alert.cache_active_alerts
101
+ begin
102
+ start = Time.now
103
+ Perus::Server::Alert.cache_active_alerts
104
+ Stats.alerts_cached!(Time.now - start)
105
+ rescue
106
+ Stats.alerts_cached!('failed')
107
+ end
70
108
  end
71
109
 
72
- cache_alerts_task.execution_interval = Server.options.cache_alerts_mins * 60
110
+ cache_alerts_task.execution_interval = Server.options.cache_alerts_secs
73
111
  cache_alerts_task.execute
74
112
  end
75
113
 
@@ -292,25 +292,25 @@ main {
292
292
  width: 450px;
293
293
  }
294
294
 
295
- #meta dl {
295
+ dl {
296
296
  display: inline-block;
297
297
  vertical-align: top;
298
298
  width: 450px;
299
299
  }
300
300
 
301
- #meta dt, #meta dd {
301
+ dt, dd {
302
302
  display: inline-block;
303
303
  vertical-align: top;
304
304
  margin-bottom: 10px;
305
305
  font-size: 13px;
306
306
  }
307
307
 
308
- #meta dt {
308
+ dt {
309
309
  width: 100px;
310
310
  font-weight: 600;
311
311
  }
312
312
 
313
- #meta dd {
313
+ dd {
314
314
  width: 340px;
315
315
  line-height: 16px;
316
316
  }
@@ -507,3 +507,22 @@ article.graph {
507
507
  padding: 15px 20px;
508
508
  border-radius: 4px;
509
509
  }
510
+
511
+ #actions-menu {
512
+ }
513
+
514
+ #actions-menu p {
515
+ display: inline-block;
516
+ float: left;
517
+ }
518
+
519
+ #actions-menu form {
520
+ display: inline-block;
521
+ float: right;
522
+ margin-top: 30px;
523
+ }
524
+
525
+ h2.stats {
526
+ margin-bottom: 10px;
527
+ margin-top: 20px;
528
+ }
@@ -1,3 +1,4 @@
1
+ require 'concurrent'
1
2
  require 'thin'
2
3
 
3
4
  DEFAULT_SERVER_OPTIONS = {
@@ -10,7 +11,8 @@ DEFAULT_SERVER_OPTIONS = {
10
11
  'site_name' => 'Perus',
11
12
  'url_prefix' => '/',
12
13
  'keep_hours' => 24,
13
- 'cache_alerts_mins' => 1
14
+ 'cache_alerts_secs' => 20,
15
+ 'stats_path' => '/var/perus.stats'
14
16
  }
15
17
  }
16
18
 
@@ -19,6 +21,8 @@ module Perus::Server
19
21
  def initialize(options_path = DEFAULT_SERVER_OPTIONS_PATH, environment = 'development')
20
22
  self.class.load_options(options_path)
21
23
  ENV['RACK_ENV'] = environment
24
+
25
+ # initialise/migrate the db and start cleanup/caching timers
22
26
  DB.start
23
27
  DB.start_timers
24
28
  end
@@ -31,6 +35,19 @@ module Perus::Server
31
35
  )
32
36
  end
33
37
 
38
+ def self.ping_queue
39
+ # ping data is processed in a thread pool
40
+ @ping_queue ||= Concurrent::ThreadPoolExecutor.new(
41
+ min_threads: 2,
42
+ max_threads: 2,
43
+ max_queue: 0
44
+ )
45
+ end
46
+
47
+ def self.shutdown
48
+ @ping_queue.shutdown
49
+ end
50
+
34
51
  def self.options
35
52
  @options ||= Perus::Options.new
36
53
  end
@@ -0,0 +1,126 @@
1
+ require 'json'
2
+
3
+ module Perus::Server
4
+ class Stats
5
+ MAX_HISTORY = 10
6
+ attr_reader :data
7
+
8
+ # data format (json):
9
+ # {
10
+ # "vacuums": [
11
+ # ["ISO time", duration (int) || 'failed'],
12
+ # ...
13
+ # ]
14
+ # }
15
+
16
+ def initialize
17
+ if File.exists?(Server.options.stats_path)
18
+ data = IO.read(Server.options.stats_path)
19
+ unless data.empty?
20
+ @data = JSON.parse(data)
21
+ return
22
+ end
23
+ end
24
+
25
+ @data = {
26
+ 'vacuums' => [],
27
+ 'alerts_caches' => [],
28
+ 'cleans' => []
29
+ }
30
+ end
31
+
32
+ def save
33
+ IO.write(Server.options.stats_path, JSON.dump(@data))
34
+ end
35
+
36
+ # vacuuming
37
+ def last_vacuum
38
+ entry = @data['vacuums'].last
39
+ entry ? entry.first : nil
40
+ end
41
+
42
+ def vacuum_time
43
+ entry = @data['vacuums'].last
44
+ entry ? entry.last : nil
45
+ end
46
+
47
+ def average_vacuum_time
48
+ entries = @data['vacuums']
49
+ return nil if entries.empty?
50
+ ints = entries.map(&:last).select {|time| time.is_a?(Numeric)}
51
+ ints.reduce(:+).to_f / ints.length
52
+ end
53
+
54
+ def self.vacuumed!(time)
55
+ stats = Stats.new
56
+ list = stats.data['vacuums']
57
+ list << [Time.now.to_s, time]
58
+
59
+ if list.length > MAX_HISTORY
60
+ list.drop(list.length - MAX_HISTORY)
61
+ end
62
+
63
+ stats.save
64
+ end
65
+
66
+ # alerts caching
67
+ def last_alerts_cache
68
+ entry = @data['alerts_caches'].last
69
+ entry ? entry.first : nil
70
+ end
71
+
72
+ def alerts_cache_time
73
+ entry = @data['alerts_caches'].last
74
+ entry ? entry.last : nil
75
+ end
76
+
77
+ def average_alerts_cache_time
78
+ entries = @data['alerts_caches']
79
+ return nil if entries.empty?
80
+ ints = entries.map(&:last).select {|time| time.is_a?(Numeric)}
81
+ ints.reduce(:+).to_f / ints.length
82
+ end
83
+
84
+ def self.alerts_cached!(time)
85
+ stats = Stats.new
86
+ list = stats.data['alerts_caches']
87
+ list << [Time.now.to_s, time]
88
+
89
+ if list.length > MAX_HISTORY
90
+ list.drop(list.length - MAX_HISTORY)
91
+ end
92
+
93
+ stats.save
94
+ end
95
+
96
+ # cleaning
97
+ def last_clean
98
+ entry = @data['cleans'].last
99
+ entry ? entry.first : nil
100
+ end
101
+
102
+ def clean_time
103
+ entry = @data['cleans'].last
104
+ entry ? entry.last : nil
105
+ end
106
+
107
+ def average_clean_time
108
+ entries = @data['cleans']
109
+ return nil if entries.empty?
110
+ ints = entries.map(&:last).select {|time| time.is_a?(Numeric)}
111
+ ints.reduce(:+).to_f / ints.length
112
+ end
113
+
114
+ def self.cleaned!(time)
115
+ stats = Stats.new
116
+ list = stats.data['cleans']
117
+ list << [Time.now.to_s, time]
118
+
119
+ if list.length > MAX_HISTORY
120
+ list.drop(list.length - MAX_HISTORY)
121
+ end
122
+
123
+ stats.save
124
+ end
125
+ end
126
+ end
@@ -31,6 +31,7 @@
31
31
  <%= nav_item("#{url_prefix}admin/configs", 'Configs') %>
32
32
  <%= nav_item("#{url_prefix}admin/alerts", 'Alerts') %>
33
33
  <%= nav_item("#{url_prefix}admin/scripts", 'Scripts') %>
34
+ <%= nav_item("#{url_prefix}admin/stats", 'Stats') %>
34
35
  </ul>
35
36
  <% end %>
36
37
  </li>
@@ -0,0 +1,31 @@
1
+ <h1>Background Stats</h1>
2
+
3
+ <h2 class="stats">Vacuuming</h2>
4
+ <dl>
5
+ <dt>Last Vacuum</dt>
6
+ <dd><%= @stats.last_vacuum || 'nil' %></dd>
7
+ <dt>Vacuum Time</dt>
8
+ <dd><%= "%0.4f" % @stats.vacuum_time rescue 'nil' %></dd>
9
+ <dt>Average Vacuum Time</dt>
10
+ <dd><%= "%0.4f" % @stats.average_vacuum_time rescue 'nil' %></dd>
11
+ </dl>
12
+
13
+ <h2 class="stats">Alerts Caching</h2>
14
+ <dl>
15
+ <dt>Last Alerts Cache</dt>
16
+ <dd><%= @stats.last_alerts_cache || 'nil' %></dd>
17
+ <dt>Alerts Cache Time</dt>
18
+ <dd><%= "%0.4f" % @stats.alerts_cache_time rescue 'nil' %></dd>
19
+ <dt>Average Alerts Cache Time</dt>
20
+ <dd><%= "%0.4f" % @stats.average_alerts_cache_time rescue 'nil' %></dd>
21
+ </dl>
22
+
23
+ <h2 class="stats">Cleaning</h2>
24
+ <dl>
25
+ <dt>Last Clean</dt>
26
+ <dd><%= @stats.last_clean || 'nil' %></dd>
27
+ <dt>Clean Time</dt>
28
+ <dd><%= "%0.4f" % @stats.clean_time rescue 'nil' %></dd>
29
+ <dt>Average Clean Time</dt>
30
+ <dd><%= "%0.4f" % @stats.average_clean_time rescue 'nil' %></dd>
31
+ </dl>
@@ -17,22 +17,29 @@
17
17
  <% end %>
18
18
  <p id="no-systems" <% unless @systems.empty? %>style="display: none"<% end %>>No Systems</p>
19
19
 
20
- <p id="add-command">
21
- Add action to <%= @title %>:
22
- <select id="command-select">
23
- <option></option>
24
- <optgroup label="Commands">
25
- <% command_actions.each do |command| %>
26
- <option value="<%= command.name %>"><%= command.human_name %></option>
27
- <% end %>
28
- </optgroup>
29
- <optgroup label="Scripts">
30
- <% @scripts.each do |script| %>
31
- <option value="<%= script.code_name %>"><%= script.name %></option>
32
- <% end %>
33
- </optgroup>
34
- </select>
35
- </p>
20
+
21
+ <menu id="actions-menu">
22
+ <p id="add-command">
23
+ Add Action to <%= @title %>:
24
+ <select id="command-select">
25
+ <option></option>
26
+ <optgroup label="Commands">
27
+ <% command_actions.each do |command| %>
28
+ <option value="<%= command.name %>"><%= command.human_name %></option>
29
+ <% end %>
30
+ </optgroup>
31
+ <optgroup label="Scripts">
32
+ <% @scripts.each do |script| %>
33
+ <option value="<%= script.code_name %>"><%= script.name %></option>
34
+ <% end %>
35
+ </optgroup>
36
+ </select>
37
+ </p>
38
+ <form id="delete-completed" method="POST" action="<%= url_prefix %><%= @action_url %>">
39
+ <input type="hidden" name="_method" value="DELETE">
40
+ <input type="submit" value="Delete Completed Actions">
41
+ </form>
42
+ </menu>
36
43
 
37
44
  <!-- new commands -->
38
45
  <% command_actions.each do |command| %>
@@ -48,6 +55,11 @@
48
55
  <% end %>
49
56
 
50
57
  <script>
58
+ $('#delete-completed').submit(function(event) {
59
+ if (!confirm('Are you sure you want to delete all completed actions? Their results will no longer be available.'))
60
+ event.preventDefault();
61
+ });
62
+
51
63
  $('#command-select').change(function() {
52
64
  $('form.command').hide();
53
65
  $('form[data-command="' + $(this).val() + '"]').show();
@@ -1,3 +1,3 @@
1
1
  module Perus
2
- VERSION = "0.1.11"
2
+ VERSION = "0.1.12"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Will Cannings
@@ -196,6 +196,7 @@ description:
196
196
  email:
197
197
  - me@willcannings.com
198
198
  executables:
199
+ - perus-console
199
200
  - perus-pinger
200
201
  - perus-server
201
202
  extensions: []
@@ -210,6 +211,7 @@ files:
210
211
  - Rakefile
211
212
  - bin/console
212
213
  - bin/setup
214
+ - exe/perus-console
213
215
  - exe/perus-pinger
214
216
  - exe/perus-server
215
217
  - lib/perus.rb
@@ -304,6 +306,7 @@ files:
304
306
  - lib/perus/server/public/js/dygraph-combined.js
305
307
  - lib/perus/server/public/js/jquery.js
306
308
  - lib/perus/server/server.rb
309
+ - lib/perus/server/stats.rb
307
310
  - lib/perus/server/views/admin/edit.erb
308
311
  - lib/perus/server/views/admin/index.erb
309
312
  - lib/perus/server/views/admin/new.erb
@@ -317,6 +320,7 @@ files:
317
320
  - lib/perus/server/views/layout.erb
318
321
  - lib/perus/server/views/scripts/edit.erb
319
322
  - lib/perus/server/views/scripts/form.erb
323
+ - lib/perus/server/views/stats.erb
320
324
  - lib/perus/server/views/system.erb
321
325
  - lib/perus/server/views/systems.erb
322
326
  - lib/perus/server/views/systems/form.erb
@@ -347,3 +351,4 @@ signing_key:
347
351
  specification_version: 4
348
352
  summary: Simple system overview server
349
353
  test_files: []
354
+ has_rdoc: