litmus_paper 0.8.6 → 0.8.7

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -1,6 +1,8 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.8.7
4
3
  - 1.9.2
5
4
  - 1.9.3
6
5
  - 2.0.0
6
+ notifications:
7
+ email:
8
+ - blue@getbraintree.com
@@ -22,6 +22,14 @@ module LitmusPaper
22
22
  _create_status_file(StatusFile.global_up_file)
23
23
  end
24
24
 
25
+ delete "/health" do
26
+ _delete_status_file(StatusFile.global_health_file)
27
+ end
28
+
29
+ post "/health" do
30
+ _create_status_file(StatusFile.global_health_file)
31
+ end
32
+
25
33
  get "/:service/status" do
26
34
  health = LitmusPaper.check_service(params[:service])
27
35
  if health.nil?
@@ -38,8 +46,14 @@ module LitmusPaper
38
46
  headers = {"X-Health" => health.value.to_s}
39
47
  body = "Health: #{health.value}\n"
40
48
  if health.forced?
49
+ if health.direction == :health
50
+ status = "health"
51
+ reason = health.forced_reason.split("\n").join(" ")
52
+ else
53
+ reason = health.forced_reason
54
+ end
41
55
  body << "Measured Health: #{health.measured_health}\n"
42
- body << "Forced Reason: #{health.forced_reason}\n"
56
+ body << "Forced Reason: #{reason}\n"
43
57
  end
44
58
  body << health.summary
45
59
 
@@ -59,6 +73,14 @@ module LitmusPaper
59
73
  _create_status_file(StatusFile.service_down_file(params[:service]))
60
74
  end
61
75
 
76
+ delete "/:service/health" do
77
+ _delete_status_file(StatusFile.service_health_file(params[:service]))
78
+ end
79
+
80
+ post "/:service/health" do
81
+ _create_status_file(StatusFile.service_health_file(params[:service]))
82
+ end
83
+
62
84
  delete "/:service/up" do
63
85
  _delete_status_file(StatusFile.service_up_file(params[:service]))
64
86
  end
@@ -76,7 +98,7 @@ module LitmusPaper
76
98
  end
77
99
 
78
100
  def _create_status_file(status_file)
79
- status_file.create(params[:reason])
101
+ status_file.create(params[:reason], params[:health])
80
102
  _text 201, "File created"
81
103
  end
82
104
 
@@ -9,17 +9,21 @@ module LitmusPaper
9
9
  def self.build_request(options, args)
10
10
  options.merge! _default_options
11
11
  opt_parser = _extend_default_parser(options) do |opts|
12
- opts.banner = "Usage: litmusctl force <up|down> [service] [options]"
12
+ opts.banner = "Usage: litmusctl force <up|down|health N> [service] [options]"
13
13
  opts.on("-d", "--delete", "Remove status file") do
14
14
  options[:delete] = true
15
15
  end
16
16
  opts.on("-r", "--reason=reason", String, "Reason for status file") do |reason|
17
- options[:reason] = reason
17
+ options[:reason] = reason.gsub("\n", " ")
18
18
  end
19
19
  end
20
20
 
21
21
  opt_parser.parse! args
22
- direction, service = args
22
+ if args[0] == "health" && !options[:delete]
23
+ direction, value, service = args
24
+ else
25
+ direction, service = args
26
+ end
23
27
  path = service ? "/#{service}/#{direction}" : "/#{direction}"
24
28
 
25
29
  if options[:delete]
@@ -27,10 +31,12 @@ module LitmusPaper
27
31
  else
28
32
  if !options.has_key?(:reason)
29
33
  print "Reason? "
30
- options[:reason] = STDIN.gets.chomp
34
+ options[:reason] = STDIN.gets.chomp.gsub("\n", " ")
31
35
  end
32
36
  request = Net::HTTP::Post.new(path)
33
- request.set_form_data('reason' => options[:reason])
37
+ params = {'reason' => options[:reason]}
38
+ params.merge!({'health' => value}) if direction == 'health'
39
+ request.set_form_data(params)
34
40
  end
35
41
 
36
42
  request
@@ -19,9 +19,25 @@ module LitmusPaper
19
19
  @forced != :none
20
20
  end
21
21
 
22
+ def direction
23
+ @forced
24
+ end
25
+
22
26
  def value
23
27
  if forced?
