io-metrics 0.3.0 → 0.4.0
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/io/metrics/listener/darwin.rb +8 -6
- data/lib/io/metrics/listener/linux.rb +37 -13
- data/lib/io/metrics/listener.rb +8 -3
- data/lib/io/metrics/version.rb +1 -1
- data/readme.md +6 -0
- data/releases.md +6 -0
- data.tar.gz.sig +0 -0
- metadata +1 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef189b6c38ef215409837bfe6d596116856ee48177fe67c63f6926f32e58e611
|
|
4
|
+
data.tar.gz: a62a323790464b1ef965cfbf1ff1aba63e329e025f0f3b0b93696a809aa7095a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b763b526aa7e8e82428206ba4b0a7d652fa3dd89ba4f3d8a0db7677af8c4e19171d459d6fd750f0b2f43aa3ac99461821c6f6fae61f1ac9c5024a569bb4af6ec
|
|
7
|
+
data.tar.gz: 79e05df4f9d528c29cc1030c59623efeb6b3147f029e6361133e1a5e0660fc9a46a78d8ff7baa5026bc2db892a8ac29bbfe99b6a310314bfaa96a84fd144e02b
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -33,12 +33,14 @@ class IO
|
|
|
33
33
|
return Addrinfo.tcp(ip, port)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# Handle IPv6
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
# Handle IPv6: [::1].8080, [fe80::1%lo0].8080, [::].8080
|
|
37
|
+
if address =~ /\A\[([^\]]+)\]\.(\d+)\z/
|
|
38
|
+
ip = $1.sub(/%.*\z/, "") # strip zone ID (e.g. %lo0)
|
|
39
|
+
port = $2.to_i
|
|
40
|
+
return Addrinfo.tcp(ip, port)
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
nil
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
# Build a stable string key for TCP listener filter matching (same style as Linux / user filters).
|
|
@@ -83,7 +85,7 @@ class IO
|
|
|
83
85
|
# Apply filter if specified
|
|
84
86
|
next if address_filter && !address_filter.include?(key.downcase)
|
|
85
87
|
|
|
86
|
-
listeners[key] ||= Listener.new(addrinfo, 0, 0, 0)
|
|
88
|
+
listeners[key] ||= Listener.new(addrinfo, 0, 0, 0, 0, 0)
|
|
87
89
|
listeners[key].queued_count = queue_length
|
|
88
90
|
|
|
89
91
|
# active_count and close_wait_count set to 0 (netstat -L doesn't expose connection states)
|
|
@@ -162,8 +162,10 @@ class IO
|
|
|
162
162
|
address_filter = addresses ? addresses.map{|address| address.downcase}.to_set : nil
|
|
163
163
|
connections = []
|
|
164
164
|
close_wait_connections = []
|
|
165
|
+
fin_wait_connections = []
|
|
166
|
+
time_wait_connections = []
|
|
165
167
|
|
|
166
|
-
# Single pass: collect LISTEN sockets
|
|
168
|
+
# Single pass: collect LISTEN sockets and tracked connection states.
|
|
167
169
|
File.foreach(file) do |line|
|
|
168
170
|
next if line.start_with?("sl")
|
|
169
171
|
|
|
@@ -193,14 +195,16 @@ class IO
|
|
|
193
195
|
# Apply filter if specified
|
|
194
196
|
next if address_filter && !address_filter.include?(local_address.downcase)
|
|
195
197
|
|
|
196
|
-
listeners[local_address] ||= Listener.new(Addrinfo.tcp(local_ip, local_port), 0, 0, 0)
|
|
198
|
+
listeners[local_address] ||= Listener.new(Addrinfo.tcp(local_ip, local_port), 0, 0, 0, 0, 0)
|
|
197
199
|
# rx_queue shows number of connections waiting to be accepted.
|
|
198
200
|
# Accumulate across SO_REUSEPORT sockets sharing the same address.
|
|
199
201
|
listeners[local_address].queued_count += rx_queue_hex.to_i(16)
|
|
200
202
|
listeners[local_address].active_count = 0
|
|
201
203
|
listeners[local_address].close_wait_count = 0
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
listeners[local_address].fin_wait_count = 0
|
|
205
|
+
listeners[local_address].time_wait_count = 0
|
|
206
|
+
elsif state == :established || state == :close_wait ||
|
|
207
|
+
state == :fin_wait1 || state == :fin_wait2 || state == :time_wait
|
|
204
208
|
if ipv6
|
|
205
209
|
local_ip = parse_ipv6(local_ip_hex)
|
|
206
210
|
local_address = "[#{local_ip}]:#{parse_port(local_port_hex)}"
|
|
@@ -209,16 +213,17 @@ class IO
|
|
|
209
213
|
local_port = parse_port(local_port_hex)
|
|
210
214
|
local_address = "#{local_ip}:#{local_port}"
|
|
211
215
|
end
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
case state
|
|
217
|
+
when :established then connections << local_address
|
|
218
|
+
when :close_wait then close_wait_connections << local_address
|
|
219
|
+
when :fin_wait1, :fin_wait2 then fin_wait_connections << local_address
|
|
220
|
+
when :time_wait then time_wait_connections << local_address
|
|
216
221
|
end
|
|
217
222
|
end
|
|
218
223
|
end
|
|
219
224
|
end
|
|
220
225
|
|
|
221
|
-
# Count ESTABLISHED connections for each listener
|
|
226
|
+
# Count ESTABLISHED connections for each listener.
|
|
222
227
|
connections.each do |local_address|
|
|
223
228
|
if listener_address = find_matching_listener(local_address, listeners)
|
|
224
229
|
listeners[listener_address].active_count += 1
|
|
@@ -226,15 +231,34 @@ class IO
|
|
|
226
231
|
end
|
|
227
232
|
|
|
228
233
|
# Count CLOSE_WAIT connections for each listener.
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
# callbacks, or processing a request whose upstream already disconnected).
|
|
234
|
+
# Peer has closed its end; application still holds the socket
|
|
235
|
+
# (e.g. running rack.response_finished callbacks).
|
|
232
236
|
close_wait_connections.each do |local_address|
|
|
233
237
|
if listener_address = find_matching_listener(local_address, listeners)
|
|
234
238
|
listeners[listener_address].close_wait_count += 1
|
|
235
239
|
end
|
|
236
240
|
end
|
|
237
241
|
|
|
242
|
+
# Count FIN_WAIT1 + FIN_WAIT2 connections for each listener.
|
|
243
|
+
# Server has sent FIN (initiated close), waiting for peer to finish closing.
|
|
244
|
+
# FIN_WAIT1 is extremely brief (ACK in transit); FIN_WAIT2 persists until
|
|
245
|
+
# the peer sends its FIN — both represent the same "server-initiated close" phase.
|
|
246
|
+
fin_wait_connections.each do |local_address|
|
|
247
|
+
if listener_address = find_matching_listener(local_address, listeners)
|
|
248
|
+
listeners[listener_address].fin_wait_count += 1
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Count TIME_WAIT connections for each listener.
|
|
253
|
+
# Both sides have closed; the kernel holds the socket for ~60s (2×MSL) to
|
|
254
|
+
# absorb delayed packets. These consume file descriptors but represent no
|
|
255
|
+
# active work. High counts indicate fast connection churn.
|
|
256
|
+
time_wait_connections.each do |local_address|
|
|
257
|
+
if listener_address = find_matching_listener(local_address, listeners)
|
|
258
|
+
listeners[listener_address].time_wait_count += 1
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
238
262
|
# /proc lists every ESTABLISHED child with the listener's local address, including
|
|
239
263
|
# sockets still in the accept queue. Those are already counted in queued_count on the
|
|
240
264
|
# LISTEN row (same meaning as Raindrops inet_diag queued vs inode != 0 for active).
|
|
@@ -279,7 +303,7 @@ class IO
|
|
|
279
303
|
|
|
280
304
|
state = state_hex.to_i(16)
|
|
281
305
|
|
|
282
|
-
listeners[path] ||= Listener.new(Addrinfo.unix(path), 0, 0, 0)
|
|
306
|
+
listeners[path] ||= Listener.new(Addrinfo.unix(path), 0, 0, 0, 0, 0)
|
|
283
307
|
|
|
284
308
|
case state
|
|
285
309
|
when SS_CONNECTING # Queued connections
|
data/lib/io/metrics/listener.rb
CHANGED
|
@@ -7,12 +7,15 @@ require "json"
|
|
|
7
7
|
|
|
8
8
|
class IO
|
|
9
9
|
module Metrics
|
|
10
|
+
# Represents a network listener socket with its queue statistics.
|
|
10
11
|
# Represents a network listener socket with its queue statistics.
|
|
11
12
|
# @attribute address [Addrinfo | Nil] Listening endpoint from capture; nil only for {Listener.zero} placeholders.
|
|
12
13
|
# @attribute queued_count [Integer] Number of connections waiting to be accepted (currently in the accept queue).
|
|
13
14
|
# @attribute active_count [Integer] Number of accepted connections in ESTABLISHED state.
|
|
14
|
-
# @attribute close_wait_count [Integer] Number of
|
|
15
|
-
|
|
15
|
+
# @attribute close_wait_count [Integer] Number of connections in CLOSE_WAIT state (peer has closed; application still holds the socket).
|
|
16
|
+
# @attribute fin_wait_count [Integer] Number of connections in FIN_WAIT1 or FIN_WAIT2 state (server has initiated close; peer has not yet completed close).
|
|
17
|
+
# @attribute time_wait_count [Integer] Number of connections in TIME_WAIT state (both sides closed; waiting ~60s for delayed packets before releasing the port/fd).
|
|
18
|
+
class Listener < Struct.new(:address, :queued_count, :active_count, :close_wait_count, :fin_wait_count, :time_wait_count)
|
|
16
19
|
# Serialize for JSON; address uses Addrinfo#inspect_sockaddr.
|
|
17
20
|
def as_json(*)
|
|
18
21
|
{
|
|
@@ -20,6 +23,8 @@ class IO
|
|
|
20
23
|
queued_count: queued_count,
|
|
21
24
|
active_count: active_count,
|
|
22
25
|
close_wait_count: close_wait_count,
|
|
26
|
+
fin_wait_count: fin_wait_count,
|
|
27
|
+
time_wait_count: time_wait_count,
|
|
23
28
|
}
|
|
24
29
|
end
|
|
25
30
|
|
|
@@ -31,7 +36,7 @@ class IO
|
|
|
31
36
|
# Create a zero-initialized Listener instance (no endpoint; for tests or templates).
|
|
32
37
|
# @returns [Listener] Counters zero; {#address} is nil.
|
|
33
38
|
def self.zero
|
|
34
|
-
new(nil, 0, 0, 0
|
|
39
|
+
new(nil, 0, 0, 0, 0, 0)
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
# Whether listener stats can be captured on this system.
|
data/lib/io/metrics/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -14,6 +14,12 @@ Please see the [project documentation](https://socketry.github.io/io-metrics/) f
|
|
|
14
14
|
|
|
15
15
|
Please see the [project releases](https://socketry.github.io/io-metrics/releases/index) for all releases.
|
|
16
16
|
|
|
17
|
+
### v0.4.0
|
|
18
|
+
|
|
19
|
+
- Introduce `Listener#fin_wait_count`: connections in `FIN_WAIT1` or `FIN_WAIT2` state — server has sent FIN (initiated close) and is waiting for the peer to finish closing. Symmetric counterpart to `close_wait_count`.
|
|
20
|
+
- Introduce `Listener#time_wait_count`: connections in `TIME_WAIT` state — both sides have closed; the kernel holds the socket for \~60s (2×MSL) to absorb delayed packets. High counts indicate fast connection churn.
|
|
21
|
+
- Fix `parse_address` for IPv6 listeners: replace non-existent `Addrinfo.parse` with an explicit `[ipv6addr].port` pattern match.
|
|
22
|
+
|
|
17
23
|
### v0.3.0
|
|
18
24
|
|
|
19
25
|
- **Breaking** Rename `Listener` fields: `queue_size` → `queued_count`, `active_connections` → `active_count`.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.4.0
|
|
4
|
+
|
|
5
|
+
- Introduce `Listener#fin_wait_count`: connections in `FIN_WAIT1` or `FIN_WAIT2` state — server has sent FIN (initiated close) and is waiting for the peer to finish closing. Symmetric counterpart to `close_wait_count`.
|
|
6
|
+
- Introduce `Listener#time_wait_count`: connections in `TIME_WAIT` state — both sides have closed; the kernel holds the socket for \~60s (2×MSL) to absorb delayed packets. High counts indicate fast connection churn.
|
|
7
|
+
- Fix `parse_address` for IPv6 listeners: replace non-existent `Addrinfo.parse` with an explicit `[ipv6addr].port` pattern match.
|
|
8
|
+
|
|
3
9
|
## v0.3.0
|
|
4
10
|
|
|
5
11
|
- **Breaking** Rename `Listener` fields: `queue_size` → `queued_count`, `active_connections` → `active_count`.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|