newshound 0.1.1 → 0.2.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 +152 -155
- data/lib/generators/newshound/install/install_generator.rb +9 -50
- data/lib/generators/newshound/install/templates/newshound.rb +27 -33
- data/lib/newshound/authorization.rb +46 -0
- data/lib/newshound/configuration.rb +12 -18
- data/lib/newshound/exception_reporter.rb +91 -12
- data/lib/newshound/middleware/banner_injector.rb +288 -0
- data/lib/newshound/que_reporter.rb +54 -20
- data/lib/newshound/railtie.rb +32 -36
- data/lib/newshound/version.rb +1 -1
- data/lib/newshound.rb +5 -43
- data/newshound.gemspec +2 -7
- metadata +6 -52
- data/lib/newshound/daily_report_job.rb +0 -31
- data/lib/newshound/scheduler.rb +0 -42
- data/lib/newshound/slack_notifier.rb +0 -44
- data/lib/newshound/transport/base.rb +0 -28
- data/lib/newshound/transport/slack.rb +0 -67
- data/lib/newshound/transport/sns.rb +0 -115
|
@@ -24,6 +24,14 @@ module Newshound
|
|
|
24
24
|
*format_exceptions
|
|
25
25
|
]
|
|
26
26
|
end
|
|
27
|
+
alias_method :report, :generate_report
|
|
28
|
+
|
|
29
|
+
# Returns data formatted for the banner UI
|
|
30
|
+
def banner_data
|
|
31
|
+
{
|
|
32
|
+
exceptions: recent_exceptions.map { |exception| format_exception_for_banner(exception) }
|
|
33
|
+
}
|
|
34
|
+
end
|
|
27
35
|
|
|
28
36
|
private
|
|
29
37
|
|
|
@@ -53,29 +61,100 @@ module Newshound
|
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
def format_exception_text(exception, number)
|
|
64
|
+
details = parse_exception_details(exception)
|
|
65
|
+
|
|
56
66
|
<<~TEXT
|
|
57
|
-
*#{number}. #{exception
|
|
67
|
+
*#{number}. #{exception_title(exception)}*
|
|
58
68
|
• *Time:* #{exception.created_at.strftime('%I:%M %p')}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
#{format_message(exception)}
|
|
69
|
+
#{format_controller(details)}
|
|
70
|
+
#{format_message(exception, details)}
|
|
62
71
|
TEXT
|
|
63
72
|
end
|
|
64
73
|
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
def exception_title(exception)
|
|
75
|
+
if exception.respond_to?(:title) && exception.title.present?
|
|
76
|
+
exception.title
|
|
77
|
+
elsif exception.respond_to?(:exception_class) && exception.exception_class.present?
|
|
78
|
+
exception.exception_class
|
|
79
|
+
else
|
|
80
|
+
"Unknown Exception"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def parse_exception_details(exception)
|
|
85
|
+
return {} unless exception.respond_to?(:body) && exception.body.present?
|
|
86
|
+
|
|
87
|
+
JSON.parse(exception.body)
|
|
88
|
+
rescue JSON::ParserError
|
|
89
|
+
{}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def format_controller(details)
|
|
93
|
+
return String.new unless details["controller_name"] && details["action_name"]
|
|
94
|
+
|
|
95
|
+
"• *Controller:* #{details['controller_name']}##{details['action_name']}\n"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def format_message(exception, details = nil)
|
|
99
|
+
details ||= parse_exception_details(exception)
|
|
100
|
+
|
|
101
|
+
# Try to get message from different sources
|
|
102
|
+
message = if details["message"].present?
|
|
103
|
+
details["message"]
|
|
104
|
+
elsif exception.respond_to?(:message) && exception.message.present?
|
|
105
|
+
exception.message
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
return String.new unless message.present?
|
|
109
|
+
|
|
110
|
+
message = message.to_s.truncate(100)
|
|
69
111
|
"• *Message:* `#{message}`"
|
|
70
112
|
end
|
|
71
113
|
|
|
72
114
|
def exception_count(exception)
|
|
73
115
|
return 0 unless exception_source
|
|
74
116
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
117
|
+
# Use title for exception-track, exception_class for other systems
|
|
118
|
+
if exception.respond_to?(:title) && exception.title.present?
|
|
119
|
+
exception_source
|
|
120
|
+
.where(title: exception.title)
|
|
121
|
+
.where("created_at >= ?", time_range.ago)
|
|
122
|
+
.count
|
|
123
|
+
elsif exception.respond_to?(:exception_class)
|
|
124
|
+
exception_source
|
|
125
|
+
.where(exception_class: exception.exception_class)
|
|
126
|
+
.where("created_at >= ?", time_range.ago)
|
|
127
|
+
.count
|
|
128
|
+
else
|
|
129
|
+
0
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def format_exception_for_banner(exception)
|
|
134
|
+
details = parse_exception_details(exception)
|
|
135
|
+
|
|
136
|
+
# Extract message
|
|
137
|
+
message = if details["message"].present?
|
|
138
|
+
details["message"].to_s
|
|
139
|
+
elsif exception.respond_to?(:message) && exception.message.present?
|
|
140
|
+
exception.message.to_s
|
|
141
|
+
else
|
|
142
|
+
String.new
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Extract location
|
|
146
|
+
location = if details["controller_name"] && details["action_name"]
|
|
147
|
+
"#{details['controller_name']}##{details['action_name']}"
|
|
148
|
+
else
|
|
149
|
+
String.new
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
title: exception_title(exception),
|
|
154
|
+
message: message.truncate(100),
|
|
155
|
+
location: location,
|
|
156
|
+
time: exception.created_at.strftime('%I:%M %p')
|
|
157
|
+
}
|
|
79
158
|
end
|
|
80
159
|
|
|
81
160
|
def no_exceptions_block
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Newshound
|
|
4
|
+
module Middleware
|
|
5
|
+
class BannerInjector
|
|
6
|
+
def initialize(app)
|
|
7
|
+
@app = app
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(env)
|
|
11
|
+
status, headers, response = @app.call(env)
|
|
12
|
+
|
|
13
|
+
# Only inject into HTML responses
|
|
14
|
+
return [status, headers, response] unless html_response?(headers)
|
|
15
|
+
return [status, headers, response] unless status == 200
|
|
16
|
+
|
|
17
|
+
# Check authorization
|
|
18
|
+
controller = env['action_controller.instance']
|
|
19
|
+
return [status, headers, response] unless controller
|
|
20
|
+
return [status, headers, response] unless Newshound::Authorization.authorized?(controller)
|
|
21
|
+
|
|
22
|
+
# Get banner HTML
|
|
23
|
+
banner_html = generate_banner_html
|
|
24
|
+
|
|
25
|
+
# Inject banner after <body> tag
|
|
26
|
+
new_response = inject_banner(response, banner_html)
|
|
27
|
+
|
|
28
|
+
# Update Content-Length header
|
|
29
|
+
if headers['Content-Length']
|
|
30
|
+
headers['Content-Length'] = new_response.bytesize.to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
[status, headers, [new_response]]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def html_response?(headers)
|
|
39
|
+
content_type = headers['Content-Type']
|
|
40
|
+
content_type && content_type.include?('text/html')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def inject_banner(response, banner_html)
|
|
44
|
+
body = response_body(response)
|
|
45
|
+
|
|
46
|
+
# Inject after <body> tag
|
|
47
|
+
body.sub(/(<body[^>]*>)/i, "\\1\n#{banner_html}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def response_body(response)
|
|
51
|
+
body = String.new
|
|
52
|
+
response.each { |part| body << part }
|
|
53
|
+
body
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def generate_banner_html
|
|
57
|
+
exception_reporter = Newshound::ExceptionReporter.new
|
|
58
|
+
que_reporter = Newshound::QueReporter.new
|
|
59
|
+
|
|
60
|
+
exception_data = exception_reporter.banner_data
|
|
61
|
+
job_data = que_reporter.banner_data
|
|
62
|
+
|
|
63
|
+
# Generate HTML from template
|
|
64
|
+
render_banner(exception_data, job_data)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_banner(exception_data, job_data)
|
|
68
|
+
<<~HTML
|
|
69
|
+
<div id="newshound-banner" class="newshound-banner newshound-collapsed">
|
|
70
|
+
#{render_styles}
|
|
71
|
+
<div class="newshound-header" onclick="document.getElementById('newshound-banner').classList.toggle('newshound-collapsed')">
|
|
72
|
+
<span class="newshound-title">
|
|
73
|
+
🐕 Newshound
|
|
74
|
+
#{summary_badge(exception_data, job_data)}
|
|
75
|
+
</span>
|
|
76
|
+
<span class="newshound-toggle">▼</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="newshound-content">
|
|
79
|
+
#{render_exceptions(exception_data)}
|
|
80
|
+
#{render_jobs(job_data)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
HTML
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def render_styles
|
|
87
|
+
<<~CSS
|
|
88
|
+
<style>
|
|
89
|
+
.newshound-banner {
|
|
90
|
+
position: fixed;
|
|
91
|
+
top: 0;
|
|
92
|
+
left: 0;
|
|
93
|
+
right: 0;
|
|
94
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
95
|
+
color: white;
|
|
96
|
+
z-index: 10000;
|
|
97
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
98
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
99
|
+
font-size: 14px;
|
|
100
|
+
}
|
|
101
|
+
.newshound-header {
|
|
102
|
+
padding: 12px 20px;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
display: flex;
|
|
105
|
+
justify-content: space-between;
|
|
106
|
+
align-items: center;
|
|
107
|
+
user-select: none;
|
|
108
|
+
}
|
|
109
|
+
.newshound-title {
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 10px;
|
|
114
|
+
}
|
|
115
|
+
.newshound-badge {
|
|
116
|
+
display: inline-block;
|
|
117
|
+
padding: 2px 8px;
|
|
118
|
+
border-radius: 12px;
|
|
119
|
+
font-size: 12px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
background: rgba(255,255,255,0.2);
|
|
122
|
+
}
|
|
123
|
+
.newshound-badge.error {
|
|
124
|
+
background: #ef4444;
|
|
125
|
+
}
|
|
126
|
+
.newshound-badge.warning {
|
|
127
|
+
background: #f59e0b;
|
|
128
|
+
}
|
|
129
|
+
.newshound-badge.success {
|
|
130
|
+
background: #10b981;
|
|
131
|
+
}
|
|
132
|
+
.newshound-toggle {
|
|
133
|
+
transition: transform 0.3s;
|
|
134
|
+
}
|
|
135
|
+
.newshound-banner.newshound-collapsed .newshound-toggle {
|
|
136
|
+
transform: rotate(-90deg);
|
|
137
|
+
}
|
|
138
|
+
.newshound-content {
|
|
139
|
+
max-height: 400px;
|
|
140
|
+
overflow-y: auto;
|
|
141
|
+
border-top: 1px solid rgba(255,255,255,0.2);
|
|
142
|
+
transition: max-height 0.3s ease-out;
|
|
143
|
+
}
|
|
144
|
+
.newshound-banner.newshound-collapsed .newshound-content {
|
|
145
|
+
max-height: 0;
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
border-top: none;
|
|
148
|
+
}
|
|
149
|
+
.newshound-section {
|
|
150
|
+
padding: 15px 20px;
|
|
151
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
152
|
+
}
|
|
153
|
+
.newshound-section:last-child {
|
|
154
|
+
border-bottom: none;
|
|
155
|
+
}
|
|
156
|
+
.newshound-section-title {
|
|
157
|
+
font-weight: 600;
|
|
158
|
+
margin-bottom: 10px;
|
|
159
|
+
font-size: 15px;
|
|
160
|
+
}
|
|
161
|
+
.newshound-item {
|
|
162
|
+
background: rgba(255,255,255,0.1);
|
|
163
|
+
padding: 10px;
|
|
164
|
+
margin-bottom: 8px;
|
|
165
|
+
border-radius: 6px;
|
|
166
|
+
font-size: 13px;
|
|
167
|
+
}
|
|
168
|
+
.newshound-item:last-child {
|
|
169
|
+
margin-bottom: 0;
|
|
170
|
+
}
|
|
171
|
+
.newshound-item-title {
|
|
172
|
+
font-weight: 600;
|
|
173
|
+
margin-bottom: 4px;
|
|
174
|
+
}
|
|
175
|
+
.newshound-item-detail {
|
|
176
|
+
opacity: 0.9;
|
|
177
|
+
font-size: 12px;
|
|
178
|
+
}
|
|
179
|
+
.newshound-grid {
|
|
180
|
+
display: grid;
|
|
181
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
182
|
+
gap: 10px;
|
|
183
|
+
}
|
|
184
|
+
.newshound-stat {
|
|
185
|
+
background: rgba(255,255,255,0.1);
|
|
186
|
+
padding: 12px;
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
text-align: center;
|
|
189
|
+
}
|
|
190
|
+
.newshound-stat-value {
|
|
191
|
+
font-size: 24px;
|
|
192
|
+
font-weight: 700;
|
|
193
|
+
display: block;
|
|
194
|
+
}
|
|
195
|
+
.newshound-stat-label {
|
|
196
|
+
font-size: 11px;
|
|
197
|
+
opacity: 0.8;
|
|
198
|
+
text-transform: uppercase;
|
|
199
|
+
letter-spacing: 0.5px;
|
|
200
|
+
}
|
|
201
|
+
</style>
|
|
202
|
+
CSS
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def summary_badge(exception_data, job_data)
|
|
206
|
+
exception_count = exception_data[:exceptions]&.length || 0
|
|
207
|
+
failed_jobs = job_data.dig(:queue_stats, :failed) || 0
|
|
208
|
+
|
|
209
|
+
if exception_count > 0 || failed_jobs > 10
|
|
210
|
+
badge_class = "error"
|
|
211
|
+
text = "#{exception_count} exceptions, #{failed_jobs} failed jobs"
|
|
212
|
+
elsif failed_jobs > 5
|
|
213
|
+
badge_class = "warning"
|
|
214
|
+
text = "#{failed_jobs} failed jobs"
|
|
215
|
+
else
|
|
216
|
+
badge_class = "success"
|
|
217
|
+
text = "All clear"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
%(<span class="newshound-badge #{badge_class}">#{text}</span>)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def render_exceptions(data)
|
|
224
|
+
exceptions = data[:exceptions] || []
|
|
225
|
+
|
|
226
|
+
if exceptions.empty?
|
|
227
|
+
return %(<div class="newshound-section"><div class="newshound-section-title">✅ Exceptions</div><div class="newshound-item">No exceptions in the last 24 hours</div></div>)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
items = exceptions.take(5).map do |ex|
|
|
231
|
+
<<~HTML
|
|
232
|
+
<div class="newshound-item">
|
|
233
|
+
<div class="newshound-item-title">#{escape_html(ex[:title])}</div>
|
|
234
|
+
<div class="newshound-item-detail">
|
|
235
|
+
#{escape_html(ex[:message])} • #{escape_html(ex[:location])} • #{escape_html(ex[:time])}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
HTML
|
|
239
|
+
end.join
|
|
240
|
+
|
|
241
|
+
<<~HTML
|
|
242
|
+
<div class="newshound-section">
|
|
243
|
+
<div class="newshound-section-title">⚠️ Recent Exceptions (#{exceptions.length})</div>
|
|
244
|
+
#{items}
|
|
245
|
+
</div>
|
|
246
|
+
HTML
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def render_jobs(data)
|
|
250
|
+
stats = data[:queue_stats] || {}
|
|
251
|
+
|
|
252
|
+
<<~HTML
|
|
253
|
+
<div class="newshound-section">
|
|
254
|
+
<div class="newshound-section-title">📊 Job Queue Status</div>
|
|
255
|
+
<div class="newshound-grid">
|
|
256
|
+
<div class="newshound-stat">
|
|
257
|
+
<span class="newshound-stat-value">#{stats[:ready_to_run] || 0}</span>
|
|
258
|
+
<span class="newshound-stat-label">Ready</span>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="newshound-stat">
|
|
261
|
+
<span class="newshound-stat-value">#{stats[:scheduled] || 0}</span>
|
|
262
|
+
<span class="newshound-stat-label">Scheduled</span>
|
|
263
|
+
</div>
|
|
264
|
+
<div class="newshound-stat">
|
|
265
|
+
<span class="newshound-stat-value">#{stats[:failed] || 0}</span>
|
|
266
|
+
<span class="newshound-stat-label">Failed</span>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="newshound-stat">
|
|
269
|
+
<span class="newshound-stat-value">#{stats[:completed_today] || 0}</span>
|
|
270
|
+
<span class="newshound-stat-label">Completed Today</span>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
HTML
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def escape_html(text)
|
|
278
|
+
return String.new unless text.present?
|
|
279
|
+
text.to_s
|
|
280
|
+
.gsub('&', '&')
|
|
281
|
+
.gsub('<', '<')
|
|
282
|
+
.gsub('>', '>')
|
|
283
|
+
.gsub('"', '"')
|
|
284
|
+
.gsub("'", ''')
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -22,6 +22,21 @@ module Newshound
|
|
|
22
22
|
queue_health_section
|
|
23
23
|
].compact
|
|
24
24
|
end
|
|
25
|
+
alias_method :report, :generate_report
|
|
26
|
+
|
|
27
|
+
# Returns data formatted for the banner UI
|
|
28
|
+
def banner_data
|
|
29
|
+
stats = queue_statistics
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
queue_stats: {
|
|
33
|
+
ready_to_run: stats[:ready],
|
|
34
|
+
scheduled: stats[:scheduled],
|
|
35
|
+
failed: stats[:failed],
|
|
36
|
+
completed_today: stats[:finished_today]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
end
|
|
25
40
|
|
|
26
41
|
private
|
|
27
42
|
|
|
@@ -42,21 +57,33 @@ module Newshound
|
|
|
42
57
|
def job_counts_by_type
|
|
43
58
|
return {} unless job_source
|
|
44
59
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
# Use raw SQL since Que::Job may not support ActiveRecord's .group method
|
|
61
|
+
results = ActiveRecord::Base.connection.execute(<<~SQL)
|
|
62
|
+
SELECT job_class, error_count, COUNT(*) as count
|
|
63
|
+
FROM que_jobs
|
|
64
|
+
WHERE finished_at IS NULL
|
|
65
|
+
GROUP BY job_class, error_count
|
|
66
|
+
ORDER BY job_class
|
|
67
|
+
SQL
|
|
68
|
+
|
|
69
|
+
results.each_with_object({}) do |row, hash|
|
|
70
|
+
job_class = row["job_class"]
|
|
71
|
+
error_count = row["error_count"].to_i
|
|
72
|
+
count = row["count"].to_i
|
|
73
|
+
|
|
74
|
+
hash[job_class] ||= {success: 0, failed: 0, total: 0}
|
|
75
|
+
|
|
76
|
+
if error_count.zero?
|
|
77
|
+
hash[job_class][:success] += count
|
|
78
|
+
else
|
|
79
|
+
hash[job_class][:failed] += count
|
|
59
80
|
end
|
|
81
|
+
|
|
82
|
+
hash[job_class][:total] += count
|
|
83
|
+
end
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
logger.error "Failed to fetch job counts: #{e.message}"
|
|
86
|
+
{}
|
|
60
87
|
end
|
|
61
88
|
|
|
62
89
|
def format_job_counts(counts)
|
|
@@ -85,20 +112,27 @@ module Newshound
|
|
|
85
112
|
def queue_statistics
|
|
86
113
|
return default_stats unless job_source
|
|
87
114
|
|
|
88
|
-
|
|
89
|
-
|
|
115
|
+
conn = ActiveRecord::Base.connection
|
|
116
|
+
current_time = conn.quote(Time.now)
|
|
117
|
+
beginning_of_day = conn.quote(Date.today.to_time)
|
|
90
118
|
|
|
91
119
|
{
|
|
92
|
-
ready:
|
|
93
|
-
scheduled:
|
|
94
|
-
failed:
|
|
95
|
-
finished_today:
|
|
120
|
+
ready: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at <= #{current_time}"),
|
|
121
|
+
scheduled: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at > #{current_time}"),
|
|
122
|
+
failed: count_jobs("error_count > 0 AND finished_at IS NULL"),
|
|
123
|
+
finished_today: count_jobs("finished_at >= #{beginning_of_day}")
|
|
96
124
|
}
|
|
97
125
|
rescue StandardError => e
|
|
98
126
|
logger.error "Failed to fetch Que statistics: #{e.message}"
|
|
99
127
|
default_stats
|
|
100
128
|
end
|
|
101
129
|
|
|
130
|
+
def count_jobs(where_clause)
|
|
131
|
+
ActiveRecord::Base.connection.select_value(
|
|
132
|
+
"SELECT COUNT(*) FROM que_jobs WHERE #{where_clause}"
|
|
133
|
+
).to_i
|
|
134
|
+
end
|
|
135
|
+
|
|
102
136
|
def default_stats
|
|
103
137
|
{ ready: 0, scheduled: 0, failed: 0, finished_today: 0 }
|
|
104
138
|
end
|
data/lib/newshound/railtie.rb
CHANGED
|
@@ -4,59 +4,55 @@ require "rails/railtie"
|
|
|
4
4
|
|
|
5
5
|
module Newshound
|
|
6
6
|
class Railtie < Rails::Railtie
|
|
7
|
+
# Register middleware to inject banner
|
|
8
|
+
initializer "newshound.middleware" do |app|
|
|
9
|
+
if Newshound.configuration.enabled
|
|
10
|
+
app.middleware.use Newshound::Middleware::BannerInjector
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
7
14
|
rake_tasks do
|
|
8
15
|
namespace :newshound do
|
|
9
|
-
desc "Send daily report immediately"
|
|
10
|
-
task report_now: :environment do
|
|
11
|
-
puts "Sending Newshound daily report..."
|
|
12
|
-
Newshound.report!
|
|
13
|
-
puts "Report sent successfully!"
|
|
14
|
-
rescue StandardError => e
|
|
15
|
-
puts "Failed to send report: #{e.message}"
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
desc "Schedule daily report job"
|
|
19
|
-
task schedule: :environment do
|
|
20
|
-
puts "Scheduling Newshound daily report..."
|
|
21
|
-
Newshound::Scheduler.run_now!
|
|
22
|
-
puts "Job enqueued successfully!"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
16
|
desc "Show current configuration"
|
|
26
17
|
task config: :environment do
|
|
27
18
|
config = Newshound.configuration
|
|
28
19
|
puts "Newshound Configuration:"
|
|
29
20
|
puts " Enabled: #{config.enabled}"
|
|
30
|
-
puts " Slack Webhook: #{config.slack_webhook_url.present? ? '[CONFIGURED]' : '[NOT SET]'}"
|
|
31
|
-
puts " Slack Channel: #{config.slack_channel}"
|
|
32
|
-
puts " Report Time: #{config.report_time}"
|
|
33
21
|
puts " Exception Limit: #{config.exception_limit}"
|
|
34
|
-
puts "
|
|
35
|
-
puts "
|
|
22
|
+
puts " Authorized Roles: #{config.authorized_roles.join(', ')}"
|
|
23
|
+
puts " Current User Method: #{config.current_user_method}"
|
|
24
|
+
puts " Custom Authorization: #{config.authorization_block.present? ? 'Yes' : 'No'}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc "Test exception reporter"
|
|
28
|
+
task test_exceptions: :environment do
|
|
29
|
+
reporter = Newshound::ExceptionReporter.new
|
|
30
|
+
data = reporter.report
|
|
31
|
+
puts "Exception Report:"
|
|
32
|
+
puts " Total exceptions: #{data[:exceptions]&.length || 0}"
|
|
33
|
+
data[:exceptions]&.each_with_index do |ex, i|
|
|
34
|
+
puts " #{i + 1}. #{ex[:title]} - #{ex[:message]}"
|
|
35
|
+
end
|
|
36
36
|
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
else
|
|
50
|
-
Rails.logger.warn "Newshound: que-scheduler not found. Daily reports will need to be triggered manually."
|
|
38
|
+
desc "Test job reporter"
|
|
39
|
+
task test_jobs: :environment do
|
|
40
|
+
reporter = Newshound::QueReporter.new
|
|
41
|
+
data = reporter.report
|
|
42
|
+
puts "Job Queue Report:"
|
|
43
|
+
puts " Ready to run: #{data.dig(:queue_stats, :ready_to_run)}"
|
|
44
|
+
puts " Scheduled: #{data.dig(:queue_stats, :scheduled)}"
|
|
45
|
+
puts " Failed: #{data.dig(:queue_stats, :failed)}"
|
|
46
|
+
puts " Completed today: #{data.dig(:queue_stats, :completed_today)}"
|
|
51
47
|
end
|
|
52
48
|
end
|
|
53
49
|
end
|
|
54
50
|
|
|
55
51
|
config.after_initialize do
|
|
56
52
|
if Newshound.configuration.valid?
|
|
57
|
-
Rails.logger.info "Newshound initialized
|
|
53
|
+
Rails.logger.info "Newshound initialized - banner will be shown to authorized users"
|
|
58
54
|
else
|
|
59
|
-
Rails.logger.warn "Newshound is
|
|
55
|
+
Rails.logger.warn "Newshound is disabled"
|
|
60
56
|
end
|
|
61
57
|
end
|
|
62
58
|
end
|
data/lib/newshound/version.rb
CHANGED
data/lib/newshound.rb
CHANGED
|
@@ -4,9 +4,8 @@ require_relative "newshound/version"
|
|
|
4
4
|
require_relative "newshound/configuration"
|
|
5
5
|
require_relative "newshound/exception_reporter"
|
|
6
6
|
require_relative "newshound/que_reporter"
|
|
7
|
-
require_relative "newshound/
|
|
8
|
-
require_relative "newshound/
|
|
9
|
-
require_relative "newshound/scheduler"
|
|
7
|
+
require_relative "newshound/authorization"
|
|
8
|
+
require_relative "newshound/middleware/banner_injector"
|
|
10
9
|
require_relative "newshound/railtie" if defined?(Rails)
|
|
11
10
|
|
|
12
11
|
module Newshound
|
|
@@ -21,46 +20,9 @@ module Newshound
|
|
|
21
20
|
yield(configuration)
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
exception_report = ExceptionReporter.new.generate_report
|
|
28
|
-
que_report = QueReporter.new.generate_report
|
|
29
|
-
|
|
30
|
-
message = format_daily_report(exception_report, que_report)
|
|
31
|
-
slack_notifier.post(message)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def format_daily_report(exception_report, que_report)
|
|
37
|
-
{
|
|
38
|
-
blocks: [
|
|
39
|
-
{
|
|
40
|
-
type: "header",
|
|
41
|
-
text: {
|
|
42
|
-
type: "plain_text",
|
|
43
|
-
text: "🐕 Daily Newshound Report",
|
|
44
|
-
emoji: true
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: "section",
|
|
49
|
-
text: {
|
|
50
|
-
type: "mrkdwn",
|
|
51
|
-
text: "*Date:* #{Date.current.strftime('%B %d, %Y')}"
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
type: "divider"
|
|
56
|
-
},
|
|
57
|
-
*exception_report,
|
|
58
|
-
{
|
|
59
|
-
type: "divider"
|
|
60
|
-
},
|
|
61
|
-
*que_report
|
|
62
|
-
]
|
|
63
|
-
}
|
|
23
|
+
# Allow setting custom authorization logic
|
|
24
|
+
def authorize_with(&block)
|
|
25
|
+
configuration.authorize_with(&block)
|
|
64
26
|
end
|
|
65
27
|
end
|
|
66
28
|
end
|