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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50d4b28faec3b5ae9389c3437d1e5687c1e54229d0abfd79305699e3f43e798b
4
- data.tar.gz: 33db46c67476acedd0ee7695d437679ffe8e3602f60de2d15363e9b59423aab6
3
+ metadata.gz: ef189b6c38ef215409837bfe6d596116856ee48177fe67c63f6926f32e58e611
4
+ data.tar.gz: a62a323790464b1ef965cfbf1ff1aba63e329e025f0f3b0b93696a809aa7095a
5
5
  SHA512:
6
- metadata.gz: 4131cbf7a3d8d15256972f057c24608ac1f9a72e958a6361ef24e36ceb97f669767e80dfc309d9c39df6d8aa530d10f82d93d9e53a61f7e6d2b1a47e16568434
7
- data.tar.gz: b3c5ae946df3a0f7d1fe59d787f91aa0faf95c2f8b8353a2b132576325f033c3e7819613923b5ba79fe87e153424591fa66f3f1cfbc6030592bfa2d44e39619d
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 or other formats: best-effort via Addrinfo.parse
37
- begin
38
- Addrinfo.parse(address)
39
- rescue ArgumentError, SocketError
40
- nil
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].queue_size = queue_length
88
- # active_connections set to 0 (can't reliably count per listener)
89
- listeners[key].active_connections = 0
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 ESTABLISHED connections
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
- # rx_queue shows number of connections waiting to be accepted.
197
- # Accumulate across SO_REUSEPORT sockets sharing the same address.
198
- listeners[local_address].queue_size += rx_queue_hex.to_i(16)
199
- listeners[local_address].active_connections = 0
200
- # Collect ESTABLISHED connections to count later
201
- elsif state == :established
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
- connections << local_address
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].active_connections += 1
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 queue_size on the
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.queue_size
227
- listener.active_connections = [listener.active_connections - backlog, 0].max
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].queue_size += 1
310
+ listeners[path].queued_count += 1
271
311
  when SS_CONNECTED # Active connections
272
- listeners[path].active_connections += 1
312
+ listeners[path].active_count += 1
273
313
  end
274
314
  end
275
315
 
@@ -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 queue_size [Integer] Number of connections waiting to be accepted (queued).
13
- # @attribute active_connections [Integer] Number of active connections (already accepted).
14
- class Listener < Struct.new(:address, :queue_size, :active_connections)
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
- queue_size: queue_size,
20
- active_connections: active_connections,
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.
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Metrics
10
- VERSION = "0.2.1"
10
+ VERSION = "0.4.0"
11
11
  end
12
12
  end
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
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file