io-metrics 0.4.0 → 0.4.1

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: ef189b6c38ef215409837bfe6d596116856ee48177fe67c63f6926f32e58e611
4
- data.tar.gz: a62a323790464b1ef965cfbf1ff1aba63e329e025f0f3b0b93696a809aa7095a
3
+ metadata.gz: db027cfe8e839feaf2916d7359836f34e182d899d69441abea2c88098af5b3be
4
+ data.tar.gz: 81f6b5d0c4569e3110b64bfdee6e7c0ebdc5b6eb1687c0d0980b5e09c449dd72
5
5
  SHA512:
6
- metadata.gz: b763b526aa7e8e82428206ba4b0a7d652fa3dd89ba4f3d8a0db7677af8c4e19171d459d6fd750f0b2f43aa3ac99461821c6f6fae61f1ac9c5024a569bb4af6ec
7
- data.tar.gz: 79e05df4f9d528c29cc1030c59623efeb6b3147f029e6361133e1a5e0660fc9a46a78d8ff7baa5026bc2db892a8ac29bbfe99b6a310314bfaa96a84fd144e02b
6
+ metadata.gz: 97cbf5f85aa49ccdb0da457b101e4375615f3ba46ca7f7b7a0366d4dded4b326de500080fb2d31583cf71157ad4758239b592dd87e7fb71bebd438af94f257d6
7
+ data.tar.gz: fba22c314ebb96e5f0ba624e1d37e9c9012555ce3c66fb842b57f433f2ae9d9fbb321ef303e5caee46155b9501818932821d9578e8a4909bd9f3e0e732b0077a
checksums.yaml.gz.sig CHANGED
@@ -1,5 +1 @@
1
- ��+%���xH@.�o�}[L�$>r�e<�ƺW%�$�x�� FǷI���h��r�݁!�d�tRxqrD(���+�~;����2�/?~ܹ�y��(�;py1zD�� +&+�Z��M��_2
2
- ��K��+-�
3
- ��\ε�n}��abX ��5��b2���^X����� |��<�}T����+bV��[��?s}`��%W�F۪�`ͦfJ=xsw�����~8�<ߙ���l��^L'T
4
- �_�R
5
- 0Uq=����7�Ԫk=���5R
1
+ &�VC��|%���Dٹ�8��� .l���Z$��^�AH��=��wpА�'ES��چ!vB_UL1ab
@@ -17,7 +17,8 @@ class IO
17
17
  end
18
18
 
19
19
  # Parse an address from netstat format to Addrinfo (TCP, numeric port).
20
- # @parameter address [String] Address string from netstat, e.g. "127.0.0.1.50876" or "*.63703".
20
+ # @parameter address [String] Address string from netstat, e.g. "127.0.0.1.50876", "*.63703",
21
+ # "[::1].8080", or "::1.8080" (bare IPv6, no brackets, as macOS netstat outputs).
21
22
  # @returns [Addrinfo | Nil] Addrinfo for the listener, or nil if the line cannot be parsed.
22
23
  def self.parse_address(address)
23
24
  # Handle wildcard addresses: *.port -> 0.0.0.0:port
@@ -26,20 +27,28 @@ class IO
26
27
  return Addrinfo.tcp("0.0.0.0", port)
27
28
  end
28
29
 
29
- # Handle IPv4 addresses: ip.port -> ip:port
30
- if address =~ /^([0-9.]+)\.(\d+)$/
31
- ip = $1
32
- port = $2.to_i
33
- return Addrinfo.tcp(ip, port)
34
- end
35
-
36
- # Handle IPv6: [::1].8080, [fe80::1%lo0].8080, [::].8080
30
+ # Handle bracketed IPv6: [::1].8080, [fe80::1%lo0].8080, [::].8080
37
31
  if address =~ /\A\[([^\]]+)\]\.(\d+)\z/
38
32
  ip = $1.sub(/%.*\z/, "") # strip zone ID (e.g. %lo0)
39
33
  port = $2.to_i
40
34
  return Addrinfo.tcp(ip, port)
41
35
  end
42
36
 
37
+ # Split at the last dot; everything after must be a numeric port.
38
+ # This handles both IPv4 (127.0.0.1.PORT) and bare IPv6 (::1.PORT, fe80::1%lo0.PORT).
39
+ if (dot_idx = address.rindex(".")) && address[dot_idx + 1..].match?(/\A\d+\z/)
40
+ ip = address[0, dot_idx]
41
+ port = address[dot_idx + 1..].to_i
42
+
43
+ if ip.include?(":")
44
+ # IPv6: strip zone ID (e.g. fe80::1%lo0 → fe80::1)
45
+ ip = ip.sub(/%.*\z/, "")
46
+ return Addrinfo.tcp(ip, port)
47
+ else
48
+ return Addrinfo.tcp(ip, port)
49
+ end
50
+ end
51
+
43
52
  nil
