io-metrics 0.1.0 → 0.2.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/io/metrics/listener/darwin.rb +36 -26
- data/lib/io/metrics/listener/linux.rb +27 -19
- data/lib/io/metrics/listener.rb +14 -6
- data/lib/io/metrics/version.rb +1 -1
- data/readme.md +25 -0
- data/releases.md +6 -0
- data.tar.gz.sig +0 -0
- metadata +4 -3
- 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: d3e42882ce221e2e9f10808ed4de4ea6a72b3490662a014727e40b49fe8826ab
|
|
4
|
+
data.tar.gz: da5595f41dfe59f91dc3355ebe6538bac4ca0bc70e1c1040c6cc6e911c977498
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cee9a635c1df4b7bfc18497e835add923739f5656737126bdc86cae1bca17a8074925243240eed1b802f741e8956d63b18c434af7f8d29853e3750aa3f55b5f7
|
|
7
|
+
data.tar.gz: 512a4e960252f726b0a50348d7ceab9f6bbc13247aab649f17a48d9d3d1b58cf489141b9095b24cebabd0d39546196a5bf454e3e8f51389b652b485e16218a6b
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require "socket"
|
|
7
|
+
|
|
6
8
|
class IO
|
|
7
9
|
module Metrics
|
|
8
10
|
# Darwin (macOS) implementation of listener statistics using netstat -L.
|
|
@@ -14,31 +16,43 @@ class IO
|
|
|
14
16
|
File.executable?(NETSTAT)
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
# Parse an address from netstat format to
|
|
19
|
+
# Parse an address from netstat format to Addrinfo (TCP, numeric port).
|
|
18
20
|
# @parameter address [String] Address string from netstat, e.g. "127.0.0.1.50876" or "*.63703".
|
|
19
|
-
# @returns [
|
|
21
|
+
# @returns [Addrinfo | Nil] Addrinfo for the listener, or nil if the line cannot be parsed.
|
|
20
22
|
def self.parse_address(address)
|
|
21
23
|
# Handle wildcard addresses: *.port -> 0.0.0.0:port
|
|
22
24
|
if address.start_with?("*.")
|
|
23
|
-
port = address[2..-1]
|
|
24
|
-
return "0.0.0.0
|
|
25
|
+
port = address[2..-1].to_i
|
|
26
|
+
return Addrinfo.tcp("0.0.0.0", port)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
# Handle IPv4 addresses: ip.port -> ip:port
|
|
28
30
|
if address =~ /^([0-9.]+)\.(\d+)$/
|
|
29
31
|
ip = $1
|
|
30
|
-
port = $2
|
|
31
|
-
return
|
|
32
|
+
port = $2.to_i
|
|
33
|
+
return Addrinfo.tcp(ip, port)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
# Handle IPv6
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
# Handle IPv6 or other formats: best-effort via Addrinfo.parse
|
|
37
|
+
begin
|
|
38
|
+
Addrinfo.parse(address)
|
|
39
|
+
rescue ArgumentError, SocketError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Build a stable string key for TCP listener filter matching (same style as Linux / user filters).
|
|
45
|
+
def self.tcp_listener_key(addrinfo)
|
|
46
|
+
if addrinfo.ipv6?
|
|
47
|
+
"[#{addrinfo.ip_address}]:#{addrinfo.ip_port}"
|
|
48
|
+
else
|
|
49
|
+
"#{addrinfo.ip_address}:#{addrinfo.ip_port}"
|
|
50
|
+
end
|
|
37
51
|
end
|
|
38
52
|
|
|
39
53
|
# Parse netstat -L output and extract listener statistics.
|
|
40
54
|
# @parameter addresses [Array(String) | Nil] Optional filter for specific addresses.
|
|
41
|
-
# @returns [
|
|
55
|
+
# @returns [Array(Listener)] One entry per listening socket reported by netstat.
|
|
42
56
|
def self.capture_tcp(addresses = nil)
|
|
43
57
|
listeners = {}
|
|
44
58
|
address_filter = addresses ? addresses.map{|address| address.downcase}.to_set : nil
|
|
@@ -62,36 +76,32 @@ class IO
|
|
|
62
76
|
# incomplete_queue_length = $2.to_i # incomplete connections (SYN_RECV)
|
|
63
77
|
# maximum_queue_length = $3.to_i # maximum queue size
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
addrinfo = parse_address(local_address_raw)
|
|
80
|
+
next unless addrinfo
|
|
67
81
|
|
|
82
|
+
key = tcp_listener_key(addrinfo)
|
|
68
83
|
# Apply filter if specified
|
|
69
|
-
next if address_filter && !address_filter.include?(
|
|
84
|
+
next if address_filter && !address_filter.include?(key.downcase)
|
|
70
85
|
|
|
71
|
-
listeners[
|
|
72
|
-
listeners[
|
|
86
|
+
listeners[key] ||= Listener.new(addrinfo, 0, 0)
|
|
87
|
+
listeners[key].queue_size = queue_length
|
|
73
88
|
# active_connections set to 0 (can't reliably count per listener)
|
|
74
|
-
listeners[
|
|
89
|
+
listeners[key].active_connections = 0
|
|
75
90
|
end
|
|
76
91
|
end
|
|
77
92
|
end
|
|
78
93
|
|
|
79
|
-
return listeners
|
|
94
|
+
return listeners.values
|
|
80
95
|
rescue Errno::ENOENT, Errno::EACCES
|
|
81
|
-
return
|
|
96
|
+
return []
|
|
82
97
|
end
|
|
83
98
|
|
|
84
99
|
# Capture listener listeners for TCP sockets.
|
|
85
100
|
# @parameter addresses [Array(String) | Nil] TCP address(es) to capture, e.g. ["0.0.0.0:80"]. If nil, captures all.
|
|
86
101
|
# @parameter paths [Array(String) | Nil] Unix socket path(s) to capture (not supported on Darwin).
|
|
87
|
-
# @returns [
|
|
102
|
+
# @returns [Array(Listener)] TCP listeners from netstat.
|
|
88
103
|
def self.capture(addresses: nil, paths: nil)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# Capture TCP listeners (Unix sockets not supported on Darwin via netstat)
|
|
92
|
-
listeners.merge!(capture_tcp(addresses))
|
|
93
|
-
|
|
94
|
-
return listeners
|
|
104
|
+
capture_tcp(addresses)
|
|
95
105
|
end
|
|
96
106
|
end
|
|
97
107
|
end
|
|
@@ -109,7 +119,7 @@ if IO::Metrics::Listener::Darwin.supported?
|
|
|
109
119
|
# Capture listener listeners for the given address(es).
|
|
110
120
|
# @parameter addresses [Array(String) | Nil] TCP address(es) to capture, e.g. ["0.0.0.0:80"]. If nil, captures all listening TCP sockets.
|
|
111
121
|
# @parameter paths [Array(String) | Nil] Unix socket path(s) to capture (not supported on Darwin).
|
|
112
|
-
# @returns [
|
|
122
|
+
# @returns [Array(Listener) | Nil] Captured listeners, or nil if not supported.
|
|
113
123
|
def capture(**options)
|
|
114
124
|
IO::Metrics::Listener::Darwin.capture(**options)
|
|
115
125
|
end
|
|
@@ -103,7 +103,7 @@ class IO
|
|
|
103
103
|
|
|
104
104
|
# Find the best matching listener for an ESTABLISHED connection.
|
|
105
105
|
# @parameter local_address [String] Local address in "ip:port" or "[ipv6]:port" format.
|
|
106
|
-
# @parameter listeners [Hash(String, Listener)]
|
|
106
|
+
# @parameter listeners [Hash(String, Listener)] Internal map from display address string to Listener.
|
|
107
107
|
# @returns [String | Nil] The address of the matching listener, or nil if no match.
|
|
108
108
|
def self.find_matching_listener(local_address, listeners)
|
|
109
109
|
# Try exact match first
|
|
@@ -149,8 +149,15 @@ class IO
|
|
|
149
149
|
# @parameter file [String] Path to /proc/net/tcp or /proc/net/tcp6.
|
|
150
150
|
# @parameter addresses [Array(String) | Nil] Optional filter for specific addresses.
|
|
151
151
|
# @parameter ipv6 [Boolean] Whether parsing IPv6 addresses.
|
|
152
|
-
# @returns [
|
|
152
|
+
# @returns [Array(Listener)] One entry per listening socket.
|
|
153
153
|
def self.capture_tcp_file(file, addresses = nil, ipv6: false)
|
|
154
|
+
gather_tcp_file(file, addresses, ipv6: ipv6).values
|
|
155
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
156
|
+
return []
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Internal: same as capture_tcp_file but returns a Hash keyed by display address for merging and connection matching.
|
|
160
|
+
def self.gather_tcp_file(file, addresses = nil, ipv6: false)
|
|
154
161
|
listeners = {}
|
|
155
162
|
address_filter = addresses ? addresses.map{|address| address.downcase}.to_set : nil
|
|
156
163
|
connections = []
|
|
@@ -174,7 +181,8 @@ class IO
|
|
|
174
181
|
if state == :listen
|
|
175
182
|
if ipv6
|
|
176
183
|
local_ip = parse_ipv6(local_ip_hex)
|
|
177
|
-
|
|
184
|
+
local_port = parse_port(local_port_hex)
|
|
185
|
+
local_address = "[#{local_ip}]:#{local_port}"
|
|
178
186
|
else
|
|
179
187
|
local_ip = parse_ipv4(local_ip_hex)
|
|
180
188
|
local_port = parse_port(local_port_hex)
|
|
@@ -182,9 +190,9 @@ class IO
|
|
|
182
190
|
end
|
|
183
191
|
|
|
184
192
|
# Apply filter if specified
|
|
185
|
-
next if address_filter && !address_filter.include?(local_address)
|
|
193
|
+
next if address_filter && !address_filter.include?(local_address.downcase)
|
|
186
194
|
|
|
187
|
-
listeners[local_address] ||= Listener.
|
|
195
|
+
listeners[local_address] ||= Listener.new(Addrinfo.tcp(local_ip, local_port), 0, 0)
|
|
188
196
|
# rx_queue shows number of connections waiting to be accepted
|
|
189
197
|
listeners[local_address].queue_size = rx_queue_hex.to_i(16)
|
|
190
198
|
listeners[local_address].active_connections = 0
|
|
@@ -218,7 +226,7 @@ class IO
|
|
|
218
226
|
# Parse /proc/net/unix and extract listener statistics for Unix domain sockets.
|
|
219
227
|
# @parameter paths [Array(String) | Nil] Optional filter for specific socket paths.
|
|
220
228
|
# @parameter file [String] Optional path to Unix socket file (defaults to "/proc/net/unix").
|
|
221
|
-
# @returns [
|
|
229
|
+
# @returns [Array(Listener)] One entry per socket path with any matching activity.
|
|
222
230
|
def self.capture_unix(paths = nil, file: "/proc/net/unix")
|
|
223
231
|
listeners = {}
|
|
224
232
|
path_filter = paths ? paths.to_set : nil
|
|
@@ -246,7 +254,7 @@ class IO
|
|
|
246
254
|
|
|
247
255
|
state = state_hex.to_i(16)
|
|
248
256
|
|
|
249
|
-
listeners[path] ||= Listener.
|
|
257
|
+
listeners[path] ||= Listener.new(Addrinfo.unix(path), 0, 0)
|
|
250
258
|
|
|
251
259
|
case state
|
|
252
260
|
when SS_CONNECTING # Queued connections
|
|
@@ -256,37 +264,37 @@ class IO
|
|
|
256
264
|
end
|
|
257
265
|
end
|
|
258
266
|
|
|
259
|
-
return listeners
|
|
267
|
+
return listeners.values
|
|
260
268
|
rescue Errno::ENOENT, Errno::EACCES
|
|
261
|
-
return
|
|
269
|
+
return []
|
|
262
270
|
end
|
|
263
271
|
|
|
264
272
|
# Parse /proc/net/tcp and /proc/net/tcp6 and extract listener statistics.
|
|
265
273
|
# @parameter addresses [Array(String) | Nil] Optional filter for specific addresses.
|
|
266
|
-
# @returns [
|
|
274
|
+
# @returns [Array(Listener)] TCP listeners from both stacks.
|
|
267
275
|
def self.capture_tcp(addresses = nil)
|
|
268
276
|
listeners = {}
|
|
269
277
|
|
|
270
278
|
# Capture IPv4 listeners and connections
|
|
271
279
|
if File.readable?("/proc/net/tcp")
|
|
272
|
-
listeners.merge!(
|
|
280
|
+
listeners.merge!(gather_tcp_file("/proc/net/tcp", addresses, ipv6: false))
|
|
273
281
|
end
|
|
274
282
|
|
|
275
283
|
# Capture IPv6 listeners and connections
|
|
276
284
|
if File.readable?("/proc/net/tcp6")
|
|
277
|
-
listeners.merge!(
|
|
285
|
+
listeners.merge!(gather_tcp_file("/proc/net/tcp6", addresses, ipv6: true))
|
|
278
286
|
end
|
|
279
287
|
|
|
280
|
-
return listeners
|
|
288
|
+
return listeners.values
|
|
281
289
|
end
|
|
282
290
|
|
|
283
291
|
# Capture listener listeners for TCP and/or Unix domain sockets.
|
|
284
292
|
# @parameter addresses [Array(String) | Nil] TCP address(es) to capture, e.g. ["0.0.0.0:80"]. If nil and paths is nil, captures all. If nil but paths specified, captures none.
|
|
285
293
|
# @parameter paths [Array(String) | Nil] Unix socket path(s) to capture. If nil and addresses is nil, captures all. If nil but addresses specified, captures none.
|
|
286
294
|
# @parameter unix_file [String] Optional path to Unix socket file (defaults to "/proc/net/unix").
|
|
287
|
-
# @returns [
|
|
295
|
+
# @returns [Array(Listener)] All matching listeners (TCP and/or Unix).
|
|
288
296
|
def self.capture(addresses: nil, paths: nil, unix_file: "/proc/net/unix")
|
|
289
|
-
|
|
297
|
+
result = []
|
|
290
298
|
|
|
291
299
|
# If addresses are specified but paths is nil, don't capture Unix sockets
|
|
292
300
|
# Only capture Unix sockets if paths is explicitly provided or addresses is nil
|
|
@@ -294,12 +302,12 @@ class IO
|
|
|
294
302
|
unix_paths = paths.nil? && !addresses.nil? ? :skip : paths
|
|
295
303
|
|
|
296
304
|
# Capture TCP listeners (only if not skipped)
|
|
297
|
-
|
|
305
|
+
result.concat(capture_tcp(tcp_addresses)) unless tcp_addresses == :skip
|
|
298
306
|
|
|
299
307
|
# Capture Unix domain socket listeners (only if not skipped)
|
|
300
|
-
|
|
308
|
+
result.concat(capture_unix(unix_paths, file: unix_file)) unless unix_paths == :skip
|
|
301
309
|
|
|
302
|
-
return
|
|
310
|
+
return result
|
|
303
311
|
end
|
|
304
312
|
end
|
|
305
313
|
end
|
|
@@ -317,7 +325,7 @@ if IO::Metrics::Listener::Linux.supported?
|
|
|
317
325
|
# Capture listener listeners for the given address(es).
|
|
318
326
|
# @parameter addresses [Array(String) | Nil] TCP address(es) to capture, e.g. ["0.0.0.0:80"]. If nil, captures all listening TCP sockets.
|
|
319
327
|
# @parameter paths [Array(String) | Nil] Unix socket path(s) to capture. If nil and addresses is nil, captures all. If nil but addresses specified, captures none.
|
|
320
|
-
# @returns [
|
|
328
|
+
# @returns [Array(Listener) | Nil] Captured listeners, or nil if not supported.
|
|
321
329
|
def capture(**options)
|
|
322
330
|
IO::Metrics::Listener::Linux.capture(**options)
|
|
323
331
|
end
|
data/lib/io/metrics/listener.rb
CHANGED
|
@@ -8,20 +8,28 @@ require "json"
|
|
|
8
8
|
class IO
|
|
9
9
|
module Metrics
|
|
10
10
|
# Represents a network listener socket with its queue statistics.
|
|
11
|
+
# @attribute address [Addrinfo | Nil] Listening endpoint from capture; nil only for {Listener.zero} placeholders.
|
|
11
12
|
# @attribute queue_size [Integer] Number of connections waiting to be accepted (queued).
|
|
12
13
|
# @attribute active_connections [Integer] Number of active connections (already accepted).
|
|
13
|
-
class Listener < Struct.new(:queue_size, :active_connections)
|
|
14
|
-
|
|
14
|
+
class Listener < Struct.new(:address, :queue_size, :active_connections)
|
|
15
|
+
# Serialize for JSON; address uses Addrinfo#inspect_sockaddr.
|
|
16
|
+
def as_json(*)
|
|
17
|
+
{
|
|
18
|
+
address: address&.inspect_sockaddr,
|
|
19
|
+
queue_size: queue_size,
|
|
20
|
+
active_connections: active_connections,
|
|
21
|
+
}
|
|
22
|
+
end
|
|
15
23
|
|
|
16
24
|
# Convert the object to a JSON string.
|
|
17
25
|
def to_json(*arguments)
|
|
18
26
|
as_json.to_json(*arguments)
|
|
19
27
|
end
|
|
20
28
|
|
|
21
|
-
# Create a zero-initialized Listener instance.
|
|
22
|
-
# @returns [Listener]
|
|
29
|
+
# Create a zero-initialized Listener instance (no endpoint; for tests or templates).
|
|
30
|
+
# @returns [Listener] Counters zero; {#address} is nil.
|
|
23
31
|
def self.zero
|
|
24
|
-
|
|
32
|
+
new(nil, 0, 0)
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
# Whether listener stats can be captured on this system.
|
|
@@ -32,7 +40,7 @@ class IO
|
|
|
32
40
|
# Capture listener stats for the given address(es).
|
|
33
41
|
# @parameter addresses [Array(String) | Nil] TCP address(es) to capture, e.g. ["0.0.0.0:80"]. If nil, captures all listening TCP sockets.
|
|
34
42
|
# @parameter paths [Array(String) | Nil] Unix socket path(s) to capture. If nil and addresses is nil, captures all. If nil but addresses specified, captures none.
|
|
35
|
-
# @returns [
|
|
43
|
+
# @returns [Array(Listener) | Nil] Captured listeners, or nil if not supported.
|
|
36
44
|
def self.capture(**options)
|
|
37
45
|
return nil
|
|
38
46
|
end
|
data/lib/io/metrics/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -10,6 +10,15 @@ Please see the [project documentation](https://socketry.github.io/io-metrics/) f
|
|
|
10
10
|
|
|
11
11
|
- [Getting Started](https://socketry.github.io/io-metrics/guides/getting-started/index) - This guide explains how to use `io-metrics` to capture listener queue statistics from the host operating system.
|
|
12
12
|
|
|
13
|
+
## Releases
|
|
14
|
+
|
|
15
|
+
Please see the [project releases](https://socketry.github.io/io-metrics/releases/index) for all releases.
|
|
16
|
+
|
|
17
|
+
### v0.2.0
|
|
18
|
+
|
|
19
|
+
- **Breaking** `IO::Metrics::Listener.capture` returns an `Array` of `Listener` rows instead of a `Hash` keyed by address string.
|
|
20
|
+
- Each `Listener` has `address` (`Addrinfo` for TCP or Unix), `queue_size`, and `active_connections`. `Listener.zero` sets `address` to `nil`. JSON uses `Addrinfo#inspect_sockaddr` for `address`, or `null` when absent.
|
|
21
|
+
|
|
13
22
|
## Contributing
|
|
14
23
|
|
|
15
24
|
We welcome contributions to this project.
|
|
@@ -20,6 +29,22 @@ We welcome contributions to this project.
|
|
|
20
29
|
4. Push to the branch (`git push origin my-new-feature`).
|
|
21
30
|
5. Create new Pull Request.
|
|
22
31
|
|
|
32
|
+
### Running Tests
|
|
33
|
+
|
|
34
|
+
To run the test suite:
|
|
35
|
+
|
|
36
|
+
``` shell
|
|
37
|
+
bundle exec sus
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Making Releases
|
|
41
|
+
|
|
42
|
+
To make a new release:
|
|
43
|
+
|
|
44
|
+
``` shell
|
|
45
|
+
bundle exec bake gem:release:patch # or minor or major
|
|
46
|
+
```
|
|
47
|
+
|
|
23
48
|
### Developer Certificate of Origin
|
|
24
49
|
|
|
25
50
|
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
data/releases.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Releases
|
|
2
|
+
|
|
3
|
+
## v0.2.0
|
|
4
|
+
|
|
5
|
+
- **Breaking** `IO::Metrics::Listener.capture` returns an `Array` of `Listener` rows instead of a `Hash` keyed by address string.
|
|
6
|
+
- Each `Listener` has `address` (`Addrinfo` for TCP or Unix), `queue_size`, and `active_connections`. `Listener.zero` sets `address` to `nil`. JSON uses `Addrinfo#inspect_sockaddr` for `address`, or `null` when absent.
|
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
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -77,6 +77,7 @@ files:
|
|
|
77
77
|
- lib/io/metrics/version.rb
|
|
78
78
|
- license.md
|
|
79
79
|
- readme.md
|
|
80
|
+
- releases.md
|
|
80
81
|
homepage: https://github.com/socketry/io-metrics
|
|
81
82
|
licenses:
|
|
82
83
|
- MIT
|
|
@@ -91,14 +92,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
91
92
|
requirements:
|
|
92
93
|
- - ">="
|
|
93
94
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '3.
|
|
95
|
+
version: '3.3'
|
|
95
96
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
97
|
requirements:
|
|
97
98
|
- - ">="
|
|
98
99
|
- !ruby/object:Gem::Version
|
|
99
100
|
version: '0'
|
|
100
101
|
requirements: []
|
|
101
|
-
rubygems_version: 4.0.
|
|
102
|
+
rubygems_version: 4.0.6
|
|
102
103
|
specification_version: 4
|
|
103
104
|
summary: Extract I/O metrics from the host system.
|
|
104
105
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|