io-metrics 0.2.1 → 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: 50d4b28faec3b5ae9389c3437d1e5687c1e54229d0abfd79305699e3f43e798b
4
- data.tar.gz: 33db46c67476acedd0ee7695d437679ffe8e3602f60de2d15363e9b59423aab6
3
+ metadata.gz: f7f07281a060f8644b5f50c62d526bfca3ea15ecb8a63001192f617b0aa7690d
4
+ data.tar.gz: ae6e4568aeae519c4e69643cc60a9203e0549408db787148b035151462a12819
5
5
  SHA512:
6
- metadata.gz: 4131cbf7a3d8d15256972f057c24608ac1f9a72e958a6361ef24e36ceb97f669767e80dfc309d9c39df6d8aa530d10f82d93d9e53a61f7e6d2b1a47e16568434
7
- data.tar.gz: b3c5ae946df3a0f7d1fe59d787f91aa0faf95c2f8b8353a2b132576325f033c3e7819613923b5ba79fe87e153424591fa66f3f1cfbc6030592bfa2d44e39619d
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,13 +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
- # 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
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
202
204
  if ipv6
203
205
  local_ip = parse_ipv6(local_ip_hex)
204
206
  local_address = "[#{local_ip}]:#{parse_port(local_port_hex)}"
@@ -207,7 +209,11 @@ class IO
207
209
  local_port = parse_port(local_port_hex)
208
210
  local_address = "#{local_ip}:#{local_port}"
209
211
  end
210
- connections << local_address
212
+ if state == :established
213
+ connections << local_address
214
+ else
215
+ close_wait_connections << local_address
216
+ end
211
217
  end
212
218
  end
213
219
  end
@@ -215,16 +221,26 @@ class IO
215
221
  # Count ESTABLISHED connections for each listener
216
222
  connections.each do |local_address|
217
223
  if listener_address = find_matching_listener(local_address, listeners)
218
- 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
219
235
  end
220
236
  end
221
237
 
222
238
  # /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
239
+ # sockets still in the accept queue. Those are already counted in queued_count on the
224
240
  # LISTEN row (same meaning as Raindrops inet_diag queued vs inode != 0 for active).
225
241
  listeners.each_value do |listener|
226
- backlog = listener.queue_size
227
- listener.active_connections = [listener.active_connections - backlog, 0].max
242
+ backlog = listener.queued_count
243
+ listener.active_count = [listener.active_count - backlog, 0].max
228
244
  end
229
245
 
230
246
  return listeners
@@ -263,13 +279,13 @@ class IO
263
279
 
264
280
  state = state_hex.to_i(16)
265
281
 
266
- listeners[path] ||= Listener.new(Addrinfo.unix(path), 0, 0)
282
+ listeners[path] ||= Listener.new(Addrinfo.unix(path), 0, 0, 0)
267
283
 
268
284
  case state
269
285
  when SS_CONNECTING # Queued connections
270
- listeners[path].queue_size += 1
286
+ listeners[path].queued_count += 1
271
287
  when SS_CONNECTED # Active connections
272
- listeners[path].active_connections += 1
288
+ listeners[path].active_count += 1
273
289
  end
274
290
  end
275
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.1"
10
+ VERSION = "0.3.0"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -14,6 +14,11 @@ 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
+
17
22
  ### v0.2.1
18
23
 
19
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.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## v0.2.1
4
9
 
5
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.
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.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file