io-metrics 0.2.0 → 0.3.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: d3e42882ce221e2e9f10808ed4de4ea6a72b3490662a014727e40b49fe8826ab
4
- data.tar.gz: da5595f41dfe59f91dc3355ebe6538bac4ca0bc70e1c1040c6cc6e911c977498
3
+ metadata.gz: f7f07281a060f8644b5f50c62d526bfca3ea15ecb8a63001192f617b0aa7690d
4
+ data.tar.gz: ae6e4568aeae519c4e69643cc60a9203e0549408db787148b035151462a12819
5
5
  SHA512:
6
- metadata.gz: cee9a635c1df4b7bfc18497e835add923739f5656737126bdc86cae1bca17a8074925243240eed1b802f741e8956d63b18c434af7f8d29853e3750aa3f55b5f7
7
- data.tar.gz: 512a4e960252f726b0a50348d7ceab9f6bbc13247aab649f17a48d9d3d1b58cf489141b9095b24cebabd0d39546196a5bf454e3e8f51389b652b485e16218a6b
6
+ metadata.gz: 69bc0a438888c3f4a58790d53aa09919110e7c538c6cbdbf04165c2c6472043a47844b5b61a5d8db61df739289ea806c7f906422c734a0a3a0f3c727b7756624
7
+ data.tar.gz: a3a052adb78a2d63276282795ffc9c2d3d691f5afc7dbc2313a9fe528a1f4ee5d82157b050aef6514d52b6cf8025ff4c3344d59df41782653916bbc3491069c2
checksums.yaml.gz.sig CHANGED
Binary file
@@ -83,10 +83,12 @@ class IO
83
83
  # Apply filter if specified
84
84
  next if address_filter && !address_filter.include?(key.downcase)
85
85
 
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
86
+ listeners[key] ||= Listener.new(addrinfo, 0, 0, 0)
87
+ listeners[key].queued_count = queue_length
88
+
89
+ # active_count and close_wait_count set to 0 (netstat -L doesn't expose connection states)
90
+ listeners[key].active_count = 0
91
+ listeners[key].close_wait_count = 0
90
92
  end
91
93
  end
92
94
  end
@@ -161,8 +161,9 @@ 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 = []
164
165
 
165
- # Single pass: collect LISTEN sockets and ESTABLISHED connections
166
+ # Single pass: collect LISTEN sockets, ESTABLISHED, and CLOSE_WAIT connections
166
167
  File.foreach(file) do |line|
167
168
  next if line.start_with?("sl")
168
169
 
@@ -192,12 +193,14 @@ class IO
192
193
  # Apply filter if specified
193
194
  next if address_filter && !address_filter.include?(local_address.downcase)
194
195
 
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
- listeners[local_address].queue_size = rx_queue_hex.to_i(16)
198
- listeners[local_address].active_connections = 0
199
- # Collect ESTABLISHED connections to count later
200
- elsif state == :established
196
+ listeners[local_address] ||= Listener.new(Addrinfo.tcp(local_ip, local_port), 0, 0, 0)
197
+ # rx_queue shows number of connections waiting to be accepted.
198
+ # Accumulate across SO_REUSEPORT sockets sharing the same address.
199
+ listeners[local_address].queued_count += rx_queue_hex.to_i(16)
200
+ listeners[local_address].active_count = 0
201
+ listeners[local_address].close_wait_count = 0
202
+ # Collect ESTABLISHED and CLOSE_WAIT connections to count later
203
+ elsif state == :established || state == :close_wait
201
204
  if ipv6
202
205
  local_ip = parse_ipv6(local_ip_hex)
203
206
  local_address = "[#{local_ip}]:#{parse_port(local_port_hex)}"
@@ -206,7 +209,11 @@ class IO
206
209
  local_port = parse_port(local_port_hex)
207
210
  local_address = "#{local_ip}:#{local_port}"
208
211
  end
209
- connections << local_address
212
+ if state == :established
213
+ connections << local_address
214
+ else
215
+ close_wait_connections << local_address
216
+ end
210
217
  end
211
218
  end
212
219
  end
@@ -214,10 +221,28 @@ class IO
214
221
  # Count ESTABLISHED connections for each listener
215
222
  connections.each do |local_address|
216
223
  if listener_address = find_matching_listener(local_address, listeners)
