elbping 0.0.10 → 0.0.11
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/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:
|