solidstats 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/CHANGELOG.md +17 -0
- data/README.md +2 -0
- data/app/controllers/solidstats/dashboard_controller.rb +19 -0
- data/app/services/solidstats/log_size_monitor_service.rb +94 -0
- data/app/views/solidstats/dashboard/_log_monitor.html.erb +759 -0
- data/app/views/solidstats/dashboard/index.html.erb +14 -0
- data/config/routes.rb +4 -0
- data/lib/solidstats/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1e51da79f65a50793bc1bfc777ed19df5d81795739ceda8558d3261745b53ae
|
4
|
+
data.tar.gz: e58f0018fd96135789f192372b2b85f7165cf51a6921fa7e2a56057bebdb14b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3fc113aaaf952e1d9a320b55618b72d4b6025ec81d3bc274f29a090ddbc6a0f500bbd50aa2873d8e02d850dac992ef28214e5709f0160bc2ce0e88e348182f0a
|
7
|
+
data.tar.gz: 976d3989eeffc2781c1040846af1435147be33a407b6e5a2bae1db8dfba091f9678635b1863c3dcc36916adf5d2b334cc0f0a7d523225066bb4fdac074ebf270
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [1.1.0] - 2025-05-23
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Log Size Monitor feature:
|
12
|
+
- Comprehensive monitoring of all Rails application log files
|
13
|
+
- Total log directory size tracking with status indicators
|
14
|
+
- Individual log file monitoring with size and status details
|
15
|
+
- Visual indicators for log size status (OK, Warning, Danger)
|
16
|
+
- Visual meter showing log size relative to thresholds
|
17
|
+
- One-click log truncation for individual files or all logs
|
18
|
+
- Sortable log file table with size and last modified information
|
19
|
+
- Log management recommendations
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
- Fixed log file truncation when filename is provided without extension
|
23
|
+
- Added constraints to route handling to improve filename parameter handling
|
24
|
+
|
8
25
|
## [1.0.0] - 2025-05-22
|
9
26
|
|
10
27
|
### Added
|
data/README.md
CHANGED
@@ -7,6 +7,8 @@ Solidstats is a local-only Rails engine that shows your project's health at `/so
|
|
7
7
|
- Bundler Audit scan with detailed remediation suggestions
|
8
8
|
- Interactive vulnerability details with patched version information
|
9
9
|
- Gem impact analysis showing affected gems by severity
|
10
|
+
- Log Size Monitor for tracking and managing application log files
|
11
|
+
- Log file truncation tool for individual or all log files
|
10
12
|
- Rubocop offense count and quality metrics
|
11
13
|
- TODO/FIXME tracker with file hotspots
|
12
14
|
- Test coverage summary
|
@@ -7,10 +7,12 @@ module Solidstats
|
|
7
7
|
# Use new services for data collection
|
8
8
|
audit_service = AuditService.new
|
9
9
|
todo_service = TodoService.new
|
10
|
+
log_monitor_service = LogSizeMonitorService.new
|
10
11
|
|
11
12
|
# Get full data for detailed views
|
12
13
|
@audit_output = audit_service.fetch
|
13
14
|
@todo_items = todo_service.fetch
|
15
|
+
@log_data = log_monitor_service.collect_data
|
14
16
|
|
15
17
|
# Get summary data for dashboard cards
|
16
18
|
@audit_summary = audit_service.summary
|
@@ -26,10 +28,12 @@ module Solidstats
|
|
26
28
|
# Create services
|
27
29
|
audit_service = AuditService.new
|
28
30
|
todo_service = TodoService.new
|
31
|
+
log_monitor_service = LogSizeMonitorService.new
|
29
32
|
|
30
33
|
# Force refresh of data
|
31
34
|
audit_output = audit_service.fetch(true) # Force refresh
|
32
35
|
todo_items = todo_service.fetch(true) # Force refresh
|
36
|
+
log_data = log_monitor_service.collect_data
|
33
37
|
|
34
38
|
# Get updated summaries
|
35
39
|
audit_summary = audit_service.summary
|
@@ -44,6 +48,7 @@ module Solidstats
|
|
44
48
|
todo_items: todo_items,
|
45
49
|
audit_summary: audit_summary,
|
46
50
|
todo_summary: todo_summary,
|
51
|
+
log_data: log_data,
|
47
52
|
last_updated: last_updated,
|
48
53
|
status: "success"
|
49
54
|
}
|
@@ -54,5 +59,19 @@ module Solidstats
|
|
54
59
|
message: "Failed to refresh data: #{e.message}"
|
55
60
|
}, status: :internal_server_error
|
56
61
|
end
|
62
|
+
|
63
|
+
def truncate_log
|
64
|
+
log_monitor_service = LogSizeMonitorService.new
|
65
|
+
filename = params[:filename]
|
66
|
+
|
67
|
+
# Add .log extension if not included in the filename
|
68
|
+
if filename.present? && !filename.end_with?('.log')
|
69
|
+
filename = "#{filename}.log"
|
70
|
+
end
|
71
|
+
|
72
|
+
result = log_monitor_service.truncate_log(filename)
|
73
|
+
|
74
|
+
render json: result
|
75
|
+
end
|
57
76
|
end
|
58
77
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Solidstats
|
2
|
+
class LogSizeMonitorService
|
3
|
+
WARNING_THRESHOLD = 25 # In megabytes
|
4
|
+
DANGER_THRESHOLD = 50 # In megabytes
|
5
|
+
|
6
|
+
def collect_data
|
7
|
+
log_files = scan_log_directory
|
8
|
+
total_size_bytes = log_files.sum { |file| file[:size_bytes] }
|
9
|
+
|
10
|
+
# Create aggregated data
|
11
|
+
{
|
12
|
+
log_dir_path: log_directory,
|
13
|
+
total_size_bytes: total_size_bytes,
|
14
|
+
total_size_mb: bytes_to_mb(total_size_bytes),
|
15
|
+
status: calculate_status(total_size_bytes),
|
16
|
+
logs_count: log_files.size,
|
17
|
+
log_files: log_files,
|
18
|
+
created_at: Time.now.iso8601
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def truncate_log(filename = nil)
|
23
|
+
begin
|
24
|
+
if filename.present?
|
25
|
+
# Truncate specific log file
|
26
|
+
# Ensure filename has .log extension
|
27
|
+
filename = "#{filename}.log" unless filename.end_with?('.log')
|
28
|
+
|
29
|
+
file_path = File.join(log_directory, filename)
|
30
|
+
if File.exist?(file_path)
|
31
|
+
File.open(file_path, 'w') { |f| f.truncate(0) }
|
32
|
+
{ success: true, message: "Log file '#{filename}' truncated successfully" }
|
33
|
+
else
|
34
|
+
{ success: false, message: "Log file '#{filename}' not found" }
|
35
|
+
end
|
36
|
+
else
|
37
|
+
# Truncate all log files
|
38
|
+
scan_log_directory.each do |log_file|
|
39
|
+
File.open(log_file[:path], 'w') { |f| f.truncate(0) }
|
40
|
+
end
|
41
|
+
{ success: true, message: "All log files truncated successfully" }
|
42
|
+
end
|
43
|
+
rescue => e
|
44
|
+
{ success: false, message: "Failed to truncate log file: #{e.message}" }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def log_directory
|
51
|
+
Rails.root.join("log").to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def scan_log_directory
|
55
|
+
log_files = []
|
56
|
+
|
57
|
+
# Get all files in the log directory
|
58
|
+
Dir.glob(File.join(log_directory, "*.log")).each do |file_path|
|
59
|
+
if File.file?(file_path)
|
60
|
+
size_bytes = File.size(file_path)
|
61
|
+
filename = File.basename(file_path)
|
62
|
+
|
63
|
+
log_files << {
|
64
|
+
filename: filename,
|
65
|
+
path: file_path,
|
66
|
+
size_bytes: size_bytes,
|
67
|
+
size_mb: bytes_to_mb(size_bytes),
|
68
|
+
status: calculate_status(size_bytes),
|
69
|
+
last_modified: File.mtime(file_path)
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Sort by size (largest first)
|
75
|
+
log_files.sort_by { |file| -file[:size_bytes] }
|
76
|
+
end
|
77
|
+
|
78
|
+
def bytes_to_mb(bytes)
|
79
|
+
(bytes.to_f / (1024 * 1024)).round(2)
|
80
|
+
end
|
81
|
+
|
82
|
+
def calculate_status(size_bytes)
|
83
|
+
size_mb = bytes_to_mb(size_bytes)
|
84
|
+
|
85
|
+
if size_mb >= DANGER_THRESHOLD
|
86
|
+
:danger
|
87
|
+
elsif size_mb >= WARNING_THRESHOLD
|
88
|
+
:warning
|
89
|
+
else
|
90
|
+
:ok
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,759 @@
|
|
1
|
+
<%
|
2
|
+
# Convert status symbol to CSS class and display text
|
3
|
+
status_class = case @log_data[:status]
|
4
|
+
when :ok then "status-ok"
|
5
|
+
when :warning then "status-warning"
|
6
|
+
when :danger then "status-danger"
|
7
|
+
else ""
|
8
|
+
end
|
9
|
+
|
10
|
+
status_text = case @log_data[:status]
|
11
|
+
when :ok then "✅ OK: Log files are within acceptable limits"
|
12
|
+
when :warning then "⚠️ Warning: Log files are getting large"
|
13
|
+
when :danger then "❌ Danger: Log files are too large"
|
14
|
+
else "Unknown status"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Calculate meter fill percentage (capped at 100%)
|
18
|
+
fill_percentage = [(@log_data[:total_size_mb] / 50.0) * 100, 100].min
|
19
|
+
|
20
|
+
# Set color based on status
|
21
|
+
fill_color = case @log_data[:status]
|
22
|
+
when :ok then "#28a745"
|
23
|
+
when :warning then "#ffc107"
|
24
|
+
when :danger then "#dc3545"
|
25
|
+
else "#6c757d"
|
26
|
+
end
|
27
|
+
%>
|
28
|
+
|
29
|
+
<div class="log-monitor-card">
|
30
|
+
<div class="log-header">
|
31
|
+
<div>
|
32
|
+
<h3><span class="icon">📊</span> Log Size Monitor</h3>
|
33
|
+
<p class="log-path"><%= @log_data[:log_dir_path] %></p>
|
34
|
+
</div>
|
35
|
+
<div class="log-actions">
|
36
|
+
<button id="truncate-all-logs-btn" class="action-button truncate-button">
|
37
|
+
<span class="action-icon">✂️</span> Truncate All Logs
|
38
|
+
</button>
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
|
42
|
+
<div class="log-content">
|
43
|
+
<div class="log-status">
|
44
|
+
<div class="log-indicator <%= status_class %>"></div>
|
45
|
+
<div class="log-status-text"><%= status_text %></div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<div class="log-size-container">
|
49
|
+
<div class="log-size-label">Total Size:</div>
|
50
|
+
<div class="log-size-value"><%= @log_data[:total_size_mb] %> MB</div>
|
51
|
+
<div class="log-files-count">(<%= @log_data[:logs_count] %> log files)</div>
|
52
|
+
</div>
|
53
|
+
|
54
|
+
<div class="log-size-meter">
|
55
|
+
<div class="meter-bg"></div>
|
56
|
+
<div class="meter-fill" style="width: <%= fill_percentage %>%; background-color: <%= fill_color %>;"></div>
|
57
|
+
<div class="meter-markers">
|
58
|
+
<div class="marker" style="left: 0%">0</div>
|
59
|
+
<div class="marker warning-marker" style="left: 50%">25 MB</div>
|
60
|
+
<div class="marker danger-marker" style="left: 100%">50+ MB</div>
|
61
|
+
</div>
|
62
|
+
</div>
|
63
|
+
</div>
|
64
|
+
|
65
|
+
<div class="log-files-table">
|
66
|
+
<h4>Individual Log Files</h4>
|
67
|
+
<table>
|
68
|
+
<thead>
|
69
|
+
<tr>
|
70
|
+
<th>File Name</th>
|
71
|
+
<th>Size</th>
|
72
|
+
<th>Last Modified</th>
|
73
|
+
<th>Status</th>
|
74
|
+
<th>Actions</th>
|
75
|
+
</tr>
|
76
|
+
</thead>
|
77
|
+
<tbody>
|
78
|
+
<% @log_data[:log_files].each do |log_file| %>
|
79
|
+
<%
|
80
|
+
file_status_class = case log_file[:status]
|
81
|
+
when :ok then "status-ok"
|
82
|
+
when :warning then "status-warning"
|
83
|
+
when :danger then "status-danger"
|
84
|
+
else ""
|
85
|
+
end
|
86
|
+
%>
|
87
|
+
<tr class="<%= file_status_class %>">
|
88
|
+
<td class="filename"><%= log_file[:filename] %></td>
|
89
|
+
<td class="size"><%= log_file[:size_mb] %> MB</td>
|
90
|
+
<td class="modified"><%= log_file[:last_modified].strftime("%b %d, %Y %H:%M") %></td>
|
91
|
+
<td class="status">
|
92
|
+
<% if log_file[:status] == :ok %>
|
93
|
+
<span class="status-icon">✅</span>
|
94
|
+
<% elsif log_file[:status] == :warning %>
|
95
|
+
<span class="status-icon">⚠️</span>
|
96
|
+
<% else %>
|
97
|
+
<span class="status-icon">❌</span>
|
98
|
+
<% end %>
|
99
|
+
</td>
|
100
|
+
<td class="actions">
|
101
|
+
<button class="truncate-file-btn" data-filename="<%= log_file[:filename] %>">
|
102
|
+
<span class="action-icon">✂️</span> Truncate
|
103
|
+
</button>
|
104
|
+
</td>
|
105
|
+
</tr>
|
106
|
+
<% end %>
|
107
|
+
</tbody>
|
108
|
+
</table>
|
109
|
+
</div>
|
110
|
+
|
111
|
+
<div class="log-recommendations">
|
112
|
+
<h4>Recommendations</h4>
|
113
|
+
<ul>
|
114
|
+
<% if @log_data[:status] == :danger %>
|
115
|
+
<li><strong>Immediate action required:</strong> Your log files exceed 50MB in total. Consider truncating them now.</li>
|
116
|
+
<% elsif @log_data[:status] == :warning %>
|
117
|
+
<li><strong>Monitor closely:</strong> Your log files exceed 25MB in total. Consider truncating them soon.</li>
|
118
|
+
<% end %>
|
119
|
+
<li>Configure log rotation to automatically manage log file sizes</li>
|
120
|
+
<li>For production environments, consider using a log management service</li>
|
121
|
+
</ul>
|
122
|
+
</div>
|
123
|
+
</div>
|
124
|
+
|
125
|
+
<style>
|
126
|
+
.log-monitor-card {
|
127
|
+
background-color: #fff;
|
128
|
+
border-radius: 8px;
|
129
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
130
|
+
overflow: hidden;
|
131
|
+
margin-top: 1rem;
|
132
|
+
margin-bottom: 1.5rem;
|
133
|
+
}
|
134
|
+
|
135
|
+
.log-header {
|
136
|
+
padding: 1rem;
|
137
|
+
display: flex;
|
138
|
+
justify-content: space-between;
|
139
|
+
align-items: flex-start;
|
140
|
+
border-bottom: 1px solid #eee;
|
141
|
+
}
|
142
|
+
|
143
|
+
.log-header h3 {
|
144
|
+
font-size: 1.2rem;
|
145
|
+
margin: 0 0 0.5rem;
|
146
|
+
font-weight: 600;
|
147
|
+
display: flex;
|
148
|
+
align-items: center;
|
149
|
+
gap: 0.5rem;
|
150
|
+
}
|
151
|
+
|
152
|
+
.log-path {
|
153
|
+
font-family: monospace;
|
154
|
+
font-size: 0.8rem;
|
155
|
+
color: #666;
|
156
|
+
margin: 0;
|
157
|
+
word-break: break-all;
|
158
|
+
}
|
159
|
+
|
160
|
+
.log-actions {
|
161
|
+
flex-shrink: 0;
|
162
|
+
}
|
163
|
+
|
164
|
+
.truncate-button {
|
165
|
+
display: inline-flex;
|
166
|
+
align-items: center;
|
167
|
+
padding: 0.5rem 1rem;
|
168
|
+
background-color: #f8f9fa;
|
169
|
+
border: 1px solid #dee2e6;
|
170
|
+
color: #333;
|
171
|
+
border-radius: 4px;
|
172
|
+
font-size: 0.9rem;
|
173
|
+
cursor: pointer;
|
174
|
+
transition: all 0.2s;
|
175
|
+
}
|
176
|
+
|
177
|
+
.truncate-button:hover {
|
178
|
+
background-color: #e9ecef;
|
179
|
+
}
|
180
|
+
|
181
|
+
.log-content {
|
182
|
+
padding: 1rem;
|
183
|
+
}
|
184
|
+
|
185
|
+
.log-status {
|
186
|
+
display: flex;
|
187
|
+
align-items: center;
|
188
|
+
gap: 0.75rem;
|
189
|
+
margin-bottom: 1rem;
|
190
|
+
}
|
191
|
+
|
192
|
+
.log-indicator {
|
193
|
+
width: 12px;
|
194
|
+
height: 12px;
|
195
|
+
border-radius: 50%;
|
196
|
+
background-color: #6c757d;
|
197
|
+
}
|
198
|
+
|
199
|
+
.log-indicator.status-ok {
|
200
|
+
background-color: #28a745;
|
201
|
+
}
|
202
|
+
|
203
|
+
.log-indicator.status-warning {
|
204
|
+
background-color: #ffc107;
|
205
|
+
}
|
206
|
+
|
207
|
+
.log-indicator.status-danger {
|
208
|
+
background-color: #dc3545;
|
209
|
+
}
|
210
|
+
|
211
|
+
.log-status-text {
|
212
|
+
font-weight: 500;
|
213
|
+
}
|
214
|
+
|
215
|
+
.log-size-container {
|
216
|
+
display: flex;
|
217
|
+
align-items: center;
|
218
|
+
gap: 0.5rem;
|
219
|
+
margin-bottom: 0.75rem;
|
220
|
+
}
|
221
|
+
|
222
|
+
.log-size-label {
|
223
|
+
font-size: 0.9rem;
|
224
|
+
color: #495057;
|
225
|
+
}
|
226
|
+
|
227
|
+
.log-size-value {
|
228
|
+
font-weight: 600;
|
229
|
+
font-size: 1.1rem;
|
230
|
+
}
|
231
|
+
|
232
|
+
.log-files-count {
|
233
|
+
font-size: 0.9rem;
|
234
|
+
color: #6c757d;
|
235
|
+
}
|
236
|
+
|
237
|
+
.log-size-meter {
|
238
|
+
position: relative;
|
239
|
+
height: 30px;
|
240
|
+
background-color: #f8f9fa;
|
241
|
+
border-radius: 4px;
|
242
|
+
overflow: hidden;
|
243
|
+
margin-top: 1rem;
|
244
|
+
margin-bottom: 1rem;
|
245
|
+
}
|
246
|
+
|
247
|
+
.meter-bg {
|
248
|
+
position: absolute;
|
249
|
+
top: 0;
|
250
|
+
left: 0;
|
251
|
+
width: 100%;
|
252
|
+
height: 100%;
|
253
|
+
background: linear-gradient(to right, #28a745 0%, #28a745 50%, #ffc107 50%, #ffc107 75%, #dc3545 75%, #dc3545 100%);
|
254
|
+
opacity: 0.15;
|
255
|
+
}
|
256
|
+
|
257
|
+
.meter-fill {
|
258
|
+
position: absolute;
|
259
|
+
top: 0;
|
260
|
+
left: 0;
|
261
|
+
height: 100%;
|
262
|
+
transition: width 0.5s;
|
263
|
+
}
|
264
|
+
|
265
|
+
.meter-markers {
|
266
|
+
position: absolute;
|
267
|
+
bottom: 0;
|
268
|
+
left: 0;
|
269
|
+
width: 100%;
|
270
|
+
height: 100%;
|
271
|
+
display: flex;
|
272
|
+
align-items: flex-end;
|
273
|
+
padding-bottom: 0.25rem;
|
274
|
+
}
|
275
|
+
|
276
|
+
.marker {
|
277
|
+
position: absolute;
|
278
|
+
font-size: 0.7rem;
|
279
|
+
transform: translateX(-50%);
|
280
|
+
padding: 2px 4px;
|
281
|
+
color: #495057;
|
282
|
+
}
|
283
|
+
|
284
|
+
.warning-marker {
|
285
|
+
color: #856404;
|
286
|
+
}
|
287
|
+
|
288
|
+
.danger-marker {
|
289
|
+
color: #721c24;
|
290
|
+
}
|
291
|
+
|
292
|
+
.log-files-table {
|
293
|
+
padding: 1rem;
|
294
|
+
border-top: 1px solid #eee;
|
295
|
+
}
|
296
|
+
|
297
|
+
.log-files-table h4 {
|
298
|
+
font-size: 1rem;
|
299
|
+
margin-top: 0;
|
300
|
+
margin-bottom: 0.75rem;
|
301
|
+
}
|
302
|
+
|
303
|
+
.log-files-table table {
|
304
|
+
width: 100%;
|
305
|
+
border-collapse: collapse;
|
306
|
+
font-size: 0.9rem;
|
307
|
+
}
|
308
|
+
|
309
|
+
.log-files-table th,
|
310
|
+
.log-files-table td {
|
311
|
+
padding: 0.5rem;
|
312
|
+
text-align: left;
|
313
|
+
border-bottom: 1px solid #eee;
|
314
|
+
}
|
315
|
+
|
316
|
+
.log-files-table th {
|
317
|
+
font-weight: 600;
|
318
|
+
color: #495057;
|
319
|
+
}
|
320
|
+
|
321
|
+
.log-files-table tr:last-child td {
|
322
|
+
border-bottom: none;
|
323
|
+
}
|
324
|
+
|
325
|
+
.log-files-table .filename {
|
326
|
+
font-family: monospace;
|
327
|
+
max-width: 200px;
|
328
|
+
overflow: hidden;
|
329
|
+
text-overflow: ellipsis;
|
330
|
+
white-space: nowrap;
|
331
|
+
}
|
332
|
+
|
333
|
+
.log-files-table .status {
|
334
|
+
text-align: center;
|
335
|
+
}
|
336
|
+
|
337
|
+
.log-files-table .actions {
|
338
|
+
text-align: right;
|
339
|
+
}
|
340
|
+
|
341
|
+
.log-files-table .truncate-file-btn {
|
342
|
+
padding: 0.25rem 0.5rem;
|
343
|
+
font-size: 0.8rem;
|
344
|
+
background-color: #f8f9fa;
|
345
|
+
border: 1px solid #dee2e6;
|
346
|
+
border-radius: 4px;
|
347
|
+
cursor: pointer;
|
348
|
+
display: inline-flex;
|
349
|
+
align-items: center;
|
350
|
+
gap: 0.25rem;
|
351
|
+
}
|
352
|
+
|
353
|
+
.log-files-table .truncate-file-btn:hover {
|
354
|
+
background-color: #e9ecef;
|
355
|
+
}
|
356
|
+
|
357
|
+
.log-files-table tr.status-danger {
|
358
|
+
background-color: rgba(220, 53, 69, 0.05);
|
359
|
+
}
|
360
|
+
|
361
|
+
.log-files-table tr.status-warning {
|
362
|
+
background-color: rgba(255, 193, 7, 0.05);
|
363
|
+
}
|
364
|
+
|
365
|
+
.log-recommendations {
|
366
|
+
padding: 1rem;
|
367
|
+
background-color: #f8f9fa;
|
368
|
+
border-top: 1px solid #eee;
|
369
|
+
}
|
370
|
+
|
371
|
+
.log-recommendations h4 {
|
372
|
+
font-size: 1rem;
|
373
|
+
margin-top: 0;
|
374
|
+
margin-bottom: 0.5rem;
|
375
|
+
}
|
376
|
+
|
377
|
+
.log-recommendations ul {
|
378
|
+
margin-bottom: 0;
|
379
|
+
padding-left: 1.5rem;
|
380
|
+
}
|
381
|
+
|
382
|
+
.log-recommendations li {
|
383
|
+
margin-bottom: 0.3rem;
|
384
|
+
font-size: 0.9rem;
|
385
|
+
}
|
386
|
+
|
387
|
+
.log-recommendations li:last-child {
|
388
|
+
margin-bottom: 0;
|
389
|
+
}
|
390
|
+
</style>
|
391
|
+
|
392
|
+
<script>
|
393
|
+
document.addEventListener('DOMContentLoaded', function() {
|
394
|
+
// Truncate all logs
|
395
|
+
const truncateAllLogsBtn = document.getElementById('truncate-all-logs-btn');
|
396
|
+
if (truncateAllLogsBtn) {
|
397
|
+
truncateAllLogsBtn.addEventListener('click', function() {
|
398
|
+
// Confirm before truncating
|
399
|
+
if (!confirm('Are you sure you want to truncate ALL log files? This action cannot be undone.')) {
|
400
|
+
return;
|
401
|
+
}
|
402
|
+
|
403
|
+
truncateLog(null, this);
|
404
|
+
});
|
405
|
+
}
|
406
|
+
|
407
|
+
// Individual file truncate buttons
|
408
|
+
document.querySelectorAll('.truncate-file-btn').forEach(function(button) {
|
409
|
+
button.addEventListener('click', function() {
|
410
|
+
const filename = this.getAttribute('data-filename');
|
411
|
+
|
412
|
+
// Confirm before truncating
|
413
|
+
if (!confirm(`Are you sure you want to truncate log file: ${filename}? This action cannot be undone.`)) {
|
414
|
+
return;
|
415
|
+
}
|
416
|
+
|
417
|
+
truncateLog(filename, this);
|
418
|
+
});
|
419
|
+
});
|
420
|
+
|
421
|
+
function truncateLog(filename, button) {
|
422
|
+
const originalText = button.innerHTML;
|
423
|
+
button.innerHTML = '<span class="action-icon">⏳</span> Truncating...';
|
424
|
+
button.disabled = true;
|
425
|
+
|
426
|
+
// Strip .log extension from filename if present (controller will add it back)
|
427
|
+
let urlFilename = filename;
|
428
|
+
if (urlFilename && urlFilename.endsWith('.log')) {
|
429
|
+
urlFilename = urlFilename.substring(0, urlFilename.length - 4);
|
430
|
+
}
|
431
|
+
|
432
|
+
const url = urlFilename ?
|
433
|
+
'<%= solidstats.truncate_log_path %>/' + encodeURIComponent(urlFilename) :
|
434
|
+
'<%= solidstats.truncate_log_path %>';
|
435
|
+
|
436
|
+
fetch(url, {
|
437
|
+
method: 'POST',
|
438
|
+
headers: {
|
439
|
+
'Accept': 'application/json',
|
440
|
+
'X-Requested-With': 'XMLHttpRequest',
|
441
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
442
|
+
},
|
443
|
+
credentials: 'same-origin'
|
444
|
+
})
|
445
|
+
.then(response => response.json())
|
446
|
+
.then(data => {
|
447
|
+
if (data.success) {
|
448
|
+
showNotification(data.message, 'success');
|
449
|
+
// Reload the page to see updated log sizes
|
450
|
+
window.location.reload();
|
451
|
+
} else {
|
452
|
+
showNotification(data.message, 'error');
|
453
|
+
// Reset button state
|
454
|
+
button.innerHTML = originalText;
|
455
|
+
button.disabled = false;
|
456
|
+
}
|
457
|
+
})
|
458
|
+
.catch(error => {
|
459
|
+
console.error('Error truncating log file:', error);
|
460
|
+
showNotification('Failed to truncate log file', 'error');
|
461
|
+
|
462
|
+
// Reset button state
|
463
|
+
button.innerHTML = originalText;
|
464
|
+
button.disabled = false;
|
465
|
+
});
|
466
|
+
}
|
467
|
+
});
|
468
|
+
|
469
|
+
// Notification function if not already defined
|
470
|
+
if (typeof showNotification !== 'function') {
|
471
|
+
function showNotification(message, type) {
|
472
|
+
// Create notification element
|
473
|
+
const notification = document.createElement('div');
|
474
|
+
notification.className = `toast-notification ${type}`;
|
475
|
+
notification.textContent = message;
|
476
|
+
|
477
|
+
// Add to body
|
478
|
+
document.body.appendChild(notification);
|
479
|
+
|
480
|
+
// Show notification
|
481
|
+
setTimeout(() => {
|
482
|
+
notification.classList.add('visible');
|
483
|
+
}, 10);
|
484
|
+
|
485
|
+
// Hide and remove notification
|
486
|
+
setTimeout(() => {
|
487
|
+
notification.classList.remove('visible');
|
488
|
+
setTimeout(() => {
|
489
|
+
notification.remove();
|
490
|
+
}, 300);
|
491
|
+
}, 5000);
|
492
|
+
}
|
493
|
+
}
|
494
|
+
</script>
|
495
|
+
|
496
|
+
<style>
|
497
|
+
.log-monitor-card {
|
498
|
+
background-color: #fff;
|
499
|
+
border-radius: 8px;
|
500
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
501
|
+
overflow: hidden;
|
502
|
+
margin-top: 1rem;
|
503
|
+
margin-bottom: 1.5rem;
|
504
|
+
}
|
505
|
+
|
506
|
+
.log-header {
|
507
|
+
padding: 1rem;
|
508
|
+
display: flex;
|
509
|
+
justify-content: space-between;
|
510
|
+
align-items: flex-start;
|
511
|
+
border-bottom: 1px solid #eee;
|
512
|
+
}
|
513
|
+
|
514
|
+
.log-header h3 {
|
515
|
+
font-size: 1.2rem;
|
516
|
+
margin: 0 0 0.5rem;
|
517
|
+
font-weight: 600;
|
518
|
+
display: flex;
|
519
|
+
align-items: center;
|
520
|
+
gap: 0.5rem;
|
521
|
+
}
|
522
|
+
|
523
|
+
.log-path {
|
524
|
+
font-family: monospace;
|
525
|
+
font-size: 0.8rem;
|
526
|
+
color: #666;
|
527
|
+
margin: 0;
|
528
|
+
word-break: break-all;
|
529
|
+
}
|
530
|
+
|
531
|
+
.log-actions {
|
532
|
+
flex-shrink: 0;
|
533
|
+
}
|
534
|
+
|
535
|
+
.truncate-button {
|
536
|
+
display: inline-flex;
|
537
|
+
align-items: center;
|
538
|
+
padding: 0.5rem 1rem;
|
539
|
+
background-color: #f8f9fa;
|
540
|
+
border: 1px solid #dee2e6;
|
541
|
+
color: #333;
|
542
|
+
border-radius: 4px;
|
543
|
+
font-size: 0.9rem;
|
544
|
+
cursor: pointer;
|
545
|
+
transition: all 0.2s;
|
546
|
+
}
|
547
|
+
|
548
|
+
.truncate-button:hover {
|
549
|
+
background-color: #e9ecef;
|
550
|
+
}
|
551
|
+
|
552
|
+
.log-content {
|
553
|
+
padding: 1rem;
|
554
|
+
}
|
555
|
+
|
556
|
+
.log-status {
|
557
|
+
display: flex;
|
558
|
+
align-items: center;
|
559
|
+
gap: 0.75rem;
|
560
|
+
margin-bottom: 1rem;
|
561
|
+
}
|
562
|
+
|
563
|
+
.log-indicator {
|
564
|
+
width: 12px;
|
565
|
+
height: 12px;
|
566
|
+
border-radius: 50%;
|
567
|
+
background-color: #6c757d;
|
568
|
+
}
|
569
|
+
|
570
|
+
.log-indicator.status-ok {
|
571
|
+
background-color: #28a745;
|
572
|
+
}
|
573
|
+
|
574
|
+
.log-indicator.status-warning {
|
575
|
+
background-color: #ffc107;
|
576
|
+
}
|
577
|
+
|
578
|
+
.log-indicator.status-danger {
|
579
|
+
background-color: #dc3545;
|
580
|
+
}
|
581
|
+
|
582
|
+
.log-status-text {
|
583
|
+
font-weight: 500;
|
584
|
+
}
|
585
|
+
|
586
|
+
.log-size-container {
|
587
|
+
display: flex;
|
588
|
+
align-items: center;
|
589
|
+
gap: 0.5rem;
|
590
|
+
margin-bottom: 0.75rem;
|
591
|
+
}
|
592
|
+
|
593
|
+
.log-size-label {
|
594
|
+
font-size: 0.9rem;
|
595
|
+
color: #495057;
|
596
|
+
}
|
597
|
+
|
598
|
+
.log-size-value {
|
599
|
+
font-weight: 600;
|
600
|
+
font-size: 1.1rem;
|
601
|
+
}
|
602
|
+
|
603
|
+
.log-size-meter {
|
604
|
+
position: relative;
|
605
|
+
height: 30px;
|
606
|
+
background-color: #f8f9fa;
|
607
|
+
border-radius: 4px;
|
608
|
+
overflow: hidden;
|
609
|
+
margin-top: 1rem;
|
610
|
+
margin-bottom: 1rem;
|
611
|
+
}
|
612
|
+
|
613
|
+
.meter-bg {
|
614
|
+
position: absolute;
|
615
|
+
top: 0;
|
616
|
+
left: 0;
|
617
|
+
width: 100%;
|
618
|
+
height: 100%;
|
619
|
+
background: linear-gradient(to right, #28a745 0%, #28a745 50%, #ffc107 50%, #ffc107 75%, #dc3545 75%, #dc3545 100%);
|
620
|
+
opacity: 0.15;
|
621
|
+
}
|
622
|
+
|
623
|
+
.meter-fill {
|
624
|
+
position: absolute;
|
625
|
+
top: 0;
|
626
|
+
left: 0;
|
627
|
+
height: 100%;
|
628
|
+
transition: width 0.5s;
|
629
|
+
}
|
630
|
+
|
631
|
+
.meter-markers {
|
632
|
+
position: absolute;
|
633
|
+
bottom: 0;
|
634
|
+
left: 0;
|
635
|
+
width: 100%;
|
636
|
+
height: 100%;
|
637
|
+
display: flex;
|
638
|
+
align-items: flex-end;
|
639
|
+
padding-bottom: 0.25rem;
|
640
|
+
}
|
641
|
+
|
642
|
+
.marker {
|
643
|
+
position: absolute;
|
644
|
+
font-size: 0.7rem;
|
645
|
+
transform: translateX(-50%);
|
646
|
+
padding: 2px 4px;
|
647
|
+
color: #495057;
|
648
|
+
}
|
649
|
+
|
650
|
+
.warning-marker {
|
651
|
+
color: #856404;
|
652
|
+
}
|
653
|
+
|
654
|
+
.danger-marker {
|
655
|
+
color: #721c24;
|
656
|
+
}
|
657
|
+
|
658
|
+
.log-recommendations {
|
659
|
+
padding: 1rem;
|
660
|
+
background-color: #f8f9fa;
|
661
|
+
border-top: 1px solid #eee;
|
662
|
+
}
|
663
|
+
|
664
|
+
.log-recommendations h4 {
|
665
|
+
font-size: 1rem;
|
666
|
+
margin-top: 0;
|
667
|
+
margin-bottom: 0.5rem;
|
668
|
+
}
|
669
|
+
|
670
|
+
.log-recommendations ul {
|
671
|
+
margin-bottom: 0;
|
672
|
+
padding-left: 1.5rem;
|
673
|
+
}
|
674
|
+
|
675
|
+
.log-recommendations li {
|
676
|
+
margin-bottom: 0.3rem;
|
677
|
+
font-size: 0.9rem;
|
678
|
+
}
|
679
|
+
|
680
|
+
.log-recommendations li:last-child {
|
681
|
+
margin-bottom: 0;
|
682
|
+
}
|
683
|
+
</style>
|
684
|
+
|
685
|
+
<script>
|
686
|
+
document.addEventListener('DOMContentLoaded', function() {
|
687
|
+
const truncateButton = document.getElementById('truncate-log-btn');
|
688
|
+
if (truncateButton) {
|
689
|
+
truncateButton.addEventListener('click', function() {
|
690
|
+
// Confirm before truncating
|
691
|
+
if (!confirm('Are you sure you want to truncate the log file? This action cannot be undone.')) {
|
692
|
+
return;
|
693
|
+
}
|
694
|
+
|
695
|
+
const button = this;
|
696
|
+
const originalText = button.innerHTML;
|
697
|
+
button.innerHTML = '<span class="action-icon">⏳</span> Truncating...';
|
698
|
+
button.disabled = true;
|
699
|
+
|
700
|
+
fetch('<%= solidstats.truncate_log_path %>', {
|
701
|
+
method: 'POST',
|
702
|
+
headers: {
|
703
|
+
'Accept': 'application/json',
|
704
|
+
'X-Requested-With': 'XMLHttpRequest',
|
705
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
706
|
+
},
|
707
|
+
credentials: 'same-origin'
|
708
|
+
})
|
709
|
+
.then(response => response.json())
|
710
|
+
.then(data => {
|
711
|
+
if (data.success) {
|
712
|
+
showNotification(data.message, 'success');
|
713
|
+
// Reload the page to see updated log size
|
714
|
+
window.location.reload();
|
715
|
+
} else {
|
716
|
+
showNotification(data.message, 'error');
|
717
|
+
// Reset button state
|
718
|
+
button.innerHTML = originalText;
|
719
|
+
button.disabled = false;
|
720
|
+
}
|
721
|
+
})
|
722
|
+
.catch(error => {
|
723
|
+
console.error('Error truncating log file:', error);
|
724
|
+
showNotification('Failed to truncate log file', 'error');
|
725
|
+
|
726
|
+
// Reset button state
|
727
|
+
button.innerHTML = originalText;
|
728
|
+
button.disabled = false;
|
729
|
+
});
|
730
|
+
});
|
731
|
+
}
|
732
|
+
});
|
733
|
+
|
734
|
+
// Notification function if not already defined
|
735
|
+
if (typeof showNotification !== 'function') {
|
736
|
+
function showNotification(message, type) {
|
737
|
+
// Create notification element
|
738
|
+
const notification = document.createElement('div');
|
739
|
+
notification.className = `toast-notification ${type}`;
|
740
|
+
notification.textContent = message;
|
741
|
+
|
742
|
+
// Add to body
|
743
|
+
document.body.appendChild(notification);
|
744
|
+
|
745
|
+
// Show notification
|
746
|
+
setTimeout(() => {
|
747
|
+
notification.classList.add('visible');
|
748
|
+
}, 10);
|
749
|
+
|
750
|
+
// Hide and remove notification
|
751
|
+
setTimeout(() => {
|
752
|
+
notification.classList.remove('visible');
|
753
|
+
setTimeout(() => {
|
754
|
+
notification.remove();
|
755
|
+
}, 300);
|
756
|
+
}, 5000);
|
757
|
+
}
|
758
|
+
}
|
759
|
+
</script>
|
@@ -53,6 +53,14 @@
|
|
53
53
|
<div class="summary-label">Test Coverage</div>
|
54
54
|
</div>
|
55
55
|
</div>
|
56
|
+
|
57
|
+
<div class="summary-card status-<%= @log_data[:status] %>" data-section="code-quality" data-tab="log-monitor">
|
58
|
+
<div class="summary-icon">📊</div>
|
59
|
+
<div class="summary-data">
|
60
|
+
<div class="summary-value"><%= @log_data[:total_size_mb] %> MB</div>
|
61
|
+
<div class="summary-label">Log Files Size</div>
|
62
|
+
</div>
|
63
|
+
</div>
|
56
64
|
</div>
|
57
65
|
</section>
|
58
66
|
|
@@ -237,6 +245,9 @@
|
|
237
245
|
<button class="tab-button active" data-tab="quality-metrics">Metrics</button>
|
238
246
|
<button class="tab-button" data-tab="test-coverage">Test Coverage</button>
|
239
247
|
<button class="tab-button" data-tab="code-health">Code Health</button>
|
248
|
+
<button class="tab-button" data-tab="log-monitor">
|
249
|
+
<span class="tab-icon">📊</span> Log Monitor
|
250
|
+
</button>
|
240
251
|
</div>
|
241
252
|
|
242
253
|
<div class="tabs-content">
|
@@ -271,6 +282,9 @@
|
|
271
282
|
</div>
|
272
283
|
</div>
|
273
284
|
</div>
|
285
|
+
<div class="tab-content" id="log-monitor">
|
286
|
+
<%= render partial: 'solidstats/dashboard/log_monitor' %>
|
287
|
+
</div>
|
274
288
|
</div>
|
275
289
|
</div>
|
276
290
|
</section>
|
data/config/routes.rb
CHANGED
@@ -1,4 +1,8 @@
|
|
1
1
|
Solidstats::Engine.routes.draw do
|
2
2
|
root to: "dashboard#index"
|
3
3
|
get "refresh", to: "dashboard#refresh", as: :refresh
|
4
|
+
|
5
|
+
# Route for truncating logs - accepts filename without extension
|
6
|
+
post "truncate-log(/:filename)", to: "dashboard#truncate_log", as: :truncate_log,
|
7
|
+
constraints: { filename: /[^\.]+/ }
|
4
8
|
end
|
data/lib/solidstats/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solidstats
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- MezbahAlam
|
@@ -59,8 +59,10 @@ files:
|
|
59
59
|
- app/models/solidstats/application_record.rb
|
60
60
|
- app/services/solidstats/audit_service.rb
|
61
61
|
- app/services/solidstats/data_collector_service.rb
|
62
|
+
- app/services/solidstats/log_size_monitor_service.rb
|
62
63
|
- app/services/solidstats/todo_service.rb
|
63
64
|
- app/views/layouts/solidstats/application.html.erb
|
65
|
+
- app/views/solidstats/dashboard/_log_monitor.html.erb
|
64
66
|
- app/views/solidstats/dashboard/_todos.html.erb
|
65
67
|
- app/views/solidstats/dashboard/audit/_additional_styles.css
|
66
68
|
- app/views/solidstats/dashboard/audit/_audit_badge.html.erb
|