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
data/newshound.gemspec
CHANGED
|
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["salbanez"]
|
|
9
9
|
spec.email = ["salbanez@example.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "
|
|
12
|
-
spec.description = "Newshound
|
|
11
|
+
spec.summary = "Real-time web UI banner for monitoring Que jobs and exception tracking"
|
|
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/salbanez/newshound"
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
spec.required_ruby_version = ">= 2.7.0"
|
|
@@ -29,11 +29,6 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
|
|
30
30
|
# Runtime dependencies
|
|
31
31
|
spec.add_dependency "rails", ">= 6.0"
|
|
32
|
-
spec.add_dependency "slack-ruby-client", "~> 2.0"
|
|
33
32
|
spec.add_dependency "que", ">= 1.0"
|
|
34
|
-
spec.add_dependency "que-scheduler", ">= 4.0"
|
|
35
33
|
spec.add_dependency "exception-track", ">= 0.1"
|
|
36
|
-
|
|
37
|
-
# Optional dependency for SNS transport
|
|
38
|
-
spec.add_development_dependency "aws-sdk-sns", "~> 1.0"
|
|
39
34
|
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: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- salbanez
|
|
@@ -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: slack-ruby-client
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '2.0'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '2.0'
|
|
40
26
|
- !ruby/object:Gem::Dependency
|
|
41
27
|
name: que
|
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,20 +37,6 @@ dependencies:
|
|
|
51
37
|
- - ">="
|
|
52
38
|
- !ruby/object:Gem::Version
|
|
53
39
|
version: '1.0'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: que-scheduler
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - ">="
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '4.0'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - ">="
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '4.0'
|
|
68
40
|
- !ruby/object:Gem::Dependency
|
|
69
41
|
name: exception-track
|
|
70
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,22 +51,8 @@ dependencies:
|
|
|
79
51
|
- - ">="
|
|
80
52
|
- !ruby/object:Gem::Version
|
|
81
53
|
version: '0.1'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - "~>"
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: '1.0'
|
|
89
|
-
type: :development
|
|
90
|
-
prerelease: false
|
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
-
requirements:
|
|
93
|
-
- - "~>"
|
|
94
|
-
- !ruby/object:Gem::Version
|
|
95
|
-
version: '1.0'
|
|
96
|
-
description: Newshound sniffs out exceptions and job statuses in your Rails app and
|
|
97
|
-
reports them daily to Slack
|
|
54
|
+
description: Newshound displays exceptions and job statuses in a collapsible banner
|
|
55
|
+
for authorized users in your Rails app
|
|
98
56
|
email:
|
|
99
57
|
- salbanez@example.com
|
|
100
58
|
executables: []
|
|
@@ -110,16 +68,12 @@ files:
|
|
|
110
68
|
- lib/generators/newshound/install/install_generator.rb
|
|
111
69
|
- lib/generators/newshound/install/templates/newshound.rb
|
|
112
70
|
- lib/newshound.rb
|
|
71
|
+
- lib/newshound/authorization.rb
|
|
113
72
|
- lib/newshound/configuration.rb
|
|
114
|
-
- lib/newshound/daily_report_job.rb
|
|
115
73
|
- lib/newshound/exception_reporter.rb
|
|
74
|
+
- lib/newshound/middleware/banner_injector.rb
|
|
116
75
|
- lib/newshound/que_reporter.rb
|
|
117
76
|
- lib/newshound/railtie.rb
|
|
118
|
-
- lib/newshound/scheduler.rb
|
|
119
|
-
- lib/newshound/slack_notifier.rb
|
|
120
|
-
- lib/newshound/transport/base.rb
|
|
121
|
-
- lib/newshound/transport/slack.rb
|
|
122
|
-
- lib/newshound/transport/sns.rb
|
|
123
77
|
- lib/newshound/version.rb
|
|
124
78
|
- newshound.gemspec
|
|
125
79
|
homepage: https://github.com/salbanez/newshound
|
|
@@ -145,5 +99,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
145
99
|
requirements: []
|
|
146
100
|
rubygems_version: 3.6.9
|
|
147
101
|
specification_version: 4
|
|
148
|
-
summary:
|
|
102
|
+
summary: Real-time web UI banner for monitoring Que jobs and exception tracking
|
|
149
103
|
test_files: []
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Newshound
|
|
4
|
-
if defined?(::Que::Job)
|
|
5
|
-
class DailyReportJob < ::Que::Job
|
|
6
|
-
def run
|
|
7
|
-
return unless Newshound.configuration.valid?
|
|
8
|
-
|
|
9
|
-
Newshound.report!
|
|
10
|
-
|
|
11
|
-
destroy
|
|
12
|
-
rescue StandardError => e
|
|
13
|
-
Rails.logger.error "Newshound::DailyReportJob failed: #{e.message}"
|
|
14
|
-
Rails.logger.error e.backtrace.join("\n")
|
|
15
|
-
|
|
16
|
-
# Re-raise to let Que handle retry logic
|
|
17
|
-
raise
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
else
|
|
21
|
-
class DailyReportJob
|
|
22
|
-
def self.enqueue(*args)
|
|
23
|
-
Rails.logger.warn "Que is not available. DailyReportJob cannot be enqueued."
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def run
|
|
27
|
-
Rails.logger.warn "Que is not available. DailyReportJob cannot be run."
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
data/lib/newshound/scheduler.rb
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Newshound
|
|
4
|
-
class Scheduler
|
|
5
|
-
def self.schedule_daily_report
|
|
6
|
-
return unless defined?(::Que::Scheduler)
|
|
7
|
-
|
|
8
|
-
config = Newshound.configuration
|
|
9
|
-
return unless config.valid?
|
|
10
|
-
|
|
11
|
-
# Note: Que-scheduler uses a YAML config file (config/que_schedule.yml)
|
|
12
|
-
# This method returns the configuration that should be added to that file
|
|
13
|
-
# or can be used to manually schedule the job
|
|
14
|
-
schedule_config = {
|
|
15
|
-
"newshound_daily_report" => {
|
|
16
|
-
"class" => "Newshound::DailyReportJob",
|
|
17
|
-
"cron" => build_cron_expression(config.report_time),
|
|
18
|
-
"queue" => "default",
|
|
19
|
-
"args" => []
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
# Log the configuration for visibility
|
|
24
|
-
if defined?(Rails) && Rails.logger
|
|
25
|
-
Rails.logger.info "Newshound daily report scheduled for #{config.report_time} (cron: #{schedule_config['newshound_daily_report']['cron']})"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
schedule_config
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def self.build_cron_expression(time_string)
|
|
32
|
-
# Convert "09:00" format to cron expression
|
|
33
|
-
# Format: minute hour * * *
|
|
34
|
-
hour, minute = time_string.split(":").map(&:to_i)
|
|
35
|
-
"#{minute} #{hour} * * *"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def self.run_now!
|
|
39
|
-
Newshound::DailyReportJob.enqueue
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "slack-ruby-client"
|
|
4
|
-
|
|
5
|
-
module Newshound
|
|
6
|
-
class SlackNotifier
|
|
7
|
-
attr_reader :configuration, :logger, :transport
|
|
8
|
-
|
|
9
|
-
def initialize(configuration: nil, logger: nil, transport: nil)
|
|
10
|
-
@configuration = configuration || Newshound.configuration
|
|
11
|
-
@logger = logger || (defined?(Rails) ? Rails.logger : Logger.new(STDOUT))
|
|
12
|
-
@transport = transport || build_transport
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def post(message)
|
|
16
|
-
return unless configuration.valid?
|
|
17
|
-
|
|
18
|
-
transport.deliver(message)
|
|
19
|
-
rescue StandardError => e
|
|
20
|
-
logger.error "Newshound: Failed to send notification: #{e.message}"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def build_transport
|
|
26
|
-
case configuration.transport_adapter
|
|
27
|
-
when :sns, "sns"
|
|
28
|
-
require_relative "transport/sns"
|
|
29
|
-
Transport::Sns.new(configuration: configuration, logger: logger)
|
|
30
|
-
when :slack, "slack", nil
|
|
31
|
-
require_relative "transport/slack"
|
|
32
|
-
Transport::Slack.new(configuration: configuration, logger: logger)
|
|
33
|
-
else
|
|
34
|
-
if configuration.transport_adapter.is_a?(Class)
|
|
35
|
-
configuration.transport_adapter.new(configuration: configuration, logger: logger)
|
|
36
|
-
elsif configuration.transport_adapter.respond_to?(:new)
|
|
37
|
-
configuration.transport_adapter.new(configuration: configuration, logger: logger)
|
|
38
|
-
else
|
|
39
|
-
raise ArgumentError, "Invalid transport adapter: #{configuration.transport_adapter}"
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Newshound
|
|
4
|
-
module Transport
|
|
5
|
-
class Base
|
|
6
|
-
attr_reader :configuration, :logger
|
|
7
|
-
|
|
8
|
-
def initialize(configuration: nil, logger: nil)
|
|
9
|
-
@configuration = configuration || Newshound.configuration
|
|
10
|
-
@logger = logger || default_logger
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def deliver(message)
|
|
14
|
-
raise NotImplementedError, "Subclasses must implement the #deliver method"
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
protected
|
|
18
|
-
|
|
19
|
-
def default_logger
|
|
20
|
-
if defined?(Rails)
|
|
21
|
-
Rails.logger
|
|
22
|
-
else
|
|
23
|
-
Logger.new(STDOUT)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "slack-ruby-client"
|
|
4
|
-
require_relative "base"
|
|
5
|
-
|
|
6
|
-
module Newshound
|
|
7
|
-
module Transport
|
|
8
|
-
class Slack < Base
|
|
9
|
-
attr_reader :webhook_client, :web_api_client
|
|
10
|
-
|
|
11
|
-
def initialize(configuration: nil, logger: nil, webhook_client: nil, web_api_client: nil)
|
|
12
|
-
super(configuration: configuration, logger: logger)
|
|
13
|
-
@webhook_client = webhook_client
|
|
14
|
-
@web_api_client = web_api_client
|
|
15
|
-
configure_slack_client
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def deliver(message)
|
|
19
|
-
return unless configuration.valid?
|
|
20
|
-
|
|
21
|
-
if webhook_configured?
|
|
22
|
-
deliver_via_webhook(message)
|
|
23
|
-
elsif web_api_configured?
|
|
24
|
-
deliver_via_web_api(message)
|
|
25
|
-
else
|
|
26
|
-
logger.error "Newshound: No valid Slack configuration found"
|
|
27
|
-
false
|
|
28
|
-
end
|
|
29
|
-
rescue StandardError => e
|
|
30
|
-
logger.error "Newshound: Failed to send Slack notification: #{e.message}"
|
|
31
|
-
false
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def configure_slack_client
|
|
37
|
-
::Slack.configure do |config|
|
|
38
|
-
config.token = ENV["SLACK_API_TOKEN"] if ENV["SLACK_API_TOKEN"]
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def webhook_configured?
|
|
43
|
-
!configuration.slack_webhook_url.nil? && !configuration.slack_webhook_url.empty?
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def web_api_configured?
|
|
47
|
-
!ENV["SLACK_API_TOKEN"].nil? && !ENV["SLACK_API_TOKEN"].empty?
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def deliver_via_webhook(message)
|
|
51
|
-
client = webhook_client || ::Slack::Incoming::Webhook.new(configuration.slack_webhook_url)
|
|
52
|
-
client.post(message)
|
|
53
|
-
true
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def deliver_via_web_api(message)
|
|
57
|
-
client = web_api_client || ::Slack::Web::Client.new
|
|
58
|
-
client.chat_postMessage(
|
|
59
|
-
channel: configuration.slack_channel,
|
|
60
|
-
blocks: message[:blocks],
|
|
61
|
-
text: "Daily Newshound Report"
|
|
62
|
-
)
|
|
63
|
-
true
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "base"
|
|
4
|
-
|
|
5
|
-
module Newshound
|
|
6
|
-
module Transport
|
|
7
|
-
class Sns < Base
|
|
8
|
-
attr_reader :sns_client
|
|
9
|
-
|
|
10
|
-
def initialize(configuration: nil, logger: nil, sns_client: nil)
|
|
11
|
-
super(configuration: configuration, logger: logger)
|
|
12
|
-
@sns_client = sns_client || build_sns_client
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def deliver(message)
|
|
16
|
-
return false unless valid_sns_configuration?
|
|
17
|
-
|
|
18
|
-
formatted_message = format_message(message)
|
|
19
|
-
|
|
20
|
-
response = sns_client.publish(
|
|
21
|
-
topic_arn: configuration.sns_topic_arn,
|
|
22
|
-
message: formatted_message,
|
|
23
|
-
subject: extract_subject(message)
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
logger.info "Newshound: Message sent to SNS, MessageId: #{response.message_id}"
|
|
27
|
-
true
|
|
28
|
-
rescue StandardError => e
|
|
29
|
-
logger.error "Newshound: Failed to send SNS notification: #{e.message}"
|
|
30
|
-
false
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def build_sns_client
|
|
36
|
-
require "aws-sdk-sns"
|
|
37
|
-
|
|
38
|
-
options = {
|
|
39
|
-
region: configuration.aws_region || ENV["AWS_REGION"] || "us-east-1"
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if configuration.aws_access_key_id && configuration.aws_secret_access_key
|
|
43
|
-
options[:credentials] = Aws::Credentials.new(
|
|
44
|
-
configuration.aws_access_key_id,
|
|
45
|
-
configuration.aws_secret_access_key
|
|
46
|
-
)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
Aws::SNS::Client.new(options)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def valid_sns_configuration?
|
|
53
|
-
if configuration.sns_topic_arn.nil? || configuration.sns_topic_arn.empty?
|
|
54
|
-
logger.error "Newshound: SNS topic ARN not configured"
|
|
55
|
-
false
|
|
56
|
-
else
|
|
57
|
-
true
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def format_message(message)
|
|
62
|
-
case message
|
|
63
|
-
when Hash
|
|
64
|
-
if message[:blocks]
|
|
65
|
-
format_slack_blocks_for_sns(message[:blocks])
|
|
66
|
-
else
|
|
67
|
-
JSON.pretty_generate(message)
|
|
68
|
-
end
|
|
69
|
-
when String
|
|
70
|
-
message
|
|
71
|
-
else
|
|
72
|
-
message.to_s
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def format_slack_blocks_for_sns(blocks)
|
|
77
|
-
lines = []
|
|
78
|
-
|
|
79
|
-
blocks.each do |block|
|
|
80
|
-
case block[:type]
|
|
81
|
-
when "section"
|
|
82
|
-
if block[:text]
|
|
83
|
-
lines << format_text_element(block[:text])
|
|
84
|
-
end
|
|
85
|
-
when "header"
|
|
86
|
-
if block[:text]
|
|
87
|
-
lines << "=== #{format_text_element(block[:text])} ==="
|
|
88
|
-
end
|
|
89
|
-
when "divider"
|
|
90
|
-
lines << "---"
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
lines.join("\n\n")
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def format_text_element(text_element)
|
|
98
|
-
return "" unless text_element
|
|
99
|
-
|
|
100
|
-
text = text_element[:text] || ""
|
|
101
|
-
text.gsub(/:([a-z_]+):/, '')
|
|
102
|
-
.gsub(/\*(.+?)\*/, '\1')
|
|
103
|
-
.gsub(/_(.+?)_/, '\1')
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def extract_subject(message)
|
|
107
|
-
if message.is_a?(Hash)
|
|
108
|
-
message[:subject] || "Newshound Notification"
|
|
109
|
-
else
|
|
110
|
-
"Newshound Notification"
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|