24
- return @forced == :up ? 100 : 0
28
+ return case @forced
29
+ when :up
30
+ 100
31
+ when :down
32
+ 0
33
+ when :health
34
+ forced_health = @forced_reason.split("\n").last.to_i
35
+
36
+ # This could potentially be argued differently, but I feel like forcing
37
+ # a health value != forcing up - if the measured health is less than the
38
+ # forced health, we should return the measured health.
39
+ measured_health < forced_health ? measured_health : forced_health
40
+ end
25
41
  end
26
42
 
27
43
  measured_health
@@ -0,0 +1,62 @@
1
+ module LitmusPaper
2
+ module Metric
3
+ class Script
4
+ attr_reader :script_pid
5
+
6
+ def initialize(command, weight, options = {})
7
+ @command = command
8
+ @weight = weight
9
+ @timeout = options.fetch(:timeout, 5)
10
+ end
11
+
12
+ def current_health
13
+ @weight * result
14
+ end
15
+
16
+ def result
17
+ value = 0
18
+ Timeout.timeout(@timeout) do
19
+ script_stdout = script_stderr = nil
20
+ script_status = POpen4.popen4(@command) do |stdout, stderr, stdin, pid|
21
+ @script_pid = pid
22
+ value = script_stdout = stdout.read.strip
23
+ script_stderr = stderr.read.strip
24
+ end
25
+ unless script_status.success?
26
+ LitmusPaper.logger.info("Available check to #{@command} failed with status #{$CHILD_STATUS.exitstatus}")
27
+ LitmusPaper.logger.info("Failed stdout: #{script_stdout}")
28
+ LitmusPaper.logger.info("Failed stderr: #{script_stderr}")
29
+ end
30
+
31
+ value.to_f
32
+ end
33
+ rescue Timeout::Error
34
+ LitmusPaper.logger.info("Available check to '#{@command}' timed out")
35
+ kill_and_reap_script(@script_pid)
36
+ 0
37
+ end
38
+
39
+ def kill_and_reap_script(pid)
40
+ Process.kill(9, pid)
41
+ stop_time = Time.now + 2
42
+ while Time.now < stop_time
43
+ if Process.waitpid(pid, Process::WNOHANG)
44
+ LitmusPaper.logger.info("Reaped PID #{pid}")
45
+ return
46
+ else
47
+ sleep 0.1
48
+ end
49
+ end
50
+ LitmusPaper.logger.error("Unable to reap PID #{pid}")
51
+ rescue Errno::ESRCH
52
+ LitmusPaper.logger.info("Attempted to kill non-existent PID #{pid} (ESRCH)")
53
+ rescue Errno::ECHILD
54
+ LitmusPaper.logger.info("Attempted to reap PID #{pid} but it has already been reaped (ECHILD)")
55
+ end
56
+
57
+ def to_s
58
+ "Metric::Script(#{@command}, #{@weight})"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -10,6 +10,10 @@ module LitmusPaper
10
10
  new("global_up", :up)
11
11
  end
12
12
 
13
+ def self.global_health_file
14
+ new("global_health", :health)
15
+ end
16
+
13
17
  def self.service_down_file(service_name)
14
18
  new("#{service_name}_down", :down)
15
19
  end
@@ -18,12 +22,18 @@ module LitmusPaper
18
22
  new("#{service_name}_up", :up)
19
23
  end
20
24
 
25
+ def self.service_health_file(service_name)
26
+ new("#{service_name}_health", :health)
27
+ end
28
+
21
29
  def self.priority_check_order_for_service(service_name)
22
30
  [
23
31
  global_down_file,
24
32
  global_up_file,
33
+ global_health_file,
25
34
  service_down_file(service_name),
26
- service_up_file(service_name)
35
+ service_up_file(service_name),
36
+ service_health_file(service_name),
27
37
  ]
28
38
  end
29
39
 
@@ -36,10 +46,11 @@ module LitmusPaper
36
46
  File.read(@path).chomp
37
47
  end
38
48
 
39
- def create(reason)
49
+ def create(reason, health = nil)
40
50
  FileUtils.mkdir_p(File.dirname(@path))
41
51
  File.open(@path, 'w') do |file|
42
52
  file.puts(reason)
53
+ file.puts(health) if health
43
54
  end
44
55
  end
45
56
 
@@ -4,7 +4,7 @@ module LitmusPaper
4
4
  def self.service_status
5
5
  max_service_length = (LitmusPaper.services.keys << "Service").max { |a, b| a.length <=> b.length }.length
6
6
 
7
- output = "Litmus Paper #{LitmusPaper::VERSION}\n"
7
+ output = "Litmus Paper #{LitmusPaper::VERSION}\n\n"
8
8
  output += sprintf(" %s │ %s │ %s │ %s\n", "Service".ljust(max_service_length), "Reported", "Measured", "Health")
