elbping 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
data/lib/elbping/cli.rb CHANGED
@@ -6,13 +6,16 @@ require 'uri'
6
6
  require 'elbping/pinger.rb'
7
7
  require 'elbping/resolver.rb'
8
8
  require 'elbping/display.rb'
9
+ require 'elbping/stats.rb'
9
10
 
10
11
  module ElbPing
12
+ # Setup and initialization for running as a CLI app happens here. Specifically, on load it will
13
+ # read in environment variables and set up default values for the app.
11
14
  module CLI
12
15
 
13
16
  # Set up default options
14
17
  OPTIONS = {}
15
- OPTIONS[:verb_len] = ENV['PING_ELB_MAXVERBLEN'] || 128
18
+ OPTIONS[:verb_len] = ENV['PING_ELB_VERBLEN'] || 128
16
19
  OPTIONS[:nameserver] = ENV['PING_ELB_NS'] || 'ns-941.amazon.com'
17
20
  OPTIONS[:count] = ENV['PING_ELB_PINGCOUNT'] || 0
18
21
  OPTIONS[:timeout] = ENV['PING_ELB_TIMEOUT'] || 10
@@ -44,81 +47,91 @@ module ElbPing
44
47
  end
45
48
  end
46
49
 
47
- # Parse options
50
+ # Displays usage
48
51
  def self.usage
49
- puts PARSER.help
52
+ ElbPing::Display.out PARSER.help
50
53
  exit(false)
51
54
  end
52
55
 
53
- # Main entry point of the program
54
- def self.main
55
- PARSER.parse!(ARGV) rescue usage
56
- run = true
57
-
58
- # Set up summary objects
59
- total_summary = {
60
- :reqs_attempted => 0,
61
- :reqs_completed => 0,
62
- :latencies => [],
63
- }
64
- node_summary = {}
56
+ # Main entry point of the program. Specifically, this method will:
57
+ #
58
+ # * Parse and validate command line arguments
59
+ # * Use the `ElbPing::Resolver` to discover ELB nodes
60
+ # * Use the `ElbPing::HttpPinger` to ping ELB nodes
61
+ # * Track the statistics for these pings
62
+ # * And, finally, use call upon `ElbPing::Display` methods to output to stdout
65
63
 
64
+ def self.main
66
65
  # Catch ctrl-c
66
+ run = true
67
67
  trap("SIGINT") {
68
68
  run = false
69
69
  }
70
70
 
71
+ ##
72
+ # Parse and validate command line arguments
73
+ PARSER.parse!(ARGV) rescue usage
74
+
71
75
  if ARGV.size < 1
72
76
  usage
73
77
  end
78
+
74
79
  unless ARGV[0] =~ URI::regexp
75
- puts "ERROR: ELB URI does not seem valid"
80
+ ElbPing::Display.error "ELB URI does not seem valid"
76
81
  usage
77
82
  end
83
+
78
84
  elb_uri_s = ARGV[0]
79
85
  elb_uri = URI.parse(elb_uri_s)
80
86
 
87
+ ##
88
+ # Discover ELB nodes
89
+ #
90
+ # TODO: Perhaps some retry logic
91
+ nameserver = OPTIONS[:nameserver]
81
92
  begin
82
- nodes = ElbPing::Resolver.find_elb_nodes(elb_uri.host,
83
- OPTIONS[:nameserver])
93
+ nodes = ElbPing::Resolver.find_elb_nodes elb_uri.host, nameserver
84
94
  rescue
85
- puts "Error querying DNS for #{elb_uri.host} (NS: #{OPTIONS[:nameserver]})"
95
+ ElbPing::Display.error "Unable to query DNS for #{elb_uri.host} using #{nameserver}"
86
96
  exit(false)
87
97
  end
88
98
 
89
99
  if nodes.size < 1
90
- puts "Could not find any ELB nodes, no pings sent."
100
+ ElbPing::Display.error "Could not find any ELB nodes, no pings sent"
91
101
  exit(false)
92
102
  end
93
103
 
