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 +46 -33
- data/lib/elbping/display.rb +45 -36
- data/lib/elbping/pinger.rb +17 -1
- data/lib/elbping/resolver.rb +17 -24
- data/lib/elbping/stats.rb +105 -0
- metadata +2 -33
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['
|
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
|
-
#
|
50
|
+
# Displays usage
|
48
51
|
def self.usage
|
49
|
-
|
52
|
+
ElbPing::Display.out PARSER.help
|
50
53
|
exit(false)
|
51
54
|
end
|
52
55
|
|
53
|
-
# Main entry point of the program
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
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
|
83
|
-
OPTIONS[:nameserver])
|
93
|
+
nodes = ElbPing::Resolver.find_elb_nodes elb_uri.host, nameserver
|
84
94
|
rescue
|
85
|
-
|
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
|
-
|
100
|
+
ElbPing::Display.error "Could not find any ELB nodes, no pings sent"
|
91
101
|
exit(false)
|
92
102
|
end
|
93
103
|
|
94
|
-
|
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])
|
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
|
-
|
111
|
-
|
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
|
-
|
128
|
+
# Register stats
|
129
|
+
stats.register status
|
118
130
|
}
|
119
131
|
iteration += 1
|
120
132
|
end
|
121
|
-
|
133
|
+
# Display the stats summary
|
134
|
+
ElbPing::Display.summary stats
|
122
135
|
end
|
123
136
|
end
|
124
137
|
end
|
data/lib/elbping/display.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
40
|
+
self.out "Response from #{node}: code=#{code.to_s} time=#{duration} ms #{exc_display}"
|
13
41
|
end
|
14
42
|
|
15
|
-
# Display summary of
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
data/lib/elbping/pinger.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/elbping/resolver.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
55
|
+
def self.find_elb_nodes(target, nameservers, timeout=5)
|
62
56
|
resp = nil
|
63
57
|
Timeout::timeout(timeout) do
|
64
|
-
TcpDNS.open :nameserver =>
|
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.
|
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:
|