9
9
  output += sprintf(" %s │ %s │ %s │ %s\n", "Name".ljust(max_service_length), "Health".ljust(8), "Health".ljust(8), "Forced?")
10
10
  output += "─" * (max_service_length + 2) + "┴" + "─" * 10 + "┴" + "─" * 10 + "┴" + "─" * 9 + "\n"
@@ -14,11 +14,16 @@ module LitmusPaper
14
14
  measured_health = health.measured_health.to_s.rjust(3)
15
15
  reported_health = health.value.to_s.rjust(3)
16
16
  service_forced = if health.forced?
17
- "Yes: #{health.forced_reason}"
18
- else
19
- "No"
20
- end
21
- output += sprintf("* %-#{max_service_length}s %s %s %s\n",
17
+ message = "Yes,"
18
+ forced_reason, forced_health = health.forced_reason.split("\n")
19
+ if forced_health
20
+ message += " Health: #{forced_health}"
21
+ end
22
+ message += " Reason: #{forced_reason}"
23
+ else
24
+ "No"
25
+ end
26
+ output += sprintf("- %-#{max_service_length}s %s %s %s\n",
22
27
  service_name,
23
28
  reported_health.center(8).colorize(_health_color(health.value)),
24
29
  measured_health.center(8),
@@ -1,3 +1,3 @@
1
1
  module LitmusPaper
2
- VERSION = "0.8.6"
2
+ VERSION = "0.8.7"
3
3
  end
data/lib/litmus_paper.rb CHANGED
@@ -37,6 +37,7 @@ require 'litmus_paper/metric/available_memory'
37
37
  require 'litmus_paper/metric/big_brother_service'
38
38
  require 'litmus_paper/metric/constant_metric'
39
39
  require 'litmus_paper/metric/cpu_load'
40
+ require 'litmus_paper/metric/script'
40
41
  require 'litmus_paper/service'
41
42
  require 'litmus_paper/status_file'
42
43
  require 'litmus_paper/terminal_output'
@@ -52,6 +52,14 @@ describe LitmusPaper::App do
52
52
  last_response.body.should include('Down for testing')
53
53
  last_response.body.should include('Up for testing')
54
54
  end
55
+
56
+ it "includes the health value if health is forced" do
57
+ LitmusPaper.services['test'] = LitmusPaper::Service.new('test')
58
+ LitmusPaper::StatusFile.service_health_file("test").create("Forcing health", 88)
59
+ get "/"
60
+ last_response.body.should include('Forcing health')
61
+ last_response.body.should include('88')
62
+ end
55
63
  end
56
64
 
57
65
  describe "POST /up" do
@@ -82,6 +90,20 @@ describe LitmusPaper::App do
82
90
  end
83
91
  end
84
92
 
93
+ describe "POST /health" do
94
+ it "creates a global healthfile" do
95
+ test_service = LitmusPaper::Service.new('test', [AlwaysAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
96
+ LitmusPaper.services['test'] = test_service
97
+
98
+ post "/health", :reason => "health for testing", :health => 88
99
+ last_response.status.should == 201
100
+
101
+ get "/test/status"
102
+ last_response.status.should == 200
103
+ last_response.body.should match(/health for testing 88/)
104
+ end
105
+ end
106
+
85
107
  describe "POST /:service/up" do
86
108
  it "creates a service specific upfile" do
87
109
  test_service = LitmusPaper::Service.new('test', [NeverAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
@@ -96,6 +118,20 @@ describe LitmusPaper::App do
96
118
  end
97
119
  end
98
120
 
121
+ describe "POST /:service/health" do
122
+ it "creates a service specific healthfile" do
123
+ test_service = LitmusPaper::Service.new('test', [AlwaysAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
124
+ LitmusPaper.services['test'] = test_service
125
+
126
+ post "/test/health", :reason => "health for testing", :health => 88
127
+ last_response.status.should == 201
128
+
129
+ get "/test/status"
130
+ last_response.status.should == 200
131
+ last_response.body.should match(/health for testing 88/)
132
+ end
133
+ end
134
+
99
135
  describe "DELETE /up" do
100
136
  it "removes the global upfile" do
101
137
  test_service = LitmusPaper::Service.new('test', [NeverAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
@@ -144,6 +180,35 @@ describe LitmusPaper::App do
144
180
  end
145
181
  end
146
182
 
183
+ describe "DELETE /health" do
184
+ it "removes the global healthfile" do
185
+ test_service = LitmusPaper::Service.new('test', [AlwaysAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
186
+ LitmusPaper.services['test'] = test_service
187
+
188
+ post "/health", :reason => "health for testing", :health => 88
189
+ last_response.status.should == 201
190
+
191
+ get "/test/status"
192
+ last_response.status.should == 200
193
+ last_response.body.should match(/health for testing 88/)
194
+
195
+ delete "/health"
196
+ last_response.status.should == 200
197
+
198
+ get "/test/status"
199
+ last_response.should be_ok
200
+ last_response.body.should_not match(/health for testing 88/)
201
+ end
202
+
203
+ it "404s if there is no healthfile" do
204
+ test_service = LitmusPaper::Service.new('test', [NeverAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
205
+
206
+ delete "/health"
207
+
208
+ last_response.status.should == 404
209
+ end
210
+ end
211
+
147
212
  describe "DELETE /:service/up" do
148
213
  it "removes a service specific upfile" do
149
214
  test_service = LitmusPaper::Service.new('test', [NeverAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
@@ -164,7 +229,63 @@ describe LitmusPaper::App do
164
229
  end
165
230
  end
166
231
 
232
+ describe "DELETE /:service/health" do
233
+ it "removes the service specific healthfile" do
234
+ test_service = LitmusPaper::Service.new('test', [AlwaysAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
235
+ LitmusPaper.services['test'] = test_service
236
+
237
+ post "/test/health", :reason => "health for testing", :health => 88
238
+ last_response.status.should == 201
239
+
240
+ get "/test/status"
241
+ last_response.status.should == 200
242
+ last_response.body.should match(/health for testing 88/)
243
+
244
+ delete "/test/health"
245
+ last_response.status.should == 200
246
+
247
+ get "/test/status"
248
+ last_response.should be_ok
249
+ last_response.body.should_not match(/health for testing 88/)
250
+ end
251
+
252
+ it "404s if there is no healthfile" do
253
+ test_service = LitmusPaper::Service.new('test', [NeverAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
254
+
255
+ delete "/test/health"
256
+
257
+ last_response.status.should == 404
258
+ end
259
+ end
260
+
167
261
  describe "GET /:service/status" do
262
+ it "returns the forced health value for a healthy service" do
263
+ test_service = LitmusPaper::Service.new('test', [AlwaysAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
264
+ LitmusPaper.services['test'] = test_service
265
+ LitmusPaper::StatusFile.service_health_file("test").create("Forcing health", 88)
266
+
267
+ get "/test/status"
268
+ last_response.should be_ok
269
+ last_response.header["X-Health"].should == "88"
270
+ last_response.body.should match(/Health: 88/)
271
+ last_response.body.should match(/Measured Health: 100/)
272
+ last_response.header["X-Health-Forced"].should == "health"
273
+ end
274
+
275
+ it "returns the actualy health value for an unhealthy service when the measured health is less than the forced value" do
276
+ test_service = LitmusPaper::Service.new('test', [NeverAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
277
+ LitmusPaper.services['test'] = test_service
278
+ LitmusPaper::StatusFile.service_health_file("test").create("Forcing health", 88)
279
+
280
+ get "/test/status"
281
+ last_response.should_not be_ok
282
+ last_response.header["X-Health"].should == "0"
283
+ last_response.header["X-Health-Forced"].should == "health"
284
+ last_response.body.should match(/Health: 0/)
285
+ last_response.body.should match(/Measured Health: 0/)
286
+ last_response.body.should match(/Forcing health 88\n/)
287
+ end
288
+
168
289
  it "is successful when the service is passing" do
169
290
  test_service = LitmusPaper::Service.new('test', [AlwaysAvailableDependency.new], [LitmusPaper::Metric::ConstantMetric.new(100)])
170
291
  LitmusPaper.services['test'] = test_service
@@ -50,6 +50,14 @@ describe 'litmusctl' do
50
50
  status.should match(/for testing/)
51
51
  end
52
52
 
53
+ it "can create a global healthfile" do
54
+ _litmusctl('force health 88 -r "for testing"').should match("File created")
55
+
56
+ status = _litmusctl('status test')
57
+ status.should match(/Health: 0/) # This service is actually failing so we'll get 0 back
58
+ status.should match(/for testing 88/)
59
+ end
60
+
53
61
  it "creates a downfile" do
54
62
  _litmusctl('force down test -r "for testing"').should match("File created")
55
63
 
@@ -58,6 +66,14 @@ describe 'litmusctl' do
58
66
  status.should match(/for testing/)
59
67
  end
60
68
 
69
+ it "creates a healthfile" do
70
+ _litmusctl('force health 88 test -r "for testing"').should match("File created")
71
+
72
+ status = _litmusctl('status test')
73
+ status.should match(/Health: 0/)
74
+ status.should match(/for testing 88/)
75
+ end
76
+
61
77
  it 'removes an upfile for the service' do
62
78
  _litmusctl('force up test -r "for testing"').should match("File created")
63
79
  _litmusctl('force up test -d').should match("File deleted")
@@ -67,6 +83,14 @@ describe 'litmusctl' do
67
83
  status.should_not match(/for testing/)
68
84
  end
69
85
 
86
+ it "removes a healthfile for the service" do
87
+ _litmusctl('force health 88 test -r "for testing"').should match("File created")
88
+ _litmusctl('force health test -d').should match("File deleted")
89
+ status = _litmusctl('status passing_test')
90
+ status.should match(/Health: \d\d/)
91
+ status.should_not match(/for testing/)
92
+ end
93
+
70
94
  it "returns not found if downfile doesn't exist" do
71
95
  _litmusctl('force down test -d').should match("NOT FOUND")
72
96
  end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe LitmusPaper::Metric::Script do
4
+ describe "#result" do
5
+ it "is 1 when the script outputs 1" do
6
+ check = LitmusPaper::Metric::Script.new("echo 1", 1)
7
+ check.current_health.should == 1
8
+ end
9
+
10
+ it "is zero when the script exits 1" do
11
+ check = LitmusPaper::Metric::Script.new("false", 1)
12
+ check.current_health.should == 0
13
+ end
14
+
15
+ it "is zero when the script exceeds the timeout" do
16
+ check = LitmusPaper::Metric::Script.new("sleep 10", 1, :timeout => 1)
17
+ check.current_health.should == 0
18
+ end
19
+
20
+ it "kills the child process when script check exceeds timeout" do
21
+ check = LitmusPaper::Metric::Script.new("sleep 50", 1, :timeout => 1)
22
+ check.current_health.should == 0
23
+ expect { Process.kill(0, check.script_pid) }.to raise_error(Errno::ESRCH)
24
+ end
25
+
26
+ it "can handle pipes" do
27
+ check = LitmusPaper::Metric::Script.new("echo 'a' | tr a 1", 1)
28
+ check.current_health.should == 1
29
+
30
+ check = LitmusPaper::Metric::Script.new("echo 'a' | tr a 0", 0)
31
+ check.current_health.should == 0
32
+ end
33
+ end
34
+
35
+ describe "to_s" do
36
+ it "returns the command" do
37
+ check = LitmusPaper::Metric::Script.new("sleep 10", 1)
38
+ check.to_s.should == "Metric::Script(sleep 10, 1)"
39
+ end
40
+ end
41
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: litmus_paper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.6
4
+ version: 0.8.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-06-30 00:00:00.000000000 Z
12
+ date: 2015-07-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
@@ -213,6 +213,7 @@ files:
213
213
  - lib/litmus_paper/metric/big_brother_service.rb
214
214
  - lib/litmus_paper/metric/constant_metric.rb
215
215
  - lib/litmus_paper/metric/cpu_load.rb
216
+ - lib/litmus_paper/metric/script.rb
216
217
  - lib/litmus_paper/service.rb
217
218
  - lib/litmus_paper/status_file.rb
218
219
  - lib/litmus_paper/terminal_output.rb
@@ -231,6 +232,7 @@ files:
231
232
  - spec/litmus_paper/metric/big_brother_service_spec.rb
232
233
  - spec/litmus_paper/metric/constant_metric_spec.rb
233
234
  - spec/litmus_paper/metric/cpu_load_spec.rb
235
+ - spec/litmus_paper/metric/script_spec.rb
234
236
  - spec/litmus_paper/service_spec.rb
235
237
  - spec/litmus_paper/status_file_spec.rb
236
238
  - spec/litmus_paper_spec.rb
@@ -289,6 +291,7 @@ test_files:
289
291
  - spec/litmus_paper/metric/big_brother_service_spec.rb
290
292
  - spec/litmus_paper/metric/constant_metric_spec.rb
291
293
  - spec/litmus_paper/metric/cpu_load_spec.rb
294
+ - spec/litmus_paper/metric/script_spec.rb
292
295
  - spec/litmus_paper/service_spec.rb
293
296
  - spec/litmus_paper/status_file_spec.rb
294
297
  - spec/litmus_paper_spec.rb