44
53
  end
45
54
 
@@ -52,47 +61,54 @@ class IO
52
61
  end
53
62
  end
54
63
 
55
- # Parse netstat -L output and extract listener statistics.
64
+ # Parse a single netstat -L output stream and accumulate into +listeners+.
65
+ # @parameter io [IO] Open pipe from netstat.
66
+ # @parameter listeners [Hash] Accumulator keyed by listener address string.
67
+ # @parameter address_filter [Set | Nil] Optional downcased address filter.
68
+ def self.parse_netstat_output(io, listeners, address_filter)
69
+ io.each_line do |line|
70
+ next if line.start_with?("Current") || line.start_with?("Listen") || line.strip.empty?
71
+
72
+ # Format: "queue_length/incomplete_queue_length/maximum_queue_length Local Address"
73
+ fields = line.split(/\s+/)
74
+ next if fields.size < 2
75
+
76
+ queue_statistics = fields[0]
77
+ local_address_raw = fields[1]
78
+
79
+ # Parse queue statistics: "queue_length/incomplete_queue_length/maximum_queue_length"
80
+ next unless queue_statistics =~ /^(\d+)\/(\d+)\/(\d+)$/
81
+ queue_length = $1.to_i
82
+ # incomplete_queue_length = $2.to_i # incomplete connections (SYN_RECV)
83
+ # maximum_queue_length = $3.to_i # maximum queue size
84
+
85
+ addrinfo = parse_address(local_address_raw)
86
+ next unless addrinfo
87
+
88
+ key = tcp_listener_key(addrinfo)
89
+ next if address_filter && !address_filter.include?(key.downcase)
90
+
91
+ listeners[key] ||= Listener.new(addrinfo, 0, 0, 0, 0, 0)
92
+ # Accumulate rather than overwrite: macOS shows both IPv4 and IPv6 wildcard
93
+ # sockets as "*.PORT", so multiple LISTEN rows can share the same key.
94
+ listeners[key].queued_count += queue_length
95
+ # active_count and close_wait_count are 0 (netstat -L doesn't expose connection states)
96
+ listeners[key].active_count = 0
97
+ listeners[key].close_wait_count = 0
98
+ end
99
+ end
100
+
101
+ # Parse netstat -L output and extract listener statistics for IPv4 and IPv6.
56
102
  # @parameter addresses [Array(String) | Nil] Optional filter for specific addresses.
57
103
  # @returns [Array(Listener)] One entry per listening socket reported by netstat.
58
104
  def self.capture_tcp(addresses = nil)
59
105
  listeners = {}
60
106
  address_filter = addresses ? addresses.map{|address| address.downcase}.to_set : nil
61
107
 
108
+ # A single `netstat -L -an -p tcp` invocation reports both IPv4 and IPv6
109
+ # listeners on macOS — no separate tcp6 pass is needed.
62
110
  IO.popen([NETSTAT, "-L", "-an", "-p", "tcp"], "r") do |io|
63
- # Skip header lines
64
- io.each_line do |line|
65
- # Skip header and empty lines
66
- next if line.start_with?("Current") || line.start_with?("Listen") || line.strip.empty?
67
-
68
- # Format: "queue_length/incomplete_queue_length/maximum_queue_length Local Address"
69
- fields = line.split(/\s+/)
70
- next if fields.size < 2
71
-
72
- queue_statistics = fields[0]
73
- local_address_raw = fields[1]
74
-
75
- # Parse queue statistics: "queue_length/incomplete_queue_length/maximum_queue_length"
76
- if queue_statistics =~ /^(\d+)\/(\d+)\/(\d+)$/
77
- queue_length = $1.to_i
78
- # incomplete_queue_length = $2.to_i # incomplete connections (SYN_RECV)
79
- # maximum_queue_length = $3.to_i # maximum queue size
80
-
81
- addrinfo = parse_address(local_address_raw)
82
- next unless addrinfo
83
-
84
- key = tcp_listener_key(addrinfo)
85
- # Apply filter if specified
86
- next if address_filter && !address_filter.include?(key.downcase)
87
-
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
94
- end
95
- end
111
+ parse_netstat_output(io, listeners, address_filter)
96
112
  end
97
113
 
98
114
  return listeners.values
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Metrics
10
- VERSION = "0.4.0"
10
+ VERSION = "0.4.1"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -14,6 +14,10 @@ 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.1
18
+
19
+ - Fix parsing of IPv6 addresses on Darwin.
20
+
17
21
  ### v0.4.0
18
22
 
19
23
  - 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`.
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.4.1
4
+
5
+ - Fix parsing of IPv6 addresses on Darwin.
6
+
3
7
  ## v0.4.0
4
8
 
5
9
  - 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`.
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.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file