visionary-sdk 0.4.0 → 0.5.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
- data/README.md +3 -0
- data/lib/visionary/schema/mysql/system.sql +11 -5
- data/lib/visionary/schema/postgres/system.sql +11 -5
- data/lib/visionary/schema/sqlite/system.sql +11 -5
- data/lib/visionary/schema.rb +4 -1
- data/lib/visionary/system/cgroup.rb +174 -0
- data/lib/visionary/system/recorder.rb +3 -0
- data/lib/visionary/version.rb +1 -1
- data/lib/visionary.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 694608f68ef27d1b7b5bbaff8ffbe6dc3ef784d542e2e48630c6a78be895a121
|
|
4
|
+
data.tar.gz: 575b22cc90763948b90c57d6cfe5aa78449033d35ae403b8bfd8703cbee5e3d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3eb21e752bfaa59d9213afe2c57ae54ac42a4e18490b6e32112bbbeeabc7a79eb3fd98828ef1945140f6940156adf114e4202646415e739a1bb3a6f589c8b8c2
|
|
7
|
+
data.tar.gz: d7d6a48dfdb652c1ca79f01eb3b2fe0a2a126e818382dd7160edd09a79535f4996b76f03cf1f86f439f227f54972de9145b827ec81c649af3d2f6a026e837f56
|
data/README.md
CHANGED
|
@@ -169,6 +169,9 @@ from `GC.stat`, heap from `GC.stat` slot and page counts, threads from
|
|
|
169
169
|
`/proc/self/status` (else `Thread.list`). Pool columns come from the
|
|
170
170
|
ActiveRecord pool when it exists, or from `c.db_stats = -> { { total:, idle:,
|
|
171
171
|
in_use:, max: } }`. On macOS and Windows most host columns are NULL.
|
|
172
|
+
In a container (or a cgroup with a CPU or memory limit) the `container_*`
|
|
173
|
+
columns come from the process's own cgroup, v2 or v1: CPU used, limit and
|
|
174
|
+
throttling, memory used (without inactive page cache) and limit, OOM kills.
|
|
172
175
|
|
|
173
176
|
## Configuration
|
|
174
177
|
|
|
@@ -66,11 +66,17 @@ CREATE TABLE IF NOT EXISTS system_metrics (
|
|
|
66
66
|
ctx_switches_ps DOUBLE,
|
|
67
67
|
proc_read_kbps DOUBLE,
|
|
68
68
|
proc_write_kbps DOUBLE,
|
|
69
|
-
proc_max_fds BIGINT
|
|
70
|
-
service VARCHAR(255),
|
|
71
|
-
version VARCHAR(255),
|
|
72
|
-
sdk VARCHAR(255),
|
|
73
|
-
runtime VARCHAR(255)
|
|
69
|
+
proc_max_fds BIGINT
|
|
70
|
+
service VARCHAR(255),
|
|
71
|
+
version VARCHAR(255),
|
|
72
|
+
sdk VARCHAR(255),
|
|
73
|
+
runtime VARCHAR(255),
|
|
74
|
+
container_cpu_cores DOUBLE,
|
|
75
|
+
container_cpu_limit DOUBLE,
|
|
76
|
+
container_cpu_throttled_percent DOUBLE,
|
|
77
|
+
container_mem_used_mb DOUBLE,
|
|
78
|
+
container_mem_limit_mb DOUBLE,
|
|
79
|
+
container_oom_kills BIGINT
|
|
74
80
|
) CHARACTER SET utf8mb4;
|
|
75
81
|
CREATE INDEX idx_system_metrics_created_at ON system_metrics (created_at);
|
|
76
82
|
CREATE INDEX idx_system_metrics_instance_created ON system_metrics (instance, created_at);
|
|
@@ -66,11 +66,17 @@ CREATE TABLE IF NOT EXISTS system_metrics (
|
|
|
66
66
|
ctx_switches_ps DOUBLE PRECISION,
|
|
67
67
|
proc_read_kbps DOUBLE PRECISION,
|
|
68
68
|
proc_write_kbps DOUBLE PRECISION,
|
|
69
|
-
proc_max_fds BIGINT
|
|
70
|
-
service TEXT,
|
|
71
|
-
version TEXT,
|
|
72
|
-
sdk TEXT,
|
|
73
|
-
runtime TEXT
|
|
69
|
+
proc_max_fds BIGINT
|
|
70
|
+
service TEXT,
|
|
71
|
+
version TEXT,
|
|
72
|
+
sdk TEXT,
|
|
73
|
+
runtime TEXT,
|
|
74
|
+
container_cpu_cores DOUBLE PRECISION,
|
|
75
|
+
container_cpu_limit DOUBLE PRECISION,
|
|
76
|
+
container_cpu_throttled_percent DOUBLE PRECISION,
|
|
77
|
+
container_mem_used_mb DOUBLE PRECISION,
|
|
78
|
+
container_mem_limit_mb DOUBLE PRECISION,
|
|
79
|
+
container_oom_kills BIGINT
|
|
74
80
|
);
|
|
75
81
|
CREATE INDEX IF NOT EXISTS idx_system_metrics_created_at ON system_metrics (created_at);
|
|
76
82
|
CREATE INDEX IF NOT EXISTS idx_system_metrics_instance_created ON system_metrics (instance, created_at);
|
|
@@ -66,11 +66,17 @@ CREATE TABLE IF NOT EXISTS system_metrics (
|
|
|
66
66
|
ctx_switches_ps REAL,
|
|
67
67
|
proc_read_kbps REAL,
|
|
68
68
|
proc_write_kbps REAL,
|
|
69
|
-
proc_max_fds INTEGER
|
|
70
|
-
service TEXT,
|
|
71
|
-
version TEXT,
|
|
72
|
-
sdk TEXT,
|
|
73
|
-
runtime TEXT
|
|
69
|
+
proc_max_fds INTEGER
|
|
70
|
+
service TEXT,
|
|
71
|
+
version TEXT,
|
|
72
|
+
sdk TEXT,
|
|
73
|
+
runtime TEXT,
|
|
74
|
+
container_cpu_cores REAL,
|
|
75
|
+
container_cpu_limit REAL,
|
|
76
|
+
container_cpu_throttled_percent REAL,
|
|
77
|
+
container_mem_used_mb REAL,
|
|
78
|
+
container_mem_limit_mb REAL,
|
|
79
|
+
container_oom_kills INTEGER
|
|
74
80
|
);
|
|
75
81
|
CREATE INDEX IF NOT EXISTS idx_system_metrics_created_at ON system_metrics (created_at);
|
|
76
82
|
CREATE INDEX IF NOT EXISTS idx_system_metrics_instance_created ON system_metrics (instance, created_at);
|
data/lib/visionary/schema.rb
CHANGED
|
@@ -47,7 +47,10 @@ module Visionary
|
|
|
47
47
|
%w[host_procs int], %w[procs_running int], %w[procs_blocked int],
|
|
48
48
|
%w[ctx_switches_ps real], %w[proc_read_kbps real], %w[proc_write_kbps real],
|
|
49
49
|
%w[proc_max_fds int], %w[service text], %w[version text],
|
|
50
|
-
%w[sdk text], %w[runtime text]
|
|
50
|
+
%w[sdk text], %w[runtime text],
|
|
51
|
+
%w[container_cpu_cores real], %w[container_cpu_limit real],
|
|
52
|
+
%w[container_cpu_throttled_percent real], %w[container_mem_used_mb real],
|
|
53
|
+
%w[container_mem_limit_mb real], %w[container_oom_kills int]
|
|
51
54
|
].freeze
|
|
52
55
|
|
|
53
56
|
SPAN_COLUMNS = %w[
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Visionary
|
|
4
|
+
module System
|
|
5
|
+
# Reads the CPU and memory limits and use of the process's own cgroup
|
|
6
|
+
# (Linux, v2 or v1): in a container these are the numbers that matter,
|
|
7
|
+
# since the host columns describe the whole machine underneath. The
|
|
8
|
+
# container_* columns are filled only in a container, or when the cgroup
|
|
9
|
+
# sets a CPU or memory limit (a systemd unit with MemoryMax=, say).
|
|
10
|
+
class Cgroup
|
|
11
|
+
MB = 1024.0 * 1024
|
|
12
|
+
MARKERS = %w[docker kubepods containerd libpod lxc].freeze
|
|
13
|
+
# Variables container platforms set in every container: Kubernetes,
|
|
14
|
+
# Railway, Render, Heroku, Cloud Run, ECS/Fargate and Azure Container Apps.
|
|
15
|
+
CONTAINER_ENV = %w[KUBERNETES_SERVICE_HOST RAILWAY_SERVICE_ID RENDER_SERVICE_ID DYNO K_SERVICE ECS_CONTAINER_METADATA_URI_V4 CONTAINER_APP_NAME].freeze
|
|
16
|
+
|
|
17
|
+
# The process's cgroup; nil off Linux or when there is none to read.
|
|
18
|
+
# root and env are for tests.
|
|
19
|
+
def self.detect(root: "/", env: ENV)
|
|
20
|
+
return nil unless Sources::LINUX || root != "/"
|
|
21
|
+
text = Sources.read(File.join(root, "proc/self/cgroup")) or return nil
|
|
22
|
+
c = new(root, text, env)
|
|
23
|
+
c.cpu_dir || c.mem_dir ? c : nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :cpu_dir, :mem_dir
|
|
27
|
+
|
|
28
|
+
def initialize(root, text, env)
|
|
29
|
+
@root = root
|
|
30
|
+
@in_ctr = in_container?(text, env)
|
|
31
|
+
@v2 = false
|
|
32
|
+
@cpu_dir = @cpu_acct = @mem_dir = nil
|
|
33
|
+
text.strip.each_line do |line|
|
|
34
|
+
id, ctrls, path = line.chomp.split(":", 3)
|
|
35
|
+
next if path.nil?
|
|
36
|
+
list = ctrls.split(",")
|
|
37
|
+
if id == "0" && ctrls.empty?
|
|
38
|
+
dir = dir_for("sys/fs/cgroup", path)
|
|
39
|
+
if File.exist?(File.join(dir, "cpu.stat"))
|
|
40
|
+
@v2 = true
|
|
41
|
+
@cpu_dir = @mem_dir = dir
|
|
42
|
+
break
|
|
43
|
+
end
|
|
44
|
+
elsif list.include?("memory")
|
|
45
|
+
@mem_dir = dir_for("sys/fs/cgroup/memory", path)
|
|
46
|
+
elsif list.include?("cpu") || list.include?("cpuacct")
|
|
47
|
+
dir = dir_for("sys/fs/cgroup/#{ctrls}", path)
|
|
48
|
+
@cpu_dir = dir if list.include?("cpu")
|
|
49
|
+
@cpu_acct = dir if list.include?("cpuacct")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
@primed = false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The baseline the first sample's deltas start from.
|
|
56
|
+
def prime
|
|
57
|
+
@usage, @periods, @throttled, @ooms = counters
|
|
58
|
+
@last = mono
|
|
59
|
+
@primed = true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Fills the container_* columns of v.
|
|
63
|
+
def sample(v)
|
|
64
|
+
cpus, mem_limit = limits
|
|
65
|
+
return if !@in_ctr && cpus.zero? && mem_limit.zero?
|
|
66
|
+
now = mono
|
|
67
|
+
usage, periods, throttled, ooms = counters
|
|
68
|
+
secs = @primed ? now - @last : 0
|
|
69
|
+
if secs.positive? && usage >= @usage
|
|
70
|
+
v["container_cpu_cores"] = ((usage - @usage) / 1e9 / secs).round(3)
|
|
71
|
+
if periods > @periods && throttled >= @throttled
|
|
72
|
+
v["container_cpu_throttled_percent"] = (100.0 * (throttled - @throttled) / (periods - @periods)).round(1)
|
|
73
|
+
elsif cpus.positive?
|
|
74
|
+
v["container_cpu_throttled_percent"] = 0.0
|
|
75
|
+
end
|
|
76
|
+
v["container_oom_kills"] = ooms - @ooms if ooms && @ooms && ooms >= @ooms
|
|
77
|
+
end
|
|
78
|
+
@usage, @periods, @throttled, @ooms = usage, periods, throttled, ooms
|
|
79
|
+
@last = now
|
|
80
|
+
@primed = true
|
|
81
|
+
v["container_cpu_limit"] = cpus.round(2) if cpus.positive?
|
|
82
|
+
if (used = mem_used)
|
|
83
|
+
v["container_mem_used_mb"] = (used / MB).round(1)
|
|
84
|
+
end
|
|
85
|
+
v["container_mem_limit_mb"] = (mem_limit / MB).round(1) if mem_limit.positive?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def mono = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
91
|
+
|
|
92
|
+
def path(*parts) = File.join(@root, *parts)
|
|
93
|
+
|
|
94
|
+
# The cgroup's directory under a controller's mount. Inside a
|
|
95
|
+
# container's cgroup namespace the path is "/", and with a private mount
|
|
96
|
+
# the host path does not exist below it, so the mount itself is the
|
|
97
|
+
# cgroup.
|
|
98
|
+
def dir_for(mount, cgroup_path)
|
|
99
|
+
dir = File.join(@root, mount, cgroup_path)
|
|
100
|
+
File.directory?(dir) ? dir : File.join(@root, mount)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Tells a container from a process on the host by the runtimes' marker
|
|
104
|
+
# files, the platforms' variables and the cgroup path.
|
|
105
|
+
def in_container?(text, env)
|
|
106
|
+
File.exist?(path(".dockerenv")) || File.exist?(path("run/.containerenv")) ||
|
|
107
|
+
CONTAINER_ENV.any? { |k| !env[k].to_s.empty? } || MARKERS.any? { |k| text.include?(k) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def read_int(file)
|
|
111
|
+
s = Sources.read(file)&.strip
|
|
112
|
+
s&.match?(/\A\d+\z/) ? s.to_i : nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# "key value" lines (cpu.stat, memory.stat, memory.events).
|
|
116
|
+
def read_kv(file)
|
|
117
|
+
out = {}
|
|
118
|
+
Sources.read(file)&.each_line do |line|
|
|
119
|
+
k, n, extra = line.split
|
|
120
|
+
out[k] = n.to_i if extra.nil? && n&.match?(/\A\d+\z/)
|
|
121
|
+
end
|
|
122
|
+
out
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The CPU limit in cores and the memory limit in bytes, 0 when
|
|
126
|
+
# unlimited.
|
|
127
|
+
def limits
|
|
128
|
+
cpus = 0.0
|
|
129
|
+
mem = 0
|
|
130
|
+
if @v2
|
|
131
|
+
q, p = Sources.read(File.join(@cpu_dir, "cpu.max"))&.split
|
|
132
|
+
cpus = q.to_f / p.to_f if q && q != "max" && p.to_f.positive?
|
|
133
|
+
mem = read_int(File.join(@mem_dir, "memory.max")) || 0 # "max": 0
|
|
134
|
+
return [cpus, mem]
|
|
135
|
+
end
|
|
136
|
+
if @cpu_dir
|
|
137
|
+
q = Sources.read(File.join(@cpu_dir, "cpu.cfs_quota_us")).to_s.strip.to_i
|
|
138
|
+
p = read_int(File.join(@cpu_dir, "cpu.cfs_period_us"))
|
|
139
|
+
cpus = q.to_f / p if q.positive? && p&.positive?
|
|
140
|
+
end
|
|
141
|
+
if @mem_dir
|
|
142
|
+
# v1 writes a page-rounded maximum int64 for no limit.
|
|
143
|
+
m = read_int(File.join(@mem_dir, "memory.limit_in_bytes"))
|
|
144
|
+
mem = m if m && m < 2**62
|
|
145
|
+
end
|
|
146
|
+
[cpus, mem]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The cumulative CPU time (ns), CFS periods, throttled periods and OOM
|
|
150
|
+
# kills (nil where the kernel doesn't count them).
|
|
151
|
+
def counters
|
|
152
|
+
if @v2
|
|
153
|
+
st = read_kv(File.join(@cpu_dir, "cpu.stat"))
|
|
154
|
+
ev = read_kv(File.join(@mem_dir, "memory.events"))
|
|
155
|
+
return [st["usage_usec"].to_i * 1000, st["nr_periods"].to_i, st["nr_throttled"].to_i, ev["oom_kill"]]
|
|
156
|
+
end
|
|
157
|
+
usage = @cpu_acct ? read_int(File.join(@cpu_acct, "cpuacct.usage")).to_i : 0
|
|
158
|
+
st = @cpu_dir ? read_kv(File.join(@cpu_dir, "cpu.stat")) : {}
|
|
159
|
+
ooms = @mem_dir ? read_kv(File.join(@mem_dir, "memory.oom_control"))["oom_kill"] : nil
|
|
160
|
+
[usage, st["nr_periods"].to_i, st["nr_throttled"].to_i, ooms]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Memory in use without the inactive page cache, as docker stats
|
|
164
|
+
# counts it.
|
|
165
|
+
def mem_used
|
|
166
|
+
return nil unless @mem_dir
|
|
167
|
+
file, key = @v2 ? %w[memory.current inactive_file] : %w[memory.usage_in_bytes total_inactive_file]
|
|
168
|
+
used = read_int(File.join(@mem_dir, file)) or return nil
|
|
169
|
+
inactive = read_kv(File.join(@mem_dir, "memory.stat"))[key]
|
|
170
|
+
inactive && inactive < used ? used - inactive : used
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -90,6 +90,7 @@ module Visionary
|
|
|
90
90
|
v["version"] = @cfg.version
|
|
91
91
|
v["sdk"] = "ruby"
|
|
92
92
|
v["runtime"] = Telemetry.runtime
|
|
93
|
+
@cgroup&.sample(v)
|
|
93
94
|
Schema::TABLES["system_metrics"].map { |c| v[c] }
|
|
94
95
|
end
|
|
95
96
|
|
|
@@ -119,6 +120,8 @@ module Visionary
|
|
|
119
120
|
@gc_ms_prev = heap[:gc_ms]
|
|
120
121
|
@db_prev = db_stats || {}
|
|
121
122
|
@prev = read_counters(stat, Sources::LINUX ? Sources.tcp : {})
|
|
123
|
+
@cgroup = Cgroup.detect # nil off Linux
|
|
124
|
+
@cgroup&.prime
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
def run
|
data/lib/visionary/version.rb
CHANGED
data/lib/visionary.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "visionary/queue"
|
|
|
11
11
|
require_relative "visionary/writer"
|
|
12
12
|
require_relative "visionary/retention"
|
|
13
13
|
require_relative "visionary/system/sources"
|
|
14
|
+
require_relative "visionary/system/cgroup"
|
|
14
15
|
require_relative "visionary/system/recorder"
|
|
15
16
|
require_relative "visionary/telemetry"
|
|
16
17
|
require_relative "visionary/telemetry/hooks"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: visionary-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- styropyr0
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
The Ruby collector for Visionary. A Rack middleware (with a Railtie for Rails)
|
|
@@ -49,6 +49,7 @@ files:
|
|
|
49
49
|
- lib/visionary/store/active_record.rb
|
|
50
50
|
- lib/visionary/store/d1.rb
|
|
51
51
|
- lib/visionary/store/sql.rb
|
|
52
|
+
- lib/visionary/system/cgroup.rb
|
|
52
53
|
- lib/visionary/system/recorder.rb
|
|
53
54
|
- lib/visionary/system/sources.rb
|
|
54
55
|
- lib/visionary/telemetry.rb
|