solidstats 3.0.0.beta.1 → 3.0.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/README.md +33 -0
- data/app/assets/javascripts/solidstats/application.js +257 -0
- data/app/assets/javascripts/solidstats/dashboard.js +179 -0
- data/app/assets/stylesheets/solidstats/application.css +6 -1
- data/app/controllers/solidstats/dashboard_controller.rb +28 -35
- data/app/controllers/solidstats/gem_metadata_controller.rb +12 -0
- data/app/controllers/solidstats/logs_controller.rb +12 -12
- data/app/controllers/solidstats/performance_controller.rb +2 -2
- data/app/controllers/solidstats/productivity_controller.rb +10 -10
- data/app/controllers/solidstats/quality_controller.rb +32 -32
- data/app/controllers/solidstats/securities_controller.rb +7 -7
- data/app/helpers/solidstats/application_helper.rb +10 -10
- data/app/helpers/solidstats/performance_helper.rb +32 -32
- data/app/helpers/solidstats/productivity_helper.rb +20 -20
- data/app/services/solidstats/bundler_audit_service.rb +13 -13
- data/app/services/solidstats/coverage_compass_service.rb +59 -59
- data/app/services/solidstats/load_lens_service.rb +90 -70
- data/app/services/solidstats/log_size_monitor_service.rb +59 -59
- data/app/services/solidstats/my_todo_service.rb +68 -68
- data/app/services/solidstats/style_patrol_service.rb +44 -44
- data/app/views/layouts/solidstats/application.html.erb +1 -1
- data/app/views/solidstats/shared/_quick_actions.html.erb +1 -1
- data/config/routes.rb +4 -4
- data/lib/generators/solidstats/clean/clean_generator.rb +24 -0
- data/lib/generators/solidstats/clean/templates/README +8 -0
- data/lib/generators/solidstats/install/install_generator.rb +32 -17
- data/lib/generators/solidstats/templates/initializer.rb +112 -0
- data/lib/solidstats/asset_compatibility.rb +238 -0
- data/lib/solidstats/asset_manifest.rb +205 -0
- data/lib/solidstats/engine.rb +49 -9
- data/lib/solidstats/version.rb +1 -1
- data/lib/solidstats.rb +24 -11
- data/lib/tasks/solidstats.rake +67 -0
- data/lib/tasks/solidstats_performance.rake +6 -29
- data/lib/tasks/solidstats_tasks.rake +16 -0
- metadata +14 -5
- data/lib/tasks/solidstats_install.rake +0 -13
@@ -4,9 +4,9 @@ module Solidstats
|
|
4
4
|
# LoadLens - Development Performance Monitoring Service
|
5
5
|
# Parses Rails development logs to extract performance metrics
|
6
6
|
class LoadLensService
|
7
|
-
DATA_DIR = Rails.root.join(
|
8
|
-
LOG_FILE = Rails.root.join(
|
9
|
-
POSITION_FILE = DATA_DIR.join(
|
7
|
+
DATA_DIR = Rails.root.join("solidstats")
|
8
|
+
LOG_FILE = Rails.root.join("log", "development.log")
|
9
|
+
POSITION_FILE = DATA_DIR.join("last_position.txt")
|
10
10
|
CACHE_FILE = "loadlens.json"
|
11
11
|
SUMMARY_FILE = "summary.json"
|
12
12
|
RETENTION_DAYS = 7
|
@@ -22,7 +22,7 @@ module Solidstats
|
|
22
22
|
|
23
23
|
def self.get_performance_data
|
24
24
|
cache_file_path = solidstats_cache_path(CACHE_FILE)
|
25
|
-
|
25
|
+
|
26
26
|
if File.exist?(cache_file_path) && cache_fresh?(cache_file_path, 15.minutes)
|
27
27
|
raw_data = JSON.parse(File.read(cache_file_path))
|
28
28
|
deep_indifferent_access(raw_data)
|
@@ -36,13 +36,13 @@ module Solidstats
|
|
36
36
|
|
37
37
|
def self.scan_and_cache
|
38
38
|
performance_data = scan_development_log
|
39
|
-
|
39
|
+
|
40
40
|
# Cache the performance data
|
41
41
|
cache_performance_data(performance_data)
|
42
|
-
|
42
|
+
|
43
43
|
# Update summary.json with performance monitoring card
|
44
44
|
update_summary_json(performance_data)
|
45
|
-
|
45
|
+
|
46
46
|
deep_indifferent_access(performance_data)
|
47
47
|
end
|
48
48
|
|
@@ -64,10 +64,10 @@ module Solidstats
|
|
64
64
|
last_position = read_last_position
|
65
65
|
processed_count = 0
|
66
66
|
current_requests = []
|
67
|
-
|
68
|
-
File.open(LOG_FILE,
|
67
|
+
|
68
|
+
File.open(LOG_FILE, "r") do |file|
|
69
69
|
file.seek(last_position)
|
70
|
-
|
70
|
+
|
71
71
|
file.each_line do |line|
|
72
72
|
process_line(line.strip, current_requests)
|
73
73
|
update_last_position(file.pos)
|
@@ -81,7 +81,7 @@ module Solidstats
|
|
81
81
|
processed_count += 1
|
82
82
|
end
|
83
83
|
end
|
84
|
-
|
84
|
+
|
85
85
|
Rails.logger.info("DevLogParser: Processed #{processed_count} requests")
|
86
86
|
{ success: true, processed: processed_count }
|
87
87
|
rescue => e
|
@@ -105,23 +105,28 @@ module Solidstats
|
|
105
105
|
end
|
106
106
|
|
107
107
|
def self.scan_development_log
|
108
|
-
#
|
108
|
+
# First, ensure the log parser runs to create perf files if they don't exist
|
109
|
+
parse_recent_log_entries_if_needed
|
110
|
+
|
111
|
+
# Now, load the performance data from the perf files
|
109
112
|
data_files = []
|
110
|
-
|
113
|
+
found_perf_files = []
|
114
|
+
|
111
115
|
# Get last 7 days of data files
|
112
116
|
7.times do |i|
|
113
117
|
date = i.days.ago.to_date
|
114
118
|
file_path = DATA_DIR.join("perf_#{date.strftime('%Y-%m-%d')}.json")
|
115
|
-
|
119
|
+
|
116
120
|
if File.exist?(file_path)
|
117
121
|
file_data = JSON.parse(File.read(file_path))
|
118
122
|
data_files.concat(file_data)
|
123
|
+
found_perf_files << file_path
|
119
124
|
end
|
120
125
|
end
|
121
126
|
|
122
127
|
# Fallback: If no data files exist, parse recent entries from development.log
|
123
128
|
if data_files.empty? && File.exist?(LOG_FILE) && Rails.env.development?
|
124
|
-
Rails.logger.info("
|
129
|
+
Rails.logger.info("LoadLensService: No perf data files found, parsing recent log entries as a fallback.")
|
125
130
|
data_files = parse_recent_log_entries
|
126
131
|
end
|
127
132
|
|
@@ -129,58 +134,73 @@ module Solidstats
|
|
129
134
|
calculate_performance_metrics(data_files)
|
130
135
|
end
|
131
136
|
|
137
|
+
def self.parse_recent_log_entries_if_needed
|
138
|
+
# Check if any perf files exist from the last 7 days
|
139
|
+
has_recent_perf_files = 7.times.any? do |i|
|
140
|
+
date = i.days.ago.to_date
|
141
|
+
file_path = DATA_DIR.join("perf_#{date.strftime('%Y-%m-%d')}.json")
|
142
|
+
File.exist?(file_path)
|
143
|
+
end
|
144
|
+
|
145
|
+
# If no recent perf files, run the parser
|
146
|
+
unless has_recent_perf_files
|
147
|
+
Rails.logger.info("LoadLensService: No recent perf files found. Parsing development log to generate initial data.")
|
148
|
+
parse_recent_log_entries
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
132
152
|
def self.parse_recent_log_entries
|
133
153
|
# Parse recent entries from development.log as fallback when no data files exist
|
134
154
|
recent_requests = []
|
135
155
|
current_requests = []
|
136
156
|
lines_processed = 0
|
137
157
|
max_lines = 2000 # Process last 2000 lines for initial bootstrap
|
138
|
-
|
158
|
+
|
139
159
|
begin
|
140
160
|
# Get the last N lines from the log file efficiently
|
141
161
|
log_lines = []
|
142
|
-
File.open(LOG_FILE,
|
162
|
+
File.open(LOG_FILE, "r") do |file|
|
143
163
|
file.each_line { |line| log_lines << line.strip }
|
144
164
|
end
|
145
|
-
|
165
|
+
|
146
166
|
# Take the last max_lines entries
|
147
167
|
log_lines = log_lines.last(max_lines) if log_lines.size > max_lines
|
148
|
-
|
168
|
+
|
149
169
|
# Process each line using existing parsing logic
|
150
170
|
log_lines.each do |line|
|
151
171
|
process_line(line, current_requests)
|
152
172
|
lines_processed += 1
|
153
|
-
|
173
|
+
|
154
174
|
# Collect completed requests
|
155
175
|
current_requests.each do |req|
|
156
176
|
if req[:completed]
|
157
177
|
# Convert to the same format as saved requests
|
158
178
|
clean_request = {
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
179
|
+
"controller" => req[:controller],
|
180
|
+
"action" => req[:action],
|
181
|
+
"http_method" => req[:http_method],
|
182
|
+
"path" => req[:path],
|
183
|
+
"status" => req[:status],
|
184
|
+
"total_time_ms" => req[:total_time_ms] || 0.0,
|
185
|
+
"view_time_ms" => req[:view_time_ms] || 0.0,
|
186
|
+
"activerecord_time_ms" => req[:activerecord_time_ms] || 0.0,
|
187
|
+
"timestamp" => req[:timestamp]
|
168
188
|
}
|
169
|
-
|
189
|
+
|
170
190
|
# Only include complete requests with controller/action
|
171
|
-
if clean_request[
|
191
|
+
if clean_request["controller"] && clean_request["action"]
|
172
192
|
recent_requests << clean_request
|
173
193
|
end
|
174
194
|
end
|
175
195
|
end
|
176
|
-
|
196
|
+
|
177
197
|
# Remove completed requests from processing queue
|
178
198
|
current_requests.reject! { |req| req[:completed] }
|
179
199
|
end
|
180
|
-
|
200
|
+
|
181
201
|
Rails.logger.info("DevLogParser: Parsed #{recent_requests.size} recent requests from #{lines_processed} log lines")
|
182
202
|
recent_requests
|
183
|
-
|
203
|
+
|
184
204
|
rescue => e
|
185
205
|
Rails.logger.error("DevLogParser: Failed to parse recent log entries: #{e.message}")
|
186
206
|
[]
|
@@ -191,11 +211,11 @@ module Solidstats
|
|
191
211
|
return default_metrics if requests.empty?
|
192
212
|
|
193
213
|
total_requests = requests.size
|
194
|
-
avg_response_time = (requests.sum { |req| req[
|
195
|
-
avg_view_time = (requests.sum { |req| req[
|
196
|
-
avg_db_time = (requests.sum { |req| req[
|
197
|
-
slow_requests = requests.count { |req| (req[
|
198
|
-
error_rate = ((requests.count { |req| (req[
|
214
|
+
avg_response_time = (requests.sum { |req| req["total_time_ms"] || 0 } / total_requests).round(2)
|
215
|
+
avg_view_time = (requests.sum { |req| req["view_time_ms"] || 0 } / total_requests).round(2)
|
216
|
+
avg_db_time = (requests.sum { |req| req["activerecord_time_ms"] || 0 } / total_requests).round(2)
|
217
|
+
slow_requests = requests.count { |req| (req["total_time_ms"] || 0) > 1000 }
|
218
|
+
error_rate = ((requests.count { |req| (req["status"] || 200) >= 400 }.to_f / total_requests) * 100).round(2)
|
199
219
|
|
200
220
|
status = determine_status(avg_response_time, error_rate, slow_requests, total_requests)
|
201
221
|
|
@@ -210,7 +230,7 @@ module Solidstats
|
|
210
230
|
status: status,
|
211
231
|
last_updated: Time.current.iso8601
|
212
232
|
},
|
213
|
-
recent_requests: requests.sort_by { |req| req[
|
233
|
+
recent_requests: requests.sort_by { |req| req["timestamp"] }.reverse.first(20),
|
214
234
|
last_updated: Time.current.iso8601
|
215
235
|
}
|
216
236
|
end
|
@@ -224,7 +244,7 @@ module Solidstats
|
|
224
244
|
avg_db_time: 0,
|
225
245
|
slow_requests: 0,
|
226
246
|
error_rate: 0,
|
227
|
-
status:
|
247
|
+
status: "info",
|
228
248
|
last_updated: Time.current.iso8601
|
229
249
|
},
|
230
250
|
recent_requests: [],
|
@@ -233,9 +253,9 @@ module Solidstats
|
|
233
253
|
end
|
234
254
|
|
235
255
|
def self.determine_status(avg_response_time, error_rate, slow_requests, total_requests)
|
236
|
-
return
|
237
|
-
return
|
238
|
-
|
256
|
+
return "error" if error_rate > 10
|
257
|
+
return "warning" if avg_response_time > 1000 || slow_requests > (total_requests * 0.1)
|
258
|
+
"success"
|
239
259
|
end
|
240
260
|
|
241
261
|
def self.process_line(line, current_requests)
|
@@ -248,7 +268,7 @@ module Solidstats
|
|
248
268
|
timestamp: parse_timestamp(timestamp),
|
249
269
|
started_at: Time.current
|
250
270
|
}
|
251
|
-
|
271
|
+
|
252
272
|
# Controller and action info
|
253
273
|
elsif match = line.match(CONTROLLER_ACTION_REGEX)
|
254
274
|
controller, action = match.captures
|
@@ -256,7 +276,7 @@ module Solidstats
|
|
256
276
|
current_request[:controller] = controller
|
257
277
|
current_request[:action] = action
|
258
278
|
end
|
259
|
-
|
279
|
+
|
260
280
|
# Request completion with timing
|
261
281
|
elsif match = line.match(COMPLETED_REGEX)
|
262
282
|
status, total_time, timing_details = match.captures
|
@@ -264,26 +284,26 @@ module Solidstats
|
|
264
284
|
current_request[:status] = status.to_i
|
265
285
|
current_request[:total_time_ms] = total_time.to_f
|
266
286
|
current_request[:completed] = true
|
267
|
-
|
287
|
+
|
268
288
|
# Extract view and ActiveRecord times from timing details if present
|
269
289
|
if timing_details
|
270
290
|
if view_match = timing_details.match(VIEW_RENDERING_REGEX)
|
271
291
|
current_request[:view_time_ms] = view_match[1].to_f
|
272
292
|
end
|
273
|
-
|
293
|
+
|
274
294
|
if ar_match = timing_details.match(ACTIVERECORD_REGEX)
|
275
295
|
current_request[:activerecord_time_ms] = ar_match[1].to_f
|
276
296
|
end
|
277
297
|
end
|
278
298
|
end
|
279
|
-
|
299
|
+
|
280
300
|
# View rendering time
|
281
301
|
elsif match = line.match(VIEW_RENDERING_REGEX)
|
282
302
|
view_time = match.captures.first
|
283
303
|
if current_request = current_requests.last
|
284
304
|
current_request[:view_time_ms] = view_time.to_f
|
285
305
|
end
|
286
|
-
|
306
|
+
|
287
307
|
# ActiveRecord time
|
288
308
|
elsif match = line.match(ACTIVERECORD_REGEX)
|
289
309
|
ar_time = match.captures.first
|
@@ -319,7 +339,7 @@ module Solidstats
|
|
319
339
|
return unless clean_request[:controller] && clean_request[:action]
|
320
340
|
|
321
341
|
perf_file = current_perf_file
|
322
|
-
|
342
|
+
|
323
343
|
# Read existing data or create new array
|
324
344
|
existing_data = if File.exist?(perf_file)
|
325
345
|
JSON.parse(File.read(perf_file))
|
@@ -335,7 +355,7 @@ module Solidstats
|
|
335
355
|
end
|
336
356
|
|
337
357
|
def self.current_perf_file
|
338
|
-
date_suffix = Date.current.strftime(
|
358
|
+
date_suffix = Date.current.strftime("%Y-%m-%d")
|
339
359
|
DATA_DIR.join("perf_#{date_suffix}.json")
|
340
360
|
end
|
341
361
|
|
@@ -345,8 +365,8 @@ module Solidstats
|
|
345
365
|
|
346
366
|
def self.cleanup_old_files
|
347
367
|
cutoff_date = RETENTION_DAYS.days.ago.to_date
|
348
|
-
|
349
|
-
Dir.glob(DATA_DIR.join(
|
368
|
+
|
369
|
+
Dir.glob(DATA_DIR.join("perf_*.json")).each do |file|
|
350
370
|
if match = File.basename(file).match(/perf_(\d{4}-\d{2}-\d{2})\.json/)
|
351
371
|
file_date = Date.parse(match[1])
|
352
372
|
File.delete(file) if file_date < cutoff_date
|
@@ -373,7 +393,7 @@ module Solidstats
|
|
373
393
|
def self.cache_performance_data(data)
|
374
394
|
cache_file_path = solidstats_cache_path(CACHE_FILE)
|
375
395
|
ensure_solidstats_directory
|
376
|
-
|
396
|
+
|
377
397
|
File.write(cache_file_path, JSON.pretty_generate(data))
|
378
398
|
rescue => e
|
379
399
|
Rails.logger.error("Failed to cache performance data: #{e.message}")
|
@@ -381,33 +401,33 @@ module Solidstats
|
|
381
401
|
|
382
402
|
def self.update_summary_json(performance_data)
|
383
403
|
summary_file_path = solidstats_cache_path(SUMMARY_FILE)
|
384
|
-
|
404
|
+
|
385
405
|
# Read existing summary or create new one
|
386
406
|
begin
|
387
407
|
existing_summary = File.exist?(summary_file_path) ? JSON.parse(File.read(summary_file_path)) : {}
|
388
408
|
rescue JSON::ParserError
|
389
409
|
existing_summary = {}
|
390
410
|
end
|
391
|
-
|
411
|
+
|
392
412
|
summary = performance_data[:summary]
|
393
|
-
|
413
|
+
|
394
414
|
# Create badges based on performance metrics
|
395
415
|
badges = []
|
396
416
|
badges << { "text" => "#{summary[:total_requests]} Requests", "color" => "info" }
|
397
|
-
|
417
|
+
|
398
418
|
case summary[:status]
|
399
|
-
when
|
419
|
+
when "error"
|
400
420
|
badges << { "text" => "High Errors", "color" => "error" }
|
401
|
-
when
|
421
|
+
when "warning"
|
402
422
|
badges << { "text" => "Slow Responses", "color" => "warning" }
|
403
423
|
else
|
404
424
|
badges << { "text" => "Healthy", "color" => "success" }
|
405
425
|
end
|
406
|
-
|
426
|
+
|
407
427
|
if summary[:avg_response_time] > 0
|
408
428
|
badges << { "text" => "#{summary[:avg_response_time]}ms avg", "color" => "neutral" }
|
409
429
|
end
|
410
|
-
|
430
|
+
|
411
431
|
# Update the LoadLens monitoring entry
|
412
432
|
existing_summary["LoadLens"] = {
|
413
433
|
"icon" => "activity",
|
@@ -417,7 +437,7 @@ module Solidstats
|
|
417
437
|
"url" => "/solidstats/performance/load_lens",
|
418
438
|
"badges" => badges
|
419
439
|
}
|
420
|
-
|
440
|
+
|
421
441
|
# Write updated summary
|
422
442
|
File.write(summary_file_path, JSON.pretty_generate(existing_summary))
|
423
443
|
rescue => e
|
@@ -427,9 +447,9 @@ module Solidstats
|
|
427
447
|
def self.generate_performance_message(summary)
|
428
448
|
if summary[:total_requests] == 0
|
429
449
|
"No requests tracked"
|
430
|
-
elsif summary[:status] ==
|
450
|
+
elsif summary[:status] == "error"
|
431
451
|
"#{summary[:error_rate]}% error rate"
|
432
|
-
elsif summary[:status] ==
|
452
|
+
elsif summary[:status] == "warning"
|
433
453
|
"#{summary[:avg_response_time]}ms avg"
|
434
454
|
else
|
435
455
|
"#{summary[:avg_response_time]}ms avg"
|
@@ -441,13 +461,13 @@ module Solidstats
|
|
441
461
|
rescue
|
442
462
|
false
|
443
463
|
end
|
444
|
-
|
464
|
+
|
445
465
|
def self.solidstats_cache_path(filename)
|
446
|
-
Rails.root.join(
|
466
|
+
Rails.root.join("solidstats", filename)
|
447
467
|
end
|
448
|
-
|
468
|
+
|
449
469
|
def self.ensure_solidstats_directory
|
450
|
-
dir_path = File.dirname(solidstats_cache_path(
|
470
|
+
dir_path = File.dirname(solidstats_cache_path("dummy"))
|
451
471
|
FileUtils.mkdir_p(dir_path) unless Dir.exist?(dir_path)
|
452
472
|
end
|
453
473
|
end
|
@@ -2,14 +2,14 @@ module Solidstats
|
|
2
2
|
class LogSizeMonitorService
|
3
3
|
CACHE_FILE = "logs.json"
|
4
4
|
SUMMARY_FILE = "summary.json"
|
5
|
-
|
5
|
+
|
6
6
|
# Size thresholds in bytes
|
7
7
|
WARNING_SIZE = 500.megabytes
|
8
8
|
ERROR_SIZE = 1.gigabyte
|
9
|
-
|
9
|
+
|
10
10
|
def self.get_logs_data
|
11
11
|
cache_file_path = solidstats_cache_path(CACHE_FILE)
|
12
|
-
|
12
|
+
|
13
13
|
if File.exist?(cache_file_path) && cache_fresh?(cache_file_path)
|
14
14
|
JSON.parse(File.read(cache_file_path))
|
15
15
|
else
|
@@ -19,40 +19,40 @@ module Solidstats
|
|
19
19
|
Rails.logger.error("Error reading logs cache, regenerating...")
|
20
20
|
scan_and_cache
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
def self.scan_and_cache
|
24
24
|
logs_data = scan_log_files
|
25
|
-
|
25
|
+
|
26
26
|
# Cache the logs data
|
27
27
|
cache_logs_data(logs_data)
|
28
|
-
|
28
|
+
|
29
29
|
# Update summary.json with log monitoring card
|
30
30
|
update_summary_json(logs_data)
|
31
|
-
|
31
|
+
|
32
32
|
logs_data
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
def self.truncate_log(filename)
|
36
36
|
return { status: "error", message: "Filename required" } if filename.blank?
|
37
|
-
|
37
|
+
|
38
38
|
log_file_path = find_log_file(filename)
|
39
39
|
return { status: "error", message: "Log file not found" } unless log_file_path
|
40
|
-
|
40
|
+
|
41
41
|
begin
|
42
42
|
# Check if file is writable
|
43
43
|
unless File.writable?(log_file_path)
|
44
44
|
return { status: "error", message: "File is not writable" }
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
# Get original size for reporting
|
48
48
|
original_size = File.size(log_file_path)
|
49
|
-
|
49
|
+
|
50
50
|
# Truncate the file
|
51
51
|
File.truncate(log_file_path, 0)
|
52
|
-
|
52
|
+
|
53
53
|
# Update cache after truncation
|
54
54
|
scan_and_cache
|
55
|
-
|
55
|
+
|
56
56
|
{
|
57
57
|
status: "success",
|
58
58
|
message: "Log file truncated successfully",
|
@@ -64,21 +64,21 @@ module Solidstats
|
|
64
64
|
{ status: "error", message: "Failed to truncate: #{e.message}" }
|
65
65
|
end
|
66
66
|
end
|
67
|
-
|
67
|
+
|
68
68
|
private
|
69
|
-
|
69
|
+
|
70
70
|
def self.scan_log_files
|
71
71
|
log_files = discover_log_files
|
72
72
|
total_size = 0
|
73
73
|
files_data = []
|
74
|
-
|
74
|
+
|
75
75
|
log_files.each do |file_path|
|
76
76
|
next unless File.exist?(file_path) && File.readable?(file_path)
|
77
|
-
|
77
|
+
|
78
78
|
file_stat = File.stat(file_path)
|
79
79
|
file_size = file_stat.size
|
80
80
|
total_size += file_size
|
81
|
-
|
81
|
+
|
82
82
|
files_data << {
|
83
83
|
name: File.basename(file_path),
|
84
84
|
path: file_path,
|
@@ -89,10 +89,10 @@ module Solidstats
|
|
89
89
|
can_truncate: File.writable?(file_path)
|
90
90
|
}
|
91
91
|
end
|
92
|
-
|
92
|
+
|
93
93
|
# Sort by size descending
|
94
94
|
files_data.sort_by! { |f| -f[:size_bytes] }
|
95
|
-
|
95
|
+
|
96
96
|
{
|
97
97
|
summary: {
|
98
98
|
total_files: files_data.length,
|
@@ -106,91 +106,91 @@ module Solidstats
|
|
106
106
|
files: files_data
|
107
107
|
}
|
108
108
|
end
|
109
|
-
|
109
|
+
|
110
110
|
def self.discover_log_files
|
111
111
|
log_paths = []
|
112
|
-
|
112
|
+
|
113
113
|
# Rails log directory
|
114
114
|
if defined?(Rails) && Rails.root
|
115
|
-
rails_log_dir = Rails.root.join(
|
116
|
-
log_paths += Dir.glob(rails_log_dir.join(
|
115
|
+
rails_log_dir = Rails.root.join("log")
|
116
|
+
log_paths += Dir.glob(rails_log_dir.join("*.log")) if Dir.exist?(rails_log_dir)
|
117
117
|
end
|
118
|
-
|
118
|
+
|
119
119
|
# Remove duplicates and filter readable files
|
120
120
|
log_paths.uniq.select { |path| File.readable?(path) }
|
121
121
|
end
|
122
|
-
|
122
|
+
|
123
123
|
def self.find_log_file(filename)
|
124
124
|
discovered_files = discover_log_files
|
125
125
|
discovered_files.find { |path| File.basename(path) == filename }
|
126
126
|
end
|
127
|
-
|
127
|
+
|
128
128
|
def self.determine_file_status(size_bytes)
|
129
129
|
if size_bytes >= ERROR_SIZE
|
130
|
-
|
130
|
+
"error"
|
131
131
|
elsif size_bytes >= WARNING_SIZE
|
132
|
-
|
132
|
+
"warning"
|
133
133
|
else
|
134
|
-
|
134
|
+
"success"
|
135
135
|
end
|
136
136
|
end
|
137
|
-
|
137
|
+
|
138
138
|
def self.determine_overall_status(total_size, files_data)
|
139
|
-
error_files = files_data.count { |f| f[:status] ==
|
140
|
-
warning_files = files_data.count { |f| f[:status] ==
|
141
|
-
|
139
|
+
error_files = files_data.count { |f| f[:status] == "error" }
|
140
|
+
warning_files = files_data.count { |f| f[:status] == "warning" }
|
141
|
+
|
142
142
|
if error_files > 0 || total_size >= ERROR_SIZE * 2
|
143
|
-
|
143
|
+
"error"
|
144
144
|
elsif warning_files > 0 || total_size >= WARNING_SIZE * 3
|
145
|
-
|
145
|
+
"warning"
|
146
146
|
else
|
147
|
-
|
147
|
+
"success"
|
148
148
|
end
|
149
149
|
end
|
150
|
-
|
150
|
+
|
151
151
|
def self.format_file_size(size_bytes)
|
152
152
|
return "0 B" if size_bytes.zero?
|
153
|
-
|
154
|
-
units = [
|
153
|
+
|
154
|
+
units = [ "B", "KB", "MB", "GB", "TB" ]
|
155
155
|
base = 1024.0
|
156
156
|
exp = (Math.log(size_bytes) / Math.log(base)).to_i
|
157
|
-
exp = [exp, units.length - 1].min
|
158
|
-
|
159
|
-
"%.1f %s" % [size_bytes / (base ** exp), units[exp]]
|
157
|
+
exp = [ exp, units.length - 1 ].min
|
158
|
+
|
159
|
+
"%.1f %s" % [ size_bytes / (base ** exp), units[exp] ]
|
160
160
|
end
|
161
|
-
|
161
|
+
|
162
162
|
def self.cache_logs_data(logs_data)
|
163
163
|
cache_file_path = solidstats_cache_path(CACHE_FILE)
|
164
164
|
ensure_solidstats_directory
|
165
|
-
|
165
|
+
|
166
166
|
File.write(cache_file_path, JSON.pretty_generate(logs_data))
|
167
167
|
rescue => e
|
168
168
|
Rails.logger.error("Failed to cache logs data: #{e.message}")
|
169
169
|
end
|
170
|
-
|
170
|
+
|
171
171
|
def self.update_summary_json(logs_data)
|
172
172
|
summary_file_path = solidstats_cache_path(SUMMARY_FILE)
|
173
|
-
|
173
|
+
|
174
174
|
# Read existing summary or create new one
|
175
175
|
begin
|
176
176
|
existing_summary = File.exist?(summary_file_path) ? JSON.parse(File.read(summary_file_path)) : {}
|
177
177
|
rescue JSON::ParserError
|
178
178
|
existing_summary = {}
|
179
179
|
end
|
180
|
-
|
180
|
+
|
181
181
|
# Create badges based on status
|
182
182
|
badges = []
|
183
183
|
badges << { "text" => "#{logs_data[:summary][:total_files]} Files", "color" => "info" }
|
184
|
-
|
184
|
+
|
185
185
|
case logs_data[:summary][:status]
|
186
|
-
when
|
186
|
+
when "error"
|
187
187
|
badges << { "text" => "Large Size", "color" => "error" }
|
188
|
-
when
|
188
|
+
when "warning"
|
189
189
|
badges << { "text" => "Growing", "color" => "warning" }
|
190
190
|
else
|
191
191
|
badges << { "text" => "Healthy", "color" => "success" }
|
192
192
|
end
|
193
|
-
|
193
|
+
|
194
194
|
# Update the log monitoring entry
|
195
195
|
existing_summary["Log Files"] = {
|
196
196
|
"icon" => "file-text",
|
@@ -200,25 +200,25 @@ module Solidstats
|
|
200
200
|
"url" => "/solidstats/logs/size",
|
201
201
|
"badges" => badges
|
202
202
|
}
|
203
|
-
|
203
|
+
|
204
204
|
# Write updated summary
|
205
205
|
File.write(summary_file_path, JSON.pretty_generate(existing_summary))
|
206
206
|
rescue => e
|
207
207
|
Rails.logger.error("Failed to update summary.json: #{e.message}")
|
208
208
|
end
|
209
|
-
|
209
|
+
|
210
210
|
def self.cache_fresh?(cache_file_path, max_age = 30.minutes)
|
211
211
|
File.mtime(cache_file_path) > max_age.ago
|
212
212
|
rescue
|
213
213
|
false
|
214
214
|
end
|
215
|
-
|
215
|
+
|
216
216
|
def self.solidstats_cache_path(filename)
|
217
|
-
Rails.root.join(
|
217
|
+
Rails.root.join("solidstats", filename)
|
218
218
|
end
|
219
|
-
|
219
|
+
|
220
220
|
def self.ensure_solidstats_directory
|
221
|
-
dir_path = File.dirname(solidstats_cache_path(
|
221
|
+
dir_path = File.dirname(solidstats_cache_path("dummy"))
|
222
222
|
FileUtils.mkdir_p(dir_path) unless Dir.exist?(dir_path)
|
223
223
|
end
|
224
224
|
end
|