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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7f07281a060f8644b5f50c62d526bfca3ea15ecb8a63001192f617b0aa7690d
4
- data.tar.gz: ae6e4568aeae519c4e69643cc60a9203e0549408db787148b035151462a12819
3
+ metadata.gz: ef189b6c38ef215409837bfe6d596116856ee48177fe67c63f6926f32e58e611
4
+ data.tar.gz: a62a323790464b1ef965cfbf1ff1aba63e329e025f0f3b0b93696a809aa7095a
5
5
  SHA512:
6
- metadata.gz: 69bc0a438888c3f4a58790d53aa09919110e7c538c6cbdbf04165c2c6472043a47844b5b61a5d8db61df739289ea806c7f906422c734a0a3a0f3c727b7756624
7
- data.tar.gz: a3a052adb78a2d63276282795ffc9c2d3d691f5afc7dbc2313a9fe528a1f4ee5d82157b050aef6514d52b6cf8025ff4c3344d59df41782653916bbc3491069c2
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,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, ESTABLISHED, and CLOSE_WAIT connections
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
- # Collect ESTABLISHED and CLOSE_WAIT connections to count later
203
- elsif state == :established || state == :close_wait
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
- if state == :established
213
- connections << local_address
214
- else
215
- close_wait_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
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
- # These are accepted connections where the peer has closed its end but the
230
- # application has not yet closed its side (e.g. still in rack.response_finished
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
@@ -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 accepted connections in CLOSE_WAIT state (peer has closed; application still processing).
15
- class Listener < Struct.new(:address, :queued_count, :active_count, :close_wait_count)
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) # address, queued_count, active_count, close_wait_count
39
+ new(nil, 0, 0, 0, 0, 0)
35
40
  end
36
41
 
37
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.3.0"
10
+ VERSION = "0.4.0"
11
11
  end
12
12
  end
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
@@ -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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file