io-metrics 0.2.1 → 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 +13 -9
- data/lib/io/metrics/listener/linux.rb +57 -17
- data/lib/io/metrics/listener.rb +13 -6
- data/lib/io/metrics/version.rb +1 -1
- data/readme.md +11 -0
- data/releases.md +11 -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,10 +85,12 @@ 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)
|
|
87
|
-
listeners[key].
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
listeners[key] ||= Listener.new(addrinfo, 0, 0, 0, 0, 0)
|
|
89
|
+
listeners[key].queued_count = queue_length
|
|
90
|
+
|
|
91
|
+
# active_count and close_wait_count set to 0 (netstat -L doesn't expose connection states)
|
|
92
|
+
listeners[key].active_count = 0
|
|
93
|
+
listeners[key].close_wait_count = 0
|
|
90
94
|
end
|
|
91
95
|
end
|
|
92
96
|
end
|
|
@@ -161,8 +161,11 @@ class IO
|
|
|
161
161
|
listeners = {}
|
|
162
162
|
address_filter = addresses ? addresses.map{|address| address.downcase}.to_set : nil
|
|
163
163
|
connections = []
|
|
164
|
+
close_wait_connections = []
|
|
165
|
+
fin_wait_connections = []
|
|
166
|
+
time_wait_connections = []
|
|
164
167
|
|
|
165
|
-
# Single pass: collect LISTEN sockets and
|
|
168
|
+
# Single pass: collect LISTEN sockets and tracked connection states.
|
|
166
169
|
File.foreach(file) do |line|
|
|
167
170
|
next if line.start_with?("sl")
|
|
168
171
|
|
|
@@ -192,13 +195,16 @@ class IO
|
|
|
192
195
|
# Apply filter if specified
|
|
193
196
|
next if address_filter && !address_filter.include?(local_address.downcase)
|
|
194
197
|
|
|
195
|
-
listeners[local_address] ||= Listener.new(Addrinfo.tcp(local_ip, local_port), 0, 0)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
listeners[local_address].
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
listeners[local_address] ||= Listener.new(Addrinfo.tcp(local_ip, local_port), 0, 0, 0, 0, 0)
|
|
199
|
+
# rx_queue shows number of connections waiting to be accepted.
|
|
200
|
+
# Accumulate across SO_REUSEPORT sockets sharing the same address.
|
|
201
|
+
listeners[local_address].queued_count += rx_queue_hex.to_i(16)
|
|
202
|
+
listeners[local_address].active_count = 0
|
|
203
|
+
listeners[local_address].close_wait_count = 0
|
|
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
|
|
202
208
|
if ipv6
|
|
203
209
|
local_ip = parse_ipv6(local_ip_hex)
|
|
204
210
|
local_address = "[#{local_ip}]:#{parse_port(local_port_hex)}"
|
|
@@ -207,24 +213,58 @@ class IO
|
|
|
207
213
|
local_port = parse_port(local_port_hex)
|
|
208
214
|
local_address = "#{local_ip}:#{local_port}"
|
|
209
215
|
end
|
|
210
|
-
|
|
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
|
|
221
|
+
end
|
|
211
222
|
end
|
|
212
223
|
end
|
|
213
224
|
end
|
|
214
225
|
|
|
215
|
-
# Count ESTABLISHED connections for each listener
|
|
226
|
+
# Count ESTABLISHED connections for each listener.
|
|
216
227
|
connections.each do |local_address|
|
|
217
228
|
if listener_address = find_matching_listener(local_address, listeners)
|
|
218
|
-
listeners[listener_address].
|
|
229
|
+
listeners[listener_address].active_count += 1
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Count CLOSE_WAIT connections for each listener.
|
|
234
|
+
# Peer has closed its end; application still holds the socket
|
|
235
|
+
# (e.g. running rack.response_finished callbacks).
|
|
236
|
+
close_wait_connections.each do |local_address|
|
|
237
|
+
if listener_address = find_matching_listener(local_address, listeners)
|
|
238
|
+
listeners[listener_address].close_wait_count += 1
|
|
239
|
+
end
|
|
240
|
+
end
|
|
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
|
|
219
259
|
end
|
|
220
260
|
end
|
|
221
261
|
|
|
222
262
|
# /proc lists every ESTABLISHED child with the listener's local address, including
|
|
223
|
-
# sockets still in the accept queue. Those are already counted in
|
|
263
|
+
# sockets still in the accept queue. Those are already counted in queued_count on the
|
|
224
264
|
# LISTEN row (same meaning as Raindrops inet_diag queued vs inode != 0 for active).
|
|
225
265
|
listeners.each_value do |listener|
|
|
226
|
-
backlog = listener.
|
|
227
|
-
listener.
|
|
266
|
+
backlog = listener.queued_count
|
|
267
|
+
listener.active_count = [listener.active_count - backlog, 0].max
|
|
228
268
|
end
|
|
229
269
|
|
|
230
270
|
return listeners
|
|
@@ -263,13 +303,13 @@ class IO
|
|
|
263
303
|
|
|
264
304
|
state = state_hex.to_i(16)
|
|
265
305
|
|
|
266
|
-
listeners[path] ||= Listener.new(Addrinfo.unix(path), 0, 0)
|
|
306
|
+
listeners[path] ||= Listener.new(Addrinfo.unix(path), 0, 0, 0, 0, 0)
|
|
267
307
|
|
|
268
308
|
case state
|
|
269
309
|
when SS_CONNECTING # Queued connections
|
|
270
|
-
listeners[path].
|
|
310
|
+
listeners[path].queued_count += 1
|
|
271
311
|
when SS_CONNECTED # Active connections
|
|
272
|
-
listeners[path].
|
|
312
|
+
listeners[path].active_count += 1
|
|
273
313
|
end
|
|
274
314
|
end
|
|
275
315
|
|
data/lib/io/metrics/listener.rb
CHANGED
|
@@ -7,17 +7,24 @@ 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
|
-
# @attribute
|
|
13
|
-
# @attribute
|
|
14
|
-
|
|
13
|
+
# @attribute queued_count [Integer] Number of connections waiting to be accepted (currently in the accept queue).
|
|
14
|
+
# @attribute active_count [Integer] Number of accepted connections in ESTABLISHED state.
|
|
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)
|
|
15
19
|
# Serialize for JSON; address uses Addrinfo#inspect_sockaddr.
|
|
16
20
|
def as_json(*)
|
|
17
21
|
{
|
|
18
22
|
address: address&.inspect_sockaddr,
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
queued_count: queued_count,
|
|
24
|
+
active_count: active_count,
|
|
25
|
+
close_wait_count: close_wait_count,
|
|
26
|
+
fin_wait_count: fin_wait_count,
|
|
27
|
+
time_wait_count: time_wait_count,
|
|
21
28
|
}
|
|
22
29
|
end
|
|
23
30
|
|
|
@@ -29,7 +36,7 @@ class IO
|
|
|
29
36
|
# Create a zero-initialized Listener instance (no endpoint; for tests or templates).
|
|
30
37
|
# @returns [Listener] Counters zero; {#address} is nil.
|
|
31
38
|
def self.zero
|
|
32
|
-
new(nil, 0, 0)
|
|
39
|
+
new(nil, 0, 0, 0, 0, 0)
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
# Whether listener stats can be captured on this system.
|
data/lib/io/metrics/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -14,6 +14,17 @@ 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
|
+
|
|
23
|
+
### v0.3.0
|
|
24
|
+
|
|
25
|
+
- **Breaking** Rename `Listener` fields: `queue_size` → `queued_count`, `active_connections` → `active_count`.
|
|
26
|
+
- Introduce `Listener#close_wait_count`: number of accepted connections in `CLOSE_WAIT` state (peer has closed; application still processing).
|
|
27
|
+
|
|
17
28
|
### v0.2.1
|
|
18
29
|
|
|
19
30
|
- Fixed `queue_size` under-reporting when multiple `SO_REUSEPORT` sockets share the same address — queue depths are now accumulated across all sockets rather than overwritten by the last one.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
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
|
+
|
|
9
|
+
## v0.3.0
|
|
10
|
+
|
|
11
|
+
- **Breaking** Rename `Listener` fields: `queue_size` → `queued_count`, `active_connections` → `active_count`.
|
|
12
|
+
- Introduce `Listener#close_wait_count`: number of accepted connections in `CLOSE_WAIT` state (peer has closed; application still processing).
|
|
13
|
+
|
|
3
14
|
## v0.2.1
|
|
4
15
|
|
|
5
16
|
- Fixed `queue_size` under-reporting when multiple `SO_REUSEPORT` sockets share the same address — queue depths are now accumulated across all sockets rather than overwritten by the last one.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|