94
- nodes.each { |node| node_summary[node] = total_summary.clone }
104
+ ##
105
+ # Set up summary objects for stats tracking of latency and loss
106
+ stats = ElbPing::Stats.new
107
+ nodes.each { |node| stats.add_node node }
95
108
 
109
+ ##
110
+ # Run the main loop of the program
96
111
  iteration = 0
97
- while (OPTIONS[:count] < 1 || iteration < OPTIONS[:count]) && run
112
+ while run && (OPTIONS[:count] < 1 || iteration < OPTIONS[:count])
113
+
98
114
  sleep OPTIONS[:wait] if iteration > 0
99
115
 
116
+ ##
117
+ # Ping each node while tracking requests, responses, and latencies
100
118
  nodes.map { |node|
101
- total_summary[:reqs_attempted] += 1
102
- node_summary[node][:reqs_attempted] += 1
103
-
104
119
  status = ElbPing::HttpPinger.ping_node(node,
105
120
  elb_uri.port,
106
121
  (elb_uri.path == "") ? "/" : elb_uri.path,
107
122
  (elb_uri.scheme == 'https'),
108
123
  OPTIONS[:verb_len], OPTIONS[:timeout])
109
124
 
110
- unless status[:code] == :timeout
111
- total_summary[:reqs_completed] += 1
112
- total_summary[:latencies] += [status[:duration]]
113
- node_summary[node][:reqs_completed] += 1
114
- node_summary[node][:latencies] += [status[:duration]]
115
- end
125
+ # Display the response from the ping
126
+ ElbPing::Display.response status
116
127
 
117
- ElbPing::Display.response(status)
128
+ # Register stats
129
+ stats.register status
118
130
  }
119
131
  iteration += 1
120
132
  end
121
- ElbPing::Display.summary(total_summary, node_summary)
133
+ # Display the stats summary
134
+ ElbPing::Display.summary stats
122
135
  end
123
136
  end
124
137
  end
@@ -1,7 +1,35 @@
1
1
 
2
2
  module ElbPing
3
+ # This is responsible for all things that send to stdout. It is mostly only used by `ElbPing::CLI`
3
4
  module Display
4
- # Format and display the ping data
5
+
6
+ # Print message to the screen. Mostly used in case someone ever wants to override it.
7
+ #
8
+ # Arguments:
9
+ # * msg: (string) Message to display
10
+
11
+ def self.out(msg)
12
+ puts msg
13
+ end
14
+
15
+ # Print error message to the screen
16
+ #
17
+ # Arguments:
18
+ # * msg: (string) Message to display
19
+
20
+ def self.error(msg)
21
+ self.out "ERROR: #{msg}"
22
+ end
23
+
24
+ # Format and display the ping data given a response
25
+ #
26
+ # Arguments:
27
+ # * status: (hash) containing:
28
+ # * :node (string) IP address of node
29
+ # * :code (Fixnum || string || symbol) HTTP status code or symbol representing error during ping
30
+ # * :duration (Fixnum) Latency in milliseconds from ping
31
+ # * :exception (string, optional) Message to display from exception
32
+
5
33
  def self.response(status)
6
34
  node = status[:node]
7
35
  code = status[:code]
@@ -9,45 +37,26 @@ module ElbPing
9
37
  exc = status[:exception]
10
38
  exc_display = exc ? "exception=#{exc}" : ''
11
39
 
12
- puts "Response from #{node}: code=#{code.to_s} time=#{duration} ms #{exc_display}"
40
+ self.out "Response from #{node}: code=#{code.to_s} time=#{duration} ms #{exc_display}"
13
41
  end
14
42
 
