munin_passenger 1.0.0 → 1.1.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
- data/Vagrantfile +2 -2
- data/lib/munin_passenger/collect.rb +88 -3
- data/lib/munin_passenger/graphs.rb +36 -24
- data/lib/munin_passenger/version.rb +1 -1
- data/munin_passenger.gemspec +1 -0
- data/spec/munin_passenger_spec.rb +48 -48
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e199f4682f3a32c4c4dc201db8bfee7bdf35c95265b404bcb6795649350f5f8
|
4
|
+
data.tar.gz: 9a9416d20a210a07462ac2bf9ceef5a83c30937245aba39d7ec025cddfd7b8d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a7678599187c76596d5fdad01a9595600bae3fdfdee7e9ac57ace8e9c88673e7401bf984945e2c05639190d6a65fadf5c381776ca23e7cd0b3d189521cdccb9
|
7
|
+
data.tar.gz: 1c70eaaa7f5ae9b8a4986b8bf924f21168c476274433c653da5c127ea017050675eff48e1a17643645e14508a57b643add5ebb3058d71a94fbd63a7adeeeae4f
|
data/Vagrantfile
CHANGED
@@ -129,7 +129,7 @@ server {
|
|
129
129
|
}
|
130
130
|
EOF
|
131
131
|
sudo rm -f /etc/nginx/sites-enabled/default
|
132
|
-
sudo ln -
|
132
|
+
sudo ln -sf /etc/nginx/sites-available/rails /etc/nginx/sites-enabled/
|
133
133
|
|
134
134
|
# munin
|
135
135
|
sudo add-apt-repository -y ppa:hawq/munin
|
@@ -141,7 +141,7 @@ server {
|
|
141
141
|
root /var/cache/munin/www;
|
142
142
|
}
|
143
143
|
EOF
|
144
|
-
sudo ln -
|
144
|
+
sudo ln -sf /etc/nginx/sites-available/munin /etc/nginx/sites-enabled/
|
145
145
|
|
146
146
|
# postgres
|
147
147
|
sudo apt-get install -y postgresql postgresql-contrib
|
@@ -1,9 +1,43 @@
|
|
1
|
+
require 'json'
|
1
2
|
require 'nokogiri'
|
2
3
|
require 'ostruct'
|
3
4
|
|
4
5
|
module MuninPassenger
|
5
6
|
module Collect
|
6
7
|
|
8
|
+
def self.default_state_file
|
9
|
+
{
|
10
|
+
'pses' => []
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.read_state_file
|
15
|
+
filename = ENV['MUNIN_STATEFILE']
|
16
|
+
if filename and File.exist?(filename)
|
17
|
+
begin
|
18
|
+
JSON.parse(File.open(filename, 'r') {|f| f.read})
|
19
|
+
rescue
|
20
|
+
$stderr.puts "WARN: Couldn't open munin statefile at #{filename}: #{$!.message}"
|
21
|
+
default_state_file
|
22
|
+
end
|
23
|
+
else
|
24
|
+
default_state_file
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.write_state_file(state)
|
29
|
+
filename = ENV['MUNIN_STATEFILE']
|
30
|
+
if filename
|
31
|
+
begin
|
32
|
+
File.open(filename, 'w') do |f|
|
33
|
+
f.write state.to_json
|
34
|
+
end
|
35
|
+
rescue
|
36
|
+
$stderr.puts "WARN: Error writing to statefile at #{filename}: #{$!.message}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
7
41
|
def self.parse_stats(f)
|
8
42
|
Nokogiri::XML(f) do |config|
|
9
43
|
config.strict.
|
@@ -26,23 +60,74 @@ module MuninPassenger
|
|
26
60
|
end
|
27
61
|
|
28
62
|
def self.get_ps_stats(doc)
|
29
|
-
|
63
|
+
state = read_state_file
|
64
|
+
slots_by_pid = {}
|
65
|
+
state['pses'].each_with_index do |pid, i|
|
66
|
+
slots_by_pid[pid] = i if pid
|
67
|
+
end
|
68
|
+
ret = [nil] * state['pses'].size # Need at least as many workers as the most we've seen.
|
69
|
+
curr_pses = []
|
30
70
|
now = Time.now
|
31
71
|
doc.xpath('//info/supergroups/supergroup').each do |x_supergroup|
|
32
72
|
x_supergroup.xpath('./group').each do |x_group|
|
33
73
|
x_group.xpath('./processes/process').each do |x_ps|
|
74
|
+
pid = x_ps.xpath('pid').first.text
|
34
75
|
ps = OpenStruct.new
|
35
|
-
ps.
|
76
|
+
ps.active = true
|
77
|
+
ps.pid = pid
|
36
78
|
ps.sessions = x_ps.xpath('sessions').first.text.to_i
|
37
79
|
ps.last_used = (now - Time.at(x_ps.xpath('last_used').first.text.to_i / 1000 / 1000).to_i).to_i # in seconds
|
38
80
|
ps.ram = x_ps.xpath('real_memory').first.text.to_i # in KB
|
39
81
|
ps.cpu = x_ps.xpath('cpu').first.text.to_i # in %
|
40
82
|
ps.uptime = (now - Time.at(x_ps.xpath('spawn_start_time').first.text.to_i / 1000 / 1000).to_i).to_i # in seconds
|
41
83
|
ps.processed = x_ps.xpath('processed').first.text.to_i # in requests
|
42
|
-
|
84
|
+
ps.last_seen = now.to_i
|
85
|
+
curr_pses << ps
|
43
86
|
end
|
44
87
|
end
|
45
88
|
end
|
89
|
+
# This is all a little tricky,
|
90
|
+
# but if we name each metric after the pid,
|
91
|
+
# munin will throw away the history when passenger is restarted and the pids change.
|
92
|
+
# So instead we have "worker1", "worker2", etc.,
|
93
|
+
# and we show the current pid in parentheses.
|
94
|
+
# Each time we collect info, we keep a pid in the same slot as before.
|
95
|
+
# If a pid goes away, we free that slot.
|
96
|
+
# If it's the first we've seen a pid,
|
97
|
+
# we give it the first free slot.
|
98
|
+
new_pses = []
|
99
|
+
curr_pses.each do |ps|
|
100
|
+
i = slots_by_pid[ps.pid]
|
101
|
+
if i
|
102
|
+
ret[i] = ps
|
103
|
+
else
|
104
|
+
new_pses << ps
|
105
|
+
end
|
106
|
+
end
|
107
|
+
new_pses.each do |ps|
|
108
|
+
i = ret.index{|x| x == nil}
|
109
|
+
if i
|
110
|
+
ret[i] = ps
|
111
|
+
else
|
112
|
+
ret << ps
|
113
|
+
end
|
114
|
+
end
|
115
|
+
state['pses'] = ret.map do |ps|
|
116
|
+
if ps
|
117
|
+
{
|
118
|
+
'pid' => ps.pid,
|
119
|
+
'last_seen' => ps.last_seen,
|
120
|
+
}
|
121
|
+
else
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
write_state_file(state)
|
126
|
+
# Now if there are still nils,
|
127
|
+
# it means we are running than fewer workers than before.
|
128
|
+
# Preserve those slots,
|
129
|
+
# but the graphing code will have to watch out
|
130
|
+
# and note that they have no process currently.
|
46
131
|
ret
|
47
132
|
end
|
48
133
|
|
@@ -30,9 +30,10 @@ graph_vlabel Requests
|
|
30
30
|
_group_#{escape_group(g.name)}_queue.label #{g.name}
|
31
31
|
EOF
|
32
32
|
end
|
33
|
-
pses.
|
33
|
+
pses.each_with_index do |ps, i|
|
34
|
+
pidstr = ps ? "(PID #{ps.pid})" : "(no PID)"
|
34
35
|
ret += <<-EOF
|
35
|
-
|
36
|
+
_worker_#{i + 1}_sessions.label Worker #{i + 1} #{pidstr}
|
36
37
|
EOF
|
37
38
|
end
|
38
39
|
ret
|
@@ -46,9 +47,10 @@ _pid_#{ps.pid}_sessions.label PID #{ps.pid}
|
|
46
47
|
_group_#{escape_group(g.name)}_queue.value #{g.queue}
|
47
48
|
EOF
|
48
49
|
end
|
49
|
-
pses.
|
50
|
+
pses.each_with_index do |ps, i|
|
51
|
+
next unless ps
|
50
52
|
ret += <<-EOF
|
51
|
-
|
53
|
+
_worker_#{i + 1}_sessions.value #{ps.sessions}
|
52
54
|
EOF
|
53
55
|
end
|
54
56
|
ret
|
@@ -66,9 +68,10 @@ graph_category passenger
|
|
66
68
|
graph_title Passenger memory usage
|
67
69
|
graph_vlabel Bytes
|
68
70
|
EOF
|
69
|
-
pses.
|
71
|
+
pses.each_with_index do |ps, i|
|
72
|
+
pidstr = ps ? "(PID #{ps.pid})" : "(no PID)"
|
70
73
|
ret += <<-EOF
|
71
|
-
|
74
|
+
_worker_#{i + 1}_ram.label Worker #{i + 1} #{pidstr}
|
72
75
|
EOF
|
73
76
|
end
|
74
77
|
ret
|
@@ -77,9 +80,10 @@ _pid_#{ps.pid}_ram.label PID #{ps.pid}
|
|
77
80
|
def self.ram_values
|
78
81
|
groups, pses = get_stats
|
79
82
|
ret = ''
|
80
|
-
pses.
|
83
|
+
pses.each_with_index do |ps, i|
|
84
|
+
next unless ps
|
81
85
|
ret += <<-EOF
|
82
|
-
|
86
|
+
_worker_#{i + 1}_ram.value #{ps.ram * 1024}
|
83
87
|
EOF
|
84
88
|
end
|
85
89
|
ret
|
@@ -97,9 +101,10 @@ graph_category passenger
|
|
97
101
|
graph_title Passenger CPU
|
98
102
|
graph_vlabel %
|
99
103
|
EOF
|
100
|
-
pses.
|
104
|
+
pses.each_with_index do |ps, i|
|
105
|
+
pidstr = ps ? "(PID #{ps.pid})" : "(no PID)"
|
101
106
|
ret += <<-EOF
|
102
|
-
|
107
|
+
_worker_#{i + 1}_cpu.label Worker #{i + 1} #{pidstr}
|
103
108
|
EOF
|
104
109
|
end
|
105
110
|
ret
|
@@ -108,9 +113,10 @@ _pid_#{ps.pid}_cpu.label PID #{ps.pid}
|
|
108
113
|
def self.cpu_values
|
109
114
|
groups, pses = get_stats
|
110
115
|
ret = ''
|
111
|
-
pses.
|
116
|
+
pses.each_with_index do |ps, i|
|
117
|
+
next unless ps
|
112
118
|
ret += <<-EOF
|
113
|
-
|
119
|
+
_worker_#{i + 1}_cpu.value #{ps.cpu}
|
114
120
|
EOF
|
115
121
|
end
|
116
122
|
ret
|
@@ -128,9 +134,10 @@ graph_category passenger
|
|
128
134
|
graph_title Requests processed
|
129
135
|
graph_vlabel Requests
|
130
136
|
EOF
|
131
|
-
pses.
|
137
|
+
pses.each_with_index do |ps, i|
|
138
|
+
pidstr = ps ? "(PID #{ps.pid})" : "(no PID)"
|
132
139
|
ret += <<-EOF
|
133
|
-
|
140
|
+
_worker_#{i + 1}_processed.label Worker #{i + 1} #{pidstr}
|
134
141
|
EOF
|
135
142
|
end
|
136
143
|
ret
|
@@ -139,9 +146,10 @@ _pid_#{ps.pid}_processed.label PID #{ps.pid}
|
|
139
146
|
def self.processed_values
|
140
147
|
groups, pses = get_stats
|
141
148
|
ret = ''
|
142
|
-
pses.
|
149
|
+
pses.each_with_index do |ps, i|
|
150
|
+
next unless ps
|
143
151
|
ret += <<-EOF
|
144
|
-
|
152
|
+
_worker_#{i + 1}_processed.value #{ps.processed}
|
145
153
|
EOF
|
146
154
|
end
|
147
155
|
ret
|
@@ -159,9 +167,10 @@ graph_category passenger
|
|
159
167
|
graph_title Uptime
|
160
168
|
graph_vlabel Hours
|
161
169
|
EOF
|
162
|
-
pses.
|
170
|
+
pses.each_with_index do |ps, i|
|
171
|
+
pidstr = ps ? "(PID #{ps.pid})" : "(no PID)"
|
163
172
|
ret += <<-EOF
|
164
|
-
|
173
|
+
_worker_#{i + 1}_uptime.label Worker #{i + 1} #{pidstr}
|
165
174
|
EOF
|
166
175
|
end
|
167
176
|
ret
|
@@ -170,9 +179,10 @@ _pid_#{ps.pid}_uptime.label PID #{ps.pid}
|
|
170
179
|
def self.uptime_values
|
171
180
|
groups, pses = get_stats
|
172
181
|
ret = ''
|
173
|
-
pses.
|
182
|
+
pses.each_with_index do |ps, i|
|
183
|
+
next unless ps
|
174
184
|
ret += <<-EOF
|
175
|
-
|
185
|
+
_worker_#{i + 1}_uptime.value #{ps.uptime.to_f / 60 / 60}
|
176
186
|
EOF
|
177
187
|
end
|
178
188
|
ret
|
@@ -190,9 +200,10 @@ graph_category passenger
|
|
190
200
|
graph_title Last used
|
191
201
|
graph_vlabel Seconds
|
192
202
|
EOF
|
193
|
-
pses.
|
203
|
+
pses.each_with_index do |ps, i|
|
204
|
+
pidstr = ps ? "(PID #{ps.pid})" : "(no PID)"
|
194
205
|
ret += <<-EOF
|
195
|
-
|
206
|
+
_worker_#{i + 1}_last_used.label Worker #{i + 1} #{pidstr}
|
196
207
|
EOF
|
197
208
|
end
|
198
209
|
ret
|
@@ -201,9 +212,10 @@ _pid_#{ps.pid}_last_used.label PID #{ps.pid}
|
|
201
212
|
def self.last_used_values
|
202
213
|
groups, pses = get_stats
|
203
214
|
ret = ''
|
204
|
-
pses.
|
215
|
+
pses.each_with_index do |ps, i|
|
216
|
+
next unless ps
|
205
217
|
ret += <<-EOF
|
206
|
-
|
218
|
+
_worker_#{i + 1}_last_used.value #{ps.last_used}
|
207
219
|
EOF
|
208
220
|
end
|
209
221
|
ret
|
data/munin_passenger.gemspec
CHANGED
@@ -10,20 +10,20 @@ graph_category passenger
|
|
10
10
|
graph_title Passenger queue
|
11
11
|
graph_vlabel Requests
|
12
12
|
_group__vagrant_example__production__queue.label /vagrant/example (production)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
_worker_1_sessions.label Worker 1 (PID 4510)
|
14
|
+
_worker_2_sessions.label Worker 2 (PID 4529)
|
15
|
+
_worker_3_sessions.label Worker 3 (PID 4548)
|
16
|
+
_worker_4_sessions.label Worker 4 (PID 4567)
|
17
17
|
EOF
|
18
18
|
end
|
19
19
|
|
20
20
|
it "gives the queue values" do
|
21
21
|
expect(MuninPassenger::Graphs.queue_values).to eq <<-EOF
|
22
22
|
_group__vagrant_example__production__queue.value 4
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
_worker_1_sessions.value 1
|
24
|
+
_worker_2_sessions.value 1
|
25
|
+
_worker_3_sessions.value 1
|
26
|
+
_worker_4_sessions.value 1
|
27
27
|
EOF
|
28
28
|
end
|
29
29
|
|
@@ -32,19 +32,19 @@ _pid_4567_sessions.value 1
|
|
32
32
|
graph_category passenger
|
33
33
|
graph_title Passenger memory usage
|
34
34
|
graph_vlabel Bytes
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
_worker_1_ram.label Worker 1 (PID 4510)
|
36
|
+
_worker_2_ram.label Worker 2 (PID 4529)
|
37
|
+
_worker_3_ram.label Worker 3 (PID 4548)
|
38
|
+
_worker_4_ram.label Worker 4 (PID 4567)
|
39
39
|
EOF
|
40
40
|
end
|
41
41
|
|
42
42
|
it "gives the ram values" do
|
43
43
|
expect(MuninPassenger::Graphs.ram_values).to eq <<-EOF
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
_worker_1_ram.value #{40400 * 1024}
|
45
|
+
_worker_2_ram.value #{24396 * 1024}
|
46
|
+
_worker_3_ram.value #{24268 * 1024}
|
47
|
+
_worker_4_ram.value #{23872 * 1024}
|
48
48
|
EOF
|
49
49
|
end
|
50
50
|
|
@@ -53,19 +53,19 @@ _pid_4567_ram.value #{23872 * 1024}
|
|
53
53
|
graph_category passenger
|
54
54
|
graph_title Passenger CPU
|
55
55
|
graph_vlabel %
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
_worker_1_cpu.label Worker 1 (PID 4510)
|
57
|
+
_worker_2_cpu.label Worker 2 (PID 4529)
|
58
|
+
_worker_3_cpu.label Worker 3 (PID 4548)
|
59
|
+
_worker_4_cpu.label Worker 4 (PID 4567)
|
60
60
|
EOF
|
61
61
|
end
|
62
62
|
|
63
63
|
it "gives the cpu values" do
|
64
64
|
expect(MuninPassenger::Graphs.cpu_values).to eq <<-EOF
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
65
|
+
_worker_1_cpu.value 2
|
66
|
+
_worker_2_cpu.value 3
|
67
|
+
_worker_3_cpu.value 2
|
68
|
+
_worker_4_cpu.value 2
|
69
69
|
EOF
|
70
70
|
end
|
71
71
|
|
@@ -74,19 +74,19 @@ _pid_4567_cpu.value 2
|
|
74
74
|
graph_category passenger
|
75
75
|
graph_title Requests processed
|
76
76
|
graph_vlabel Requests
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
77
|
+
_worker_1_processed.label Worker 1 (PID 4510)
|
78
|
+
_worker_2_processed.label Worker 2 (PID 4529)
|
79
|
+
_worker_3_processed.label Worker 3 (PID 4548)
|
80
|
+
_worker_4_processed.label Worker 4 (PID 4567)
|
81
81
|
EOF
|
82
82
|
end
|
83
83
|
|
84
84
|
it "gives the processed values" do
|
85
85
|
expect(MuninPassenger::Graphs.processed_values).to eq <<-EOF
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
86
|
+
_worker_1_processed.value 139
|
87
|
+
_worker_2_processed.value 138
|
88
|
+
_worker_3_processed.value 138
|
89
|
+
_worker_4_processed.value 138
|
90
90
|
EOF
|
91
91
|
end
|
92
92
|
|
@@ -95,20 +95,20 @@ _pid_4567_processed.value 138
|
|
95
95
|
graph_category passenger
|
96
96
|
graph_title Uptime
|
97
97
|
graph_vlabel Hours
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
98
|
+
_worker_1_uptime.label Worker 1 (PID 4510)
|
99
|
+
_worker_2_uptime.label Worker 2 (PID 4529)
|
100
|
+
_worker_3_uptime.label Worker 3 (PID 4548)
|
101
|
+
_worker_4_uptime.label Worker 4 (PID 4567)
|
102
102
|
EOF
|
103
103
|
end
|
104
104
|
|
105
105
|
it "gives the uptime values" do
|
106
106
|
Timecop.travel('Thu Sep 13 20:36:38 PDT 2018') do
|
107
107
|
expect(MuninPassenger::Graphs.uptime_values).to eq <<-EOF
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
108
|
+
_worker_1_uptime.value 9.369166666666667
|
109
|
+
_worker_2_uptime.value 9.368888888888888
|
110
|
+
_worker_3_uptime.value 9.368888888888888
|
111
|
+
_worker_4_uptime.value 9.368888888888888
|
112
112
|
EOF
|
113
113
|
end
|
114
114
|
end
|
@@ -118,20 +118,20 @@ _pid_4567_uptime.value 9.368888888888888
|
|
118
118
|
graph_category passenger
|
119
119
|
graph_title Last used
|
120
120
|
graph_vlabel Seconds
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
121
|
+
_worker_1_last_used.label Worker 1 (PID 4510)
|
122
|
+
_worker_2_last_used.label Worker 2 (PID 4529)
|
123
|
+
_worker_3_last_used.label Worker 3 (PID 4548)
|
124
|
+
_worker_4_last_used.label Worker 4 (PID 4567)
|
125
125
|
EOF
|
126
126
|
end
|
127
127
|
|
128
128
|
it "gives the last_used values" do
|
129
129
|
Timecop.travel('Thu Sep 13 20:36:38 PDT 2018') do
|
130
130
|
expect(MuninPassenger::Graphs.last_used_values).to eq <<-EOF
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
131
|
+
_worker_1_last_used.value 33697
|
132
|
+
_worker_2_last_used.value 33697
|
133
|
+
_worker_3_last_used.value 33697
|
134
|
+
_worker_4_last_used.value 33697
|
135
135
|
EOF
|
136
136
|
end
|
137
137
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: munin_passenger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul A. Jungwirth
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-10-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: json
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
111
125
|
description: Runs passenger-status to graph CPU, RAM, queue size, requests served,
|
112
126
|
etc.
|
113
127
|
email: pj@illuminatedcomputing.com
|