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 +4 -4
- checksums.yaml.gz.sig +1 -5
- data/lib/io/metrics/listener/darwin.rb +59 -43
- data/lib/io/metrics/version.rb +1 -1
- data/readme.md +4 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +1 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db027cfe8e839feaf2916d7359836f34e182d899d69441abea2c88098af5b3be
|
|
4
|
+
data.tar.gz: 81f6b5d0c4569e3110b64bfdee6e7c0ebdc5b6eb1687c0d0980b5e09c449dd72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97cbf5f85aa49ccdb0da457b101e4375615f3ba46ca7f7b7a0366d4dded4b326de500080fb2d31583cf71157ad4758239b592dd87e7fb71bebd438af94f257d6
|
|
7
|
+
data.tar.gz: fba22c314ebb96e5f0ba624e1d37e9c9012555ce3c66fb842b57f433f2ae9d9fbb321ef303e5caee46155b9501818932821d9578e8a4909bd9f3e0e732b0077a
|
checksums.yaml.gz.sig
CHANGED
|
@@ -1,5 +1 @@
|
|
|
1
|
-
|
|
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
|
+
&�V�C��|%���Dٹ�8��� .l���Z$��^�AH��=��wpА�'�ES��چ!vB�_UL1a�b
|
|
@@ -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"
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/io/metrics/version.rb
CHANGED
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
metadata.gz.sig
CHANGED
|
Binary file
|