newshound 0.2.7 → 1.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/CHANGELOG.md +21 -5
- data/lib/generators/newshound/install/install_generator.rb +1 -1
- data/lib/generators/newshound/install/templates/newshound.rb +5 -0
- data/lib/newshound/configuration.rb +2 -1
- data/lib/newshound/exception_reporter.rb +15 -1
- data/lib/newshound/job_reporter.rb +120 -0
- data/lib/newshound/jobs/base.rb +50 -0
- data/lib/newshound/jobs/que.rb +66 -0
- data/lib/newshound/jobs.rb +43 -0
- data/lib/newshound/middleware/banner_injector.rb +58 -17
- data/lib/newshound/railtie.rb +10 -10
- data/lib/newshound/version.rb +1 -1
- data/lib/newshound.rb +2 -1
- data/newshound.gemspec +8 -3
- metadata +11 -18
- data/lib/newshound/que_reporter.rb +0 -166
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c59db21db9a10838f80bf529e985a4cb1af723bcf86ba4893db0d597b1a10059
|
|
4
|
+
data.tar.gz: 66a6aa516fc12f6c7003ed286c0ffaef9fc7112c2d659b96a927b09de53c0a54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 210e8a10e784ab3a72a34291465faf6c2f213a332eca30d392561609a0316a7c28e30ae532a814bf4eca2f42fb630ed43ce8dcc4c2db458d05267fc14419c5ac
|
|
7
|
+
data.tar.gz: 6de9641e2cfb691ccb94c4185c54dabaa78d9d92ac57e173eed5087ebb5c0dd5e468c81f79d2ca83b20376bc2d29e9137e766354028493c568c0d24eca615b07
|
data/CHANGELOG.md
CHANGED
|
@@ -5,14 +5,30 @@ 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
|
-
## [0.2.
|
|
8
|
+
## [0.2.8] - 2026-02-10
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
- Ruby support to 3.3+ and added Ruby 4.0 to CI matrix (24e5b86)
|
|
13
|
+
- Job monitoring uses configurable adapter pattern via config.job_source (8d33d30)
|
|
11
14
|
|
|
12
|
-
###
|
|
15
|
+
### Removed
|
|
13
16
|
|
|
14
|
-
-
|
|
17
|
+
- Ruby 3.1 and 3.2 support (24e5b86)
|
|
18
|
+
- Hard dependency on que gem (8d33d30)
|
|
19
|
+
- QueReporter class (replaced by JobReporter + Jobs adapters) Version: major (8d33d30)
|
|
15
20
|
|
|
16
21
|
### Fixed
|
|
17
22
|
|
|
18
|
-
-
|
|
23
|
+
- Set the correct location for the repository on the web. (cb7bb88)
|
|
24
|
+
- test_exceptions rake task calling report instead of banner_data (e440188)
|
|
25
|
+
- Banner overlaying content in apps with !important body padding-top rules (16f22f1)
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Jobs adapter pattern with Base class and registry (c50ba9c)
|
|
30
|
+
- Jobs::Que adapter for Que job backend (c50ba9c)
|
|
31
|
+
- JobReporter that delegates to a configurable job source adapter (c50ba9c)
|
|
32
|
+
- ExceptionReporter#formatted_exception_count and #exception_summary helpers (e440188)
|
|
33
|
+
|
|
34
|
+
## [0.2.7] - 2026-01-30
|
|
@@ -31,7 +31,7 @@ module Newshound
|
|
|
31
31
|
say " rake newshound:test_jobs"
|
|
32
32
|
say ""
|
|
33
33
|
say " For more information, visit:", :blue
|
|
34
|
-
say " https://github.com/
|
|
34
|
+
say " https://github.com/SOFware/newshound"
|
|
35
35
|
say "===============================================================================", :green
|
|
36
36
|
say ""
|
|
37
37
|
end
|
|
@@ -14,6 +14,11 @@ Newshound.configure do |config|
|
|
|
14
14
|
# Default is :exception_track
|
|
15
15
|
config.exception_source = :exception_track # or :solid_errors
|
|
16
16
|
|
|
17
|
+
# Job source adapter for monitoring background jobs
|
|
18
|
+
# Uncomment and set to enable job monitoring in the banner
|
|
19
|
+
# config.job_source = :que # or a custom adapter instance
|
|
20
|
+
# See Newshound::Jobs::Base for the adapter interface
|
|
21
|
+
|
|
17
22
|
# User roles that are authorized to view the Newshound banner
|
|
18
23
|
# These should match the role values in your User model
|
|
19
24
|
# Default is [:developer, :super_user]
|
|
@@ -4,7 +4,7 @@ module Newshound
|
|
|
4
4
|
class Configuration
|
|
5
5
|
attr_accessor :exception_limit, :enabled, :authorized_roles,
|
|
6
6
|
:current_user_method, :authorization_block, :exception_source,
|
|
7
|
-
:warning_source, :warning_limit
|
|
7
|
+
:warning_source, :warning_limit, :job_source
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
10
10
|
@exception_limit = 10
|
|
@@ -15,6 +15,7 @@ module Newshound
|
|
|
15
15
|
@exception_source = :exception_track
|
|
16
16
|
@warning_source = nil
|
|
17
17
|
@warning_limit = 10
|
|
18
|
+
@job_source = nil
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
# Allow custom authorization logic
|
|
@@ -29,10 +29,24 @@ module Newshound
|
|
|
29
29
|
# Returns data formatted for the banner UI
|
|
30
30
|
def banner_data
|
|
31
31
|
{
|
|
32
|
-
exceptions: recent_exceptions.map { |exception|
|
|
32
|
+
exceptions: recent_exceptions.map { |exception|
|
|
33
|
+
exception_source.format_for_banner(exception)
|
|
34
|
+
}
|
|
33
35
|
}
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
def formatted_exception_count(
|
|
39
|
+
format = "Total exceptions: %s"
|
|
40
|
+
)
|
|
41
|
+
format % recent_exceptions.length
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def exception_summary
|
|
45
|
+
recent_exceptions.map.with_index(1) do |ex, i|
|
|
46
|
+
exception_source.format_for_report(ex, i)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
36
50
|
private
|
|
37
51
|
|
|
38
52
|
def resolve_exception_source(source)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Newshound
|
|
4
|
+
class JobReporter
|
|
5
|
+
attr_reader :job_source, :configuration, :logger
|
|
6
|
+
|
|
7
|
+
def initialize(job_source: nil, configuration: nil, logger: nil)
|
|
8
|
+
@configuration = configuration || Newshound.configuration
|
|
9
|
+
@job_source = resolve_job_source(job_source)
|
|
10
|
+
@logger = logger || (defined?(Rails) ? Rails.logger : Logger.new($stdout))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate_report
|
|
14
|
+
return no_jobs_block unless job_source
|
|
15
|
+
|
|
16
|
+
[
|
|
17
|
+
{
|
|
18
|
+
type: "section",
|
|
19
|
+
text: {
|
|
20
|
+
type: "mrkdwn",
|
|
21
|
+
text: "*📊 Job Queue Status*"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
job_counts_section,
|
|
25
|
+
queue_health_section
|
|
26
|
+
].compact
|
|
27
|
+
end
|
|
28
|
+
alias_method :report, :generate_report
|
|
29
|
+
|
|
30
|
+
def banner_data
|
|
31
|
+
return {queue_stats: {}} unless job_source
|
|
32
|
+
|
|
33
|
+
job_source.format_for_banner
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def resolve_job_source(source)
|
|
39
|
+
source ||= @configuration.job_source
|
|
40
|
+
return nil if source.nil?
|
|
41
|
+
|
|
42
|
+
source.is_a?(Symbol) ? Jobs.source(source) : source
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def job_counts_section
|
|
46
|
+
counts = job_source.job_counts_by_type
|
|
47
|
+
|
|
48
|
+
return no_jobs_section if counts.empty?
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
type: "section",
|
|
52
|
+
text: {
|
|
53
|
+
type: "mrkdwn",
|
|
54
|
+
text: format_job_counts(counts)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def format_job_counts(counts)
|
|
60
|
+
lines = ["*Job Counts by Type:*"]
|
|
61
|
+
|
|
62
|
+
counts.each do |job_class, stats|
|
|
63
|
+
status_emoji = (stats[:failed] > 0) ? "⚠️" : "✅"
|
|
64
|
+
lines << "• #{status_emoji} *#{job_class}*: #{stats[:total]} total (#{stats[:success]} success, #{stats[:failed]} failed)"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
lines.join("\n")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def queue_health_section
|
|
71
|
+
stats = job_source.queue_statistics
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
type: "section",
|
|
75
|
+
text: {
|
|
76
|
+
type: "mrkdwn",
|
|
77
|
+
text: format_queue_health(stats)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def format_queue_health(stats)
|
|
83
|
+
health_emoji = if stats[:failed] > 10
|
|
84
|
+
"🔴"
|
|
85
|
+
else
|
|
86
|
+
(stats[:failed] > 5) ? "🟡" : "🟢"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
<<~TEXT
|
|
90
|
+
*Queue Health #{health_emoji}*
|
|
91
|
+
• *Ready to Run:* #{stats[:ready]}
|
|
92
|
+
• *Scheduled:* #{stats[:scheduled]}
|
|
93
|
+
• *Failed (Retry Queue):* #{stats[:failed]}
|
|
94
|
+
• *Completed Today:* #{stats[:finished_today]}
|
|
95
|
+
TEXT
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def no_jobs_section
|
|
99
|
+
{
|
|
100
|
+
type: "section",
|
|
101
|
+
text: {
|
|
102
|
+
type: "mrkdwn",
|
|
103
|
+
text: "*No jobs found in the queue*"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def no_jobs_block
|
|
109
|
+
[
|
|
110
|
+
{
|
|
111
|
+
type: "section",
|
|
112
|
+
text: {
|
|
113
|
+
type: "mrkdwn",
|
|
114
|
+
text: "*✅ No Job Source Configured*"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Newshound
|
|
4
|
+
module Jobs
|
|
5
|
+
# Base class for job source adapters
|
|
6
|
+
# Each adapter is responsible for:
|
|
7
|
+
# 1. Fetching queue statistics from its specific job backend
|
|
8
|
+
# 2. Fetching job counts grouped by type
|
|
9
|
+
# 3. Formatting job data for banner display
|
|
10
|
+
#
|
|
11
|
+
# Subclasses must implement:
|
|
12
|
+
# - #queue_statistics - Returns a hash of queue stats
|
|
13
|
+
# - #job_counts_by_type - Returns a hash of job class => counts
|
|
14
|
+
#
|
|
15
|
+
# Unlike Warnings::Base and Exceptions::Base (which format individual records),
|
|
16
|
+
# #format_for_banner takes no arguments and returns aggregate queue statistics.
|
|
17
|
+
# A default implementation is provided that delegates to #queue_statistics.
|
|
18
|
+
class Base
|
|
19
|
+
# Returns queue-level statistics
|
|
20
|
+
#
|
|
21
|
+
# @return [Hash] with keys :ready, :scheduled, :failed, :finished_today
|
|
22
|
+
def queue_statistics
|
|
23
|
+
raise NotImplementedError, "#{self.class} must implement #queue_statistics"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns job counts grouped by job class
|
|
27
|
+
#
|
|
28
|
+
# @return [Hash] job_class => { success:, failed:, total: }
|
|
29
|
+
def job_counts_by_type
|
|
30
|
+
raise NotImplementedError, "#{self.class} must implement #job_counts_by_type"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns data formatted for the banner UI
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash] with key :queue_stats containing ready_to_run, scheduled, failed, completed_today
|
|
36
|
+
def format_for_banner
|
|
37
|
+
stats = queue_statistics
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
queue_stats: {
|
|
41
|
+
ready_to_run: stats[:ready],
|
|
42
|
+
scheduled: stats[:scheduled],
|
|
43
|
+
failed: stats[:failed],
|
|
44
|
+
completed_today: stats[:finished_today]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Newshound
|
|
4
|
+
module Jobs
|
|
5
|
+
class Que < Base
|
|
6
|
+
attr_reader :logger
|
|
7
|
+
|
|
8
|
+
def initialize(logger: nil)
|
|
9
|
+
@logger = logger || (defined?(Rails) ? Rails.logger : Logger.new($stdout))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def queue_statistics
|
|
13
|
+
conn = ActiveRecord::Base.connection
|
|
14
|
+
current_time = conn.quote(Time.now)
|
|
15
|
+
beginning_of_day = conn.quote(Date.today.to_time)
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
ready: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at <= #{current_time}"),
|
|
19
|
+
scheduled: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at > #{current_time}"),
|
|
20
|
+
failed: count_jobs("error_count > 0 AND finished_at IS NULL"),
|
|
21
|
+
finished_today: count_jobs("finished_at >= #{beginning_of_day}")
|
|
22
|
+
}
|
|
23
|
+
rescue => e
|
|
24
|
+
logger.error "Failed to fetch Que statistics: #{e.message}"
|
|
25
|
+
{ready: 0, scheduled: 0, failed: 0, finished_today: 0}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def job_counts_by_type
|
|
29
|
+
results = ActiveRecord::Base.connection.execute(<<~SQL)
|
|
30
|
+
SELECT job_class, error_count, COUNT(*) as count
|
|
31
|
+
FROM que_jobs
|
|
32
|
+
WHERE finished_at IS NULL
|
|
33
|
+
GROUP BY job_class, error_count
|
|
34
|
+
ORDER BY job_class
|
|
35
|
+
SQL
|
|
36
|
+
|
|
37
|
+
results.each_with_object({}) do |row, hash|
|
|
38
|
+
job_class = row["job_class"]
|
|
39
|
+
error_count = row["error_count"].to_i
|
|
40
|
+
count = row["count"].to_i
|
|
41
|
+
|
|
42
|
+
hash[job_class] ||= {success: 0, failed: 0, total: 0}
|
|
43
|
+
|
|
44
|
+
if error_count.zero?
|
|
45
|
+
hash[job_class][:success] += count
|
|
46
|
+
else
|
|
47
|
+
hash[job_class][:failed] += count
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
hash[job_class][:total] += count
|
|
51
|
+
end
|
|
52
|
+
rescue => e
|
|
53
|
+
logger.error "Failed to fetch job counts: #{e.message}"
|
|
54
|
+
{}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def count_jobs(where_clause)
|
|
60
|
+
ActiveRecord::Base.connection.select_value(
|
|
61
|
+
"SELECT COUNT(*) FROM que_jobs WHERE #{where_clause}"
|
|
62
|
+
).to_i
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Dir[File.join(__dir__, "jobs", "*.rb")].each { |file| require file }
|
|
4
|
+
|
|
5
|
+
module Newshound
|
|
6
|
+
module Jobs
|
|
7
|
+
@registry = {}
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :registry
|
|
11
|
+
|
|
12
|
+
# Register a job adapter class with a symbolic name
|
|
13
|
+
#
|
|
14
|
+
# @param name [Symbol, String] The name to register the adapter under
|
|
15
|
+
# @param adapter_class [Class] The adapter class (must inherit from Jobs::Base)
|
|
16
|
+
def register(name, adapter_class)
|
|
17
|
+
@registry[name.to_sym] = adapter_class
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get a job source adapter instance
|
|
21
|
+
#
|
|
22
|
+
# @param source [Symbol, Object] Either a symbolic name to look up, or an adapter instance
|
|
23
|
+
# @return [Jobs::Base] An instance of the job adapter
|
|
24
|
+
# @raise [RuntimeError] If the source symbol cannot be resolved
|
|
25
|
+
def source(source)
|
|
26
|
+
return source unless source.is_a?(Symbol)
|
|
27
|
+
|
|
28
|
+
if @registry.key?(source)
|
|
29
|
+
return @registry[source].new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
constant = constants.find { |c| c.to_s.gsub(/(?<!^)([A-Z])/, "_\\1").downcase == source.to_s }
|
|
33
|
+
raise "Invalid job source: #{source}" unless constant
|
|
34
|
+
|
|
35
|
+
const_get(constant).new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def clear_registry!
|
|
39
|
+
@registry = {}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -55,11 +55,11 @@ module Newshound
|
|
|
55
55
|
|
|
56
56
|
def generate_banner_html
|
|
57
57
|
exception_reporter = Newshound::ExceptionReporter.new
|
|
58
|
-
|
|
58
|
+
job_reporter = Newshound::JobReporter.new
|
|
59
59
|
warning_reporter = Newshound::WarningReporter.new
|
|
60
60
|
|
|
61
61
|
exception_data = exception_reporter.banner_data
|
|
62
|
-
job_data =
|
|
62
|
+
job_data = job_reporter.banner_data
|
|
63
63
|
warning_data = warning_reporter.banner_data
|
|
64
64
|
|
|
65
65
|
# Generate HTML from template
|
|
@@ -91,25 +91,66 @@ module Newshound
|
|
|
91
91
|
<<~JS
|
|
92
92
|
<script>
|
|
93
93
|
(function() {
|
|
94
|
+
var cachedPriority, cachedBodyRule;
|
|
95
|
+
|
|
96
|
+
// Detect once whether any non-newshound stylesheet uses !important on body padding-top
|
|
97
|
+
function detectPriority() {
|
|
98
|
+
var styles = document.querySelectorAll('style:not(#newshound-styles)');
|
|
99
|
+
|
|
100
|
+
for (var i = 0; i < styles.length; i++) {
|
|
101
|
+
if (styles[i].textContent.match(/body\\s*{[^}]*padding-top[^;]*!important/s)) {
|
|
102
|
+
return 'important';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
for (var s = 0; s < document.styleSheets.length; s++) {
|
|
108
|
+
var sheet = document.styleSheets[s];
|
|
109
|
+
if (sheet.ownerNode && sheet.ownerNode.id === 'newshound-styles') continue;
|
|
110
|
+
var rules = sheet.cssRules || [];
|
|
111
|
+
for (var r = 0; r < rules.length; r++) {
|
|
112
|
+
if (rules[r].selectorText === 'body' &&
|
|
113
|
+
rules[r].style.getPropertyPriority('padding-top') === 'important') {
|
|
114
|
+
return 'important';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {}
|
|
119
|
+
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Find the html body rule in the newshound stylesheet once
|
|
124
|
+
function findBodyRule() {
|
|
125
|
+
var styleEl = document.getElementById('newshound-styles');
|
|
126
|
+
if (!styleEl || !styleEl.sheet) return null;
|
|
127
|
+
var rules = styleEl.sheet.cssRules;
|
|
128
|
+
for (var i = 0; i < rules.length; i++) {
|
|
129
|
+
if (rules[i].selectorText === 'html body') return rules[i];
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
94
134
|
window.newshoundUpdatePadding = function() {
|
|
95
|
-
// Wait for transition to complete
|
|
96
135
|
setTimeout(function() {
|
|
97
136
|
var banner = document.getElementById('newshound-banner');
|
|
98
|
-
if (banner)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
if (!banner) return;
|
|
138
|
+
|
|
139
|
+
if (cachedBodyRule === undefined) cachedBodyRule = findBodyRule();
|
|
140
|
+
if (!cachedBodyRule) return;
|
|
141
|
+
|
|
142
|
+
if (cachedPriority === undefined) cachedPriority = detectPriority();
|
|
143
|
+
|
|
144
|
+
cachedBodyRule.style.setProperty('padding-top', banner.offsetHeight + 'px', cachedPriority);
|
|
102
145
|
}, 300);
|
|
103
146
|
};
|
|
104
147
|
|
|
105
|
-
// Set initial padding after page load
|
|
106
148
|
if (document.readyState === 'loading') {
|
|
107
149
|
document.addEventListener('DOMContentLoaded', window.newshoundUpdatePadding);
|
|
108
150
|
} else {
|
|
109
151
|
window.newshoundUpdatePadding();
|
|
110
152
|
}
|
|
111
153
|
|
|
112
|
-
// Also update on window resize
|
|
113
154
|
window.addEventListener('resize', window.newshoundUpdatePadding);
|
|
114
155
|
})();
|
|
115
156
|
</script>
|
|
@@ -118,8 +159,8 @@ module Newshound
|
|
|
118
159
|
|
|
119
160
|
def render_styles
|
|
120
161
|
<<~CSS
|
|
121
|
-
<style>
|
|
122
|
-
body {
|
|
162
|
+
<style id="newshound-styles">
|
|
163
|
+
html body {
|
|
123
164
|
padding-top: 50px;
|
|
124
165
|
transition: padding-top 0.3s ease-out;
|
|
125
166
|
}
|
|
@@ -157,13 +198,13 @@ module Newshound
|
|
|
157
198
|
font-weight: 600;
|
|
158
199
|
background: rgba(255,255,255,0.2);
|
|
159
200
|
}
|
|
160
|
-
.newshound-
|
|
201
|
+
.newshound-error {
|
|
161
202
|
background: #ef4444;
|
|
162
203
|
}
|
|
163
|
-
.newshound-
|
|
204
|
+
.newshound-warning {
|
|
164
205
|
background: #f59e0b;
|
|
165
206
|
}
|
|
166
|
-
.newshound-
|
|
207
|
+
.newshound-success {
|
|
167
208
|
background: #10b981;
|
|
168
209
|
}
|
|
169
210
|
.newshound-toggle {
|
|
@@ -245,16 +286,16 @@ module Newshound
|
|
|
245
286
|
warning_count = warning_data[:warnings]&.length || 0
|
|
246
287
|
|
|
247
288
|
if exception_count > 0 || failed_jobs > 10
|
|
248
|
-
badge_class = "error"
|
|
289
|
+
badge_class = "newshound-error"
|
|
249
290
|
text = "#{exception_count} exceptions, #{failed_jobs} failed jobs"
|
|
250
291
|
elsif warning_count > 0 || failed_jobs > 5
|
|
251
|
-
badge_class = "warning"
|
|
292
|
+
badge_class = "newshound-warning"
|
|
252
293
|
parts = []
|
|
253
294
|
parts << "#{warning_count} warnings" if warning_count > 0
|
|
254
295
|
parts << "#{failed_jobs} failed jobs" if failed_jobs > 5
|
|
255
296
|
text = parts.join(", ")
|
|
256
297
|
else
|
|
257
|
-
badge_class = "success"
|
|
298
|
+
badge_class = "newshound-success"
|
|
258
299
|
text = "All clear"
|
|
259
300
|
end
|
|
260
301
|
|
data/lib/newshound/railtie.rb
CHANGED
|
@@ -27,23 +27,23 @@ module Newshound
|
|
|
27
27
|
desc "Test exception reporter"
|
|
28
28
|
task test_exceptions: :environment do
|
|
29
29
|
reporter = Newshound::ExceptionReporter.new
|
|
30
|
-
data = reporter.report
|
|
31
30
|
puts "Exception Report:"
|
|
32
|
-
puts "
|
|
33
|
-
|
|
34
|
-
puts " #{
|
|
31
|
+
puts " #{reporter.formatted_exception_count}"
|
|
32
|
+
reporter.exception_summary.each do |line|
|
|
33
|
+
puts " #{line}"
|
|
35
34
|
end
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
desc "Test job reporter"
|
|
39
38
|
task test_jobs: :environment do
|
|
40
|
-
reporter = Newshound::
|
|
41
|
-
data = reporter.
|
|
39
|
+
reporter = Newshound::JobReporter.new
|
|
40
|
+
data = reporter.banner_data
|
|
41
|
+
stats = data[:queue_stats]
|
|
42
42
|
puts "Job Queue Report:"
|
|
43
|
-
puts " Ready to run: #{
|
|
44
|
-
puts " Scheduled: #{
|
|
45
|
-
puts " Failed: #{
|
|
46
|
-
puts " Completed today: #{
|
|
43
|
+
puts " Ready to run: #{stats[:ready_to_run] || 0}"
|
|
44
|
+
puts " Scheduled: #{stats[:scheduled] || 0}"
|
|
45
|
+
puts " Failed: #{stats[:failed] || 0}"
|
|
46
|
+
puts " Completed today: #{stats[:completed_today] || 0}"
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
end
|
data/lib/newshound/version.rb
CHANGED
data/lib/newshound.rb
CHANGED
|
@@ -6,7 +6,8 @@ require_relative "newshound/exceptions"
|
|
|
6
6
|
require_relative "newshound/exception_reporter"
|
|
7
7
|
require_relative "newshound/warnings"
|
|
8
8
|
require_relative "newshound/warning_reporter"
|
|
9
|
-
require_relative "newshound/
|
|
9
|
+
require_relative "newshound/jobs"
|
|
10
|
+
require_relative "newshound/job_reporter"
|
|
10
11
|
require_relative "newshound/authorization"
|
|
11
12
|
require_relative "newshound/middleware/banner_injector"
|
|
12
13
|
require_relative "newshound/railtie" if defined?(Rails)
|
data/newshound.gemspec
CHANGED
|
@@ -8,11 +8,11 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Savannah Moore"]
|
|
9
9
|
spec.email = ["savannah.albanez@sofwarellc.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "Real-time web UI banner for monitoring
|
|
11
|
+
spec.summary = "Real-time web UI banner for monitoring background jobs and exception tracking"
|
|
12
12
|
spec.description = "Newshound displays exceptions and job statuses in a collapsible banner for authorized users in your Rails app"
|
|
13
13
|
spec.homepage = "https://github.com/SOFware/newshound"
|
|
14
14
|
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = ">= 3.
|
|
15
|
+
spec.required_ruby_version = ">= 3.3.0"
|
|
16
16
|
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
18
|
spec.metadata["source_code_uri"] = spec.homepage
|
|
@@ -27,7 +27,12 @@ Gem::Specification.new do |spec|
|
|
|
27
27
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
28
28
|
spec.require_paths = ["lib"]
|
|
29
29
|
|
|
30
|
+
spec.post_install_message = <<~MSG
|
|
31
|
+
To complete installation, run:
|
|
32
|
+
|
|
33
|
+
rails generate newshound:install
|
|
34
|
+
MSG
|
|
35
|
+
|
|
30
36
|
# Runtime dependencies
|
|
31
37
|
spec.add_dependency "rails", ">= 6.0"
|
|
32
|
-
spec.add_dependency "que", ">= 1.0"
|
|
33
38
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: newshound
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Savannah Moore
|
|
@@ -23,20 +23,6 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '6.0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: que
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1.0'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1.0'
|
|
40
26
|
description: Newshound displays exceptions and job statuses in a collapsible banner
|
|
41
27
|
for authorized users in your Rails app
|
|
42
28
|
email:
|
|
@@ -65,8 +51,11 @@ files:
|
|
|
65
51
|
- lib/newshound/exceptions/base.rb
|
|
66
52
|
- lib/newshound/exceptions/exception_track.rb
|
|
67
53
|
- lib/newshound/exceptions/solid_errors.rb
|
|
54
|
+
- lib/newshound/job_reporter.rb
|
|
55
|
+
- lib/newshound/jobs.rb
|
|
56
|
+
- lib/newshound/jobs/base.rb
|
|
57
|
+
- lib/newshound/jobs/que.rb
|
|
68
58
|
- lib/newshound/middleware/banner_injector.rb
|
|
69
|
-
- lib/newshound/que_reporter.rb
|
|
70
59
|
- lib/newshound/railtie.rb
|
|
71
60
|
- lib/newshound/version.rb
|
|
72
61
|
- lib/newshound/warning_reporter.rb
|
|
@@ -80,6 +69,10 @@ metadata:
|
|
|
80
69
|
homepage_uri: https://github.com/SOFware/newshound
|
|
81
70
|
source_code_uri: https://github.com/SOFware/newshound
|
|
82
71
|
changelog_uri: https://github.com/SOFware/newshound/blob/main/CHANGELOG.md
|
|
72
|
+
post_install_message: |
|
|
73
|
+
To complete installation, run:
|
|
74
|
+
|
|
75
|
+
rails generate newshound:install
|
|
83
76
|
rdoc_options: []
|
|
84
77
|
require_paths:
|
|
85
78
|
- lib
|
|
@@ -87,7 +80,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
87
80
|
requirements:
|
|
88
81
|
- - ">="
|
|
89
82
|
- !ruby/object:Gem::Version
|
|
90
|
-
version: 3.
|
|
83
|
+
version: 3.3.0
|
|
91
84
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
85
|
requirements:
|
|
93
86
|
- - ">="
|
|
@@ -96,5 +89,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
96
89
|
requirements: []
|
|
97
90
|
rubygems_version: 3.6.7
|
|
98
91
|
specification_version: 4
|
|
99
|
-
summary: Real-time web UI banner for monitoring
|
|
92
|
+
summary: Real-time web UI banner for monitoring background jobs and exception tracking
|
|
100
93
|
test_files: []
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Newshound
|
|
4
|
-
class QueReporter
|
|
5
|
-
attr_reader :job_source, :logger
|
|
6
|
-
|
|
7
|
-
def initialize(job_source: nil, logger: nil)
|
|
8
|
-
@job_source = job_source || (defined?(::Que::Job) ? ::Que::Job : nil)
|
|
9
|
-
@logger = logger || (defined?(Rails) ? Rails.logger : Logger.new($stdout))
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def generate_report
|
|
13
|
-
[
|
|
14
|
-
{
|
|
15
|
-
type: "section",
|
|
16
|
-
text: {
|
|
17
|
-
type: "mrkdwn",
|
|
18
|
-
text: "*📊 Que Jobs Status*"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
job_counts_section,
|
|
22
|
-
queue_health_section
|
|
23
|
-
].compact
|
|
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
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def job_counts_section
|
|
44
|
-
counts = job_counts_by_type
|
|
45
|
-
|
|
46
|
-
return no_jobs_section if counts.empty?
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
type: "section",
|
|
50
|
-
text: {
|
|
51
|
-
type: "mrkdwn",
|
|
52
|
-
text: format_job_counts(counts)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def job_counts_by_type
|
|
58
|
-
return {} unless job_source
|
|
59
|
-
|
|
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
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
hash[job_class][:total] += count
|
|
83
|
-
end
|
|
84
|
-
rescue => e
|
|
85
|
-
logger.error "Failed to fetch job counts: #{e.message}"
|
|
86
|
-
{}
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def format_job_counts(counts)
|
|
90
|
-
lines = ["*Job Counts by Type:*"]
|
|
91
|
-
|
|
92
|
-
counts.each do |job_class, stats|
|
|
93
|
-
status_emoji = (stats[:failed] > 0) ? "⚠️" : "✅"
|
|
94
|
-
lines << "• #{status_emoji} *#{job_class}*: #{stats[:total]} total (#{stats[:success]} success, #{stats[:failed]} failed)"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
lines.join("\n")
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def queue_health_section
|
|
101
|
-
stats = queue_statistics
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
type: "section",
|
|
105
|
-
text: {
|
|
106
|
-
type: "mrkdwn",
|
|
107
|
-
text: format_queue_health(stats)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def queue_statistics
|
|
113
|
-
return default_stats unless job_source
|
|
114
|
-
|
|
115
|
-
conn = ActiveRecord::Base.connection
|
|
116
|
-
current_time = conn.quote(Time.now)
|
|
117
|
-
beginning_of_day = conn.quote(Date.today.to_time)
|
|
118
|
-
|
|
119
|
-
{
|
|
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}")
|
|
124
|
-
}
|
|
125
|
-
rescue => e
|
|
126
|
-
logger.error "Failed to fetch Que statistics: #{e.message}"
|
|
127
|
-
default_stats
|
|
128
|
-
end
|
|
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
|
-
|
|
136
|
-
def default_stats
|
|
137
|
-
{ready: 0, scheduled: 0, failed: 0, finished_today: 0}
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def format_queue_health(stats)
|
|
141
|
-
health_emoji = if stats[:failed] > 10
|
|
142
|
-
"🔴"
|
|
143
|
-
else
|
|
144
|
-
(stats[:failed] > 5) ? "🟡" : "🟢"
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
<<~TEXT
|
|
148
|
-
*Queue Health #{health_emoji}*
|
|
149
|
-
• *Ready to Run:* #{stats[:ready]}
|
|
150
|
-
• *Scheduled:* #{stats[:scheduled]}
|
|
151
|
-
• *Failed (Retry Queue):* #{stats[:failed]}
|
|
152
|
-
• *Completed Today:* #{stats[:finished_today]}
|
|
153
|
-
TEXT
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def no_jobs_section
|
|
157
|
-
{
|
|
158
|
-
type: "section",
|
|
159
|
-
text: {
|
|
160
|
-
type: "mrkdwn",
|
|
161
|
-
text: "*No jobs found in the queue*"
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|