217
- listeners[listener_address].active_connections += 1
224
+ listeners[listener_address].active_count += 1
225
+ end
226
+ end
227
+
228
+ # 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).
232
+ close_wait_connections.each do |local_address|
233
+ if listener_address = find_matching_listener(local_address, listeners)
234
+ listeners[listener_address].close_wait_count += 1
218
235
  end
219
236
  end
220
237
 
238
+ # /proc lists every ESTABLISHED child with the listener's local address, including
239
+ # sockets still in the accept queue. Those are already counted in queued_count on the
240
+ # LISTEN row (same meaning as Raindrops inet_diag queued vs inode != 0 for active).
241
+ listeners.each_value do |listener|
242
+ backlog = listener.queued_count
243
+ listener.active_count = [listener.active_count - backlog, 0].max
244
+ end
245
+
221
246
  return listeners
222
247
  rescue Errno::ENOENT, Errno::EACCES
223
248
  return {}
@@ -254,13 +279,13 @@ class IO
254
279
 
255
280
  state = state_hex.to_i(16)
256
281
 
257
- listeners[path] ||= Listener.new(Addrinfo.unix(path), 0, 0)
282
+ listeners[path] ||= Listener.new(Addrinfo.unix(path), 0, 0, 0)
258
283
 
259
284
  case state
260
285
  when SS_CONNECTING # Queued connections
261
- listeners[path].queue_size += 1
286
+ listeners[path].queued_count += 1
262
287
  when SS_CONNECTED # Active connections
263
- listeners[path].active_connections += 1
288
+ listeners[path].active_count += 1
264
289
  end
265
290
  end
266
291
 
@@ -9,15 +9,17 @@ class IO
9
9
  module Metrics
10
10
  # Represents a network listener socket with its queue statistics.
11
11
  # @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)
12
+ # @attribute queued_count [Integer] Number of connections waiting to be accepted (currently in the accept queue).
13
+ # @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
16
  # Serialize for JSON; address uses Addrinfo#inspect_sockaddr.
16
17
  def as_json(*)
17
18
  {
18
19
  address: address&.inspect_sockaddr,
19
- queue_size: queue_size,
20
- active_connections: active_connections,
20
+ queued_count: queued_count,
21
+ active_count: active_count,
22
+ close_wait_count: close_wait_count,
21
23
  }
22
24
  end
23
25
 
@@ -29,7 +31,7 @@ class IO
29
31
  # Create a zero-initialized Listener instance (no endpoint; for tests or templates).
30
32
  # @returns [Listener] Counters zero; {#address} is nil.
31
33
  def self.zero
32
- new(nil, 0, 0)
34
+ new(nil, 0, 0, 0) # address, queued_count, active_count, close_wait_count
33
35
  end
34
36
 
35
37
  # 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.0"
10
+ VERSION = "0.3.0"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -14,6 +14,16 @@ 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.3.0
18
+
19
+ - **Breaking** Rename `Listener` fields: `queue_size` → `queued_count`, `active_connections` → `active_count`.
20
+ - Introduce `Listener#close_wait_count`: number of accepted connections in `CLOSE_WAIT` state (peer has closed; application still processing).
21
+
22
+ ### v0.2.1
23
+
24
+ - 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.
25
+ - **Linux** `Listener#active_connections` for TCP no longer counts sockets that are still in the kernel accept queue (those remain in `queue_size`). Counts now match the usual “past `accept()`” meaning and align with tools such as Raindrops’ `ListenStats#active`.
26
+
17
27
  ### v0.2.0
18
28
 
19
29
  - **Breaking** `IO::Metrics::Listener.capture` returns an `Array` of `Listener` rows instead of a `Hash` keyed by address string.
data/releases.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Releases
2
2
 
3
+ ## v0.3.0
4
+
5
+ - **Breaking** Rename `Listener` fields: `queue_size` → `queued_count`, `active_connections` → `active_count`.
6
+ - Introduce `Listener#close_wait_count`: number of accepted connections in `CLOSE_WAIT` state (peer has closed; application still processing).
7
+
8
+ ## v0.2.1
9
+
10
+ - 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.
11
+ - **Linux** `Listener#active_connections` for TCP no longer counts sockets that are still in the kernel accept queue (those remain in `queue_size`). Counts now match the usual “past `accept()`” meaning and align with tools such as Raindrops’ `ListenStats#active`.
12
+
3
13
  ## v0.2.0
4
14
 
5
15
  - **Breaking** `IO::Metrics::Listener.capture` returns an `Array` of `Listener` rows instead of a `Hash` keyed by address string.
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.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file