15
- # Display summary of results (in aggregate and per-node)
16
- def self.summary(total_summary, node_summary)
17
- requests = total_summary[:reqs_attempted]
18
- responses = total_summary[:reqs_completed]
19
- loss = (1 - (responses.to_f/requests)) * 100
20
-
21
- latencies = total_summary[:latencies]
22
- avg_latency = 0
23
- unless latencies.size == 0
24
- sum_latency = latencies.inject { |sum, el| sum + el} || 0
25
- avg_latency = (sum_latency.to_f / latencies.size).to_i # ms
26
- end
27
-
28
- node_summary.each { |node, summary|
29
- requests = summary[:reqs_attempted]
30
- responses = summary[:reqs_completed]
31
- loss = (1 - (responses.to_f/requests)) * 100
32
-
33
- latencies = summary[:latencies]
34
- avg_latency = 0
35
- unless latencies.size == 0
36
- sum_latency = latencies.inject { |sum, el| sum + el} || 0
37
- avg_latency = (sum_latency.to_f / latencies.size).to_i # ms
38
- end
39
-
40
- puts "--- #{node} statistics ---"
41
- puts "#{requests} requests, #{responses} responses, #{loss.to_i}% loss"
42
- puts "min/avg/max = #{latencies.min}/#{avg_latency}/#{latencies.max} ms"
43
+ # Display summary of requests, responses, and latencies (for aggregate and per-node)
44
+ #
45
+ # Arguments:
46
+ # * stats: (ElbPing::Stats)
47
+
48
+ def self.summary(stats)
49
+ stats.nodes.keys.each { |node|
50
+ loss_pct = (stats.node_loss(node) * 100).to_i
51
+ self.out "--- #{node} statistics ---"
52
+ self.out "#{stats.nodes[node][:requests]} requests, #{stats.nodes[node][:responses]} responses, #{loss_pct}% loss"
53
+ self.out "min/avg/max = #{stats.nodes[node][:latencies].min}/#{stats.nodes[node][:latencies].mean}/#{stats.nodes[node][:latencies].max} ms"
43
54
  }
44
55
 
45
- puts '--- total statistics ---'
46
- puts "#{requests} requests, #{responses} responses, #{loss.to_i}% loss"
47
- puts "min/avg/max = #{latencies.min}/#{avg_latency}/#{latencies.max} ms"
48
-
56
+ loss_pct = (stats.total_loss * 100).to_i
57
+ self.out '--- total statistics ---'
58
+ self.out "#{stats.total[:requests]} requests, #{stats.total[:responses]} responses, #{loss_pct}% loss"
59
+ self.out "min/avg/max = #{stats.total[:latencies].min}/#{stats.total[:latencies].mean}/#{stats.total[:latencies].max} ms"
49
60
  end
50
-
51
61
  end
52
62
  end
53
-
@@ -3,9 +3,21 @@ require "net/http"
3
3
  require "net/https"
4
4
 
5
5
  module ElbPing
6
+ # Responsible for all HTTP ping-like functionality
6
7
  module HttpPinger
7
- # Make HTTP request to given node using custom request method
8
+
9
+ # Make HTTP request to given node using custom request method and measure response time
10
+ #
11
+ # Arguments:
12
+ # * node: (string) of node IP
13
+ # * port: (string || Fixnum) of positive integer [1, 65535]
14
+ # * path: (string) of path to request, e.g. "/"
15
+ # * use_ssl: (boolean) Whether or not this is HTTPS
16
+ # * verb_len: (Fixnum) of positive integer, how long the custom HTTP verb should be
17
+ # * timeout: (Fixnum) of positive integer, how many _seconds_ for connect and read timeouts
18
+
8
19
  def self.ping_node(node, port, path, use_ssl, verb_len, timeout)
20
+ ##
9
21
  # Build request class
10
22
  ping_request = Class.new(Net::HTTPRequest) do
11
23
  const_set :METHOD, "A" * verb_len
@@ -13,6 +25,7 @@ module ElbPing
13
25
  const_set :RESPONSE_HAS_BODY, false
14
26
  end
15
27
 
28
+ ##
16
29
  # Configure http object
17
30
  start = Time.now.getutc
18
31
  http = Net::HTTP.new(node, port.to_s)
@@ -20,12 +33,15 @@ module ElbPing
20
33
  http.read_timeout = timeout
21
34
  http.continue_timeout = timeout
22
35
 
36
+ # Enable SSL if it's to be used
23
37
  if use_ssl
24
38
  http.use_ssl = true
25
39
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
26
40
  http.ssl_timeout = timeout
27
41
  end
28
42
 
43
+ ##
44
+ # Make the HTTP request and handle any errors along the way
29
45
  error = nil
30
46
  exc = nil
31
47
  begin
@@ -1,8 +1,11 @@
1
1
 
2
2
  require 'resolv'
3
3
 
4
+ # A TCP-only resolver built from `Resolv::DNS`. See the docs for what it's about.
5
+ # http://ruby-doc.org/stdlib-1.9.3/libdoc/resolv/rdoc/Resolv/DNS.html
4
6
  class TcpDNS < Resolv::DNS
5
- # This is largely a copy-paste job from mri/source/lib/resolv.rb
7
+ # Override fetch_resource to use a TCP requester instead of a UDP requester. This
8
+ # is mostly borrowed from `lib/resolv.rb` with the UDP->TCP fallback logic removed.
6
9
  def fetch_resource(name, typeclass)
7
10
  lazy_initialize
8
11
  request = make_tcp_requester
@@ -19,19 +22,7 @@ class TcpDNS < Resolv::DNS
19
22
  reply, reply_name = requester.request(sender, tout)
20
23
  case reply.rcode
21
24
  when RCode::NoError
22
- if reply.tc == 1 and not Requester::TCP === requester
23
- requester.close
24
- # Retry via TCP:
25
- requester = make_tcp_requester(nameserver, port)
26
- senders = {}
27
- # This will use TCP for all remaining candidates (assuming the
28
- # current candidate does not already respond successfully via
29
- # TCP). This makes sense because we already know the full
30
- # response will not fit in an untruncated UDP packet.
31
- redo
32
- else
33
- yield(reply, reply_name)
34
- end
25
+ yield(reply, reply_name)
35
26
  return
36
27
  when RCode::NXDomain
37
28
  raise Config::NXDomain.new(reply_name.to_s)
@@ -46,22 +37,25 @@ class TcpDNS < Resolv::DNS
46
37
  end
47
38
 
48
39
  module ElbPing
40
+ # Handles all DNS resolution and, more specifically, ELB node discovery
49
41
  module Resolver
50
- def self.resolve_ns(nameserver)
51
- # Leftover from a resolver lib that wouldn't resolve its own nameservers
52
- return [nameserver]
53
- end
54
42
 
55
43
  # Resolve an ELB address to a list of node IPs. Should always return a list
56
44
  # as long as the server responded, even if it's empty.
57
- def self.find_elb_nodes(target, nameserver, timeout=5)
58
- # `timeout` is in seconds
59
- ns_addrs = resolve_ns nameserver
45
+ #
46
+ # Arguments:
47
+ # target: (string)
48
+ # nameservers: (array) of strings
49
+ # timeout: (fixnum)
50
+ #
51
+ # Could raise:
52
+ # * Timeout::Error
53
+ # * ?
60
54
 
61
- # Now resolve our ELB nodes
55
+ def self.find_elb_nodes(target, nameservers, timeout=5)
62
56
  resp = nil
63
57
  Timeout::timeout(timeout) do
64
- TcpDNS.open :nameserver => ns_addrs, :search => '', :ndots => 1 do |dns|
58
+ TcpDNS.open :nameserver => nameservers, :search => '', :ndots => 1 do |dns|
65
59
  # TODO: Exceptions
66
60
  resp = dns.getresources target, Resolv::DNS::Resource::IN::A
67
61
  end
@@ -70,4 +64,3 @@ module ElbPing
70
64
  end
71
65
  end
72
66
  end
73
-
@@ -0,0 +1,105 @@
1
+
2
+ # TODO: Needs unit tests
3
+
4
+ # An array for doing some basic stats on latencies (currently only mean)
5
+ class LatencyBucket < Array
6
+ def sum
7
+ self.inject { |sum, el| sum + el} || 0
8
+ end
9
+
10
+ def mean
11
+ i = 0
12
+ unless self.size == 0
13
+ i = (self.sum.to_f / self.size).to_i
14
+ end
15
+ end
16
+ end
17
+
18
+ module ElbPing
19
+ # Tracks the statistics of requests sent, responses received (hence loss) and latency
20
+ class Stats
21
+
22
+ attr_reader :total, :nodes
23
+
24
+ def initialize
25
+ @total = {
26
+ :requests => 0,
27
+ :responses => 0,
28
+ :latencies => LatencyBucket.new,
29
+ }
30
+ @nodes = {}
31
+ end
32
+
33
+ # Initializes stats buckets for a node if it doesn't already exist
34
+ #
35
+ # Arguments
36
+ # * node: (string) IP of node
37
+
38
+ def add_node(node)
39
+ unless @nodes.keys.include? node
40
+ @nodes[node] = {
41
+ :requests => 0,
42
+ :responses => 0,
43
+ :latencies => LatencyBucket.new,
44
+ }
45
+ end
46
+ end
47
+
48
+ # Registers stats following a ping
49
+ #
50
+ # Arguments:
51
+ # * node: (string) IP of node
52
+ # * status: (hash) Status object as returned from Pinger::ping_node
53
+
54
+ def register(status)
55
+ node = status[:node]
56
+ # Register the node if it hasn't been already
57
+ add_node node
58
+
59
+ # Update requests sent regardless of errors
60
+ @total[:requests] += 1
61
+ @nodes[node][:requests] += 1
62
+
63
+ # Don't update response counters or latencies if we encountered an error
64
+ unless [:timeout, :econnrefused, :exception].include? status[:code]
65
+ # Increment counters
66
+ @total[:responses] += 1
67
+ @nodes[node][:responses] += 1
68
+
69
+ # Track latencies
70
+ @total[:latencies] << status[:duration]
71
+ @nodes[node][:latencies] << status[:duration]
72
+ end
73
+ end
74
+
75
+ # Calculates loss across all nodes
76
+ def total_loss
77
+ calc_loss @total[:responses], @total[:requests]
78
+ end
79
+
80
+ # Calculates loss for a specific node
81
+ #
82
+ # Arguments:
83
+ # * node: (string) IP of node
84
+ #
85
+ # TODO: Handle non-existent nodes
86
+
87
+ def node_loss(node)
88
+ calc_loss @nodes[node][:responses], @nodes[node][:requests]
89
+ end
90
+
91
+ private
92
+
93
+ # Generic function to calculate loss as a per-1 float
94
+ #
95
+ # Arguments:
96
+ # * responses: (number) How many responses were received (numerator)
97
+ # * requests: (number) How many requests were sent (denominator)
98
+
99
+ def calc_loss(responses, requests)
100
+ 1 - (responses.to_f/requests)
101
+ end
102
+
103
+ end
104
+ end
105
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elbping
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,22 +11,6 @@ bindir: bin
11
11
  cert_chain: []
12
12
  date: 2013-08-13 00:00:00.000000000 Z
13
13
  dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: net-dns
16
- requirement: !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ~>
20
- - !ruby/object:Gem::Version
21
- version: 0.8.0
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ~>
28
- - !ruby/object:Gem::Version
29
- version: 0.8.0
30
14
  - !ruby/object:Gem::Dependency
31
15
  name: rake
32
16
  requirement: !ruby/object:Gem::Requirement
@@ -43,22 +27,6 @@ dependencies:
43
27
  - - ~>
44
28
  - !ruby/object:Gem::Version
45
29
  version: 10.0.4
46
- - !ruby/object:Gem::Dependency
47
- name: ipaddress
48
- requirement: !ruby/object:Gem::Requirement
49
- none: false
50
- requirements:
51
- - - ~>
52
- - !ruby/object:Gem::Version
53
- version: 0.8.0
54
- type: :runtime
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
- requirements:
59
- - - ~>
60
- - !ruby/object:Gem::Version
61
- version: 0.8.0
62
30
  description: elbping is a tool to ping all of the nodes behind an Amazon Elastic Load
63
31
  Balancer. It only works for ELBs in HTTP mode and works by triggering an HTTP 405
64
32
  (METHOD NOT ALLOWED) error caused when the ELB receives a HTTP verb that is too
@@ -73,6 +41,7 @@ files:
73
41
  - lib/elbping/display.rb
74
42
  - lib/elbping/pinger.rb
75
43
  - lib/elbping/resolver.rb
44
+ - lib/elbping/stats.rb
76
45
  - bin/elbping
77
46
  homepage: https://github.com/chooper/elbping
78
47
  licenses: