newshound 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: decd51fd02b481edd644a56bd700575010ae0b3d747fc2880803c68c32e50bfa
4
- data.tar.gz: f44304b8a0f6621296ff13900a2e59ea702b62c08ed6491ad0a60b4839b76b6d
3
+ metadata.gz: b5541adbc8e4ccb311bcce9a185946affc52cddc3d6c253779722a4901627401
4
+ data.tar.gz: 06c86ac9f83a00ce6e6e77063131a29eaf2fb7e6643528e65e6f0b3c329d1309
5
5
  SHA512:
6
- metadata.gz: 813c1c7d310854f34722819c8321d24438cafd543bf9c4dcf164e90bcf716423953ae0583656fe97476d454342fb8c4dabdfa48b66d886d6adb21abc6466521b
7
- data.tar.gz: 6c60ed849cd31e6e08066dc9893c2dcd3d3a82450923d97383717f2c5e21f2ea4943880b73ccce9926cbed83ead51b9cea0b2c08c91ba1fb8b63a6a8fb66e983
6
+ metadata.gz: 3d3e875cb30e1e3c27cd932f5f66fa8f9a6269d7e03c7cbb5428ce5ba47dd53acacb80721d5f51452b0d97a3d46a7005e93fec24c9954a96452ee861df6c962a
7
+ data.tar.gz: a5041975aab52ff2c50527f9f5050014d086908923b7f15830dce201f43a234a9086b3ba2d7747de90797ef07af0d769badbe431bd92105abd8b9e254f6b4499
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.4.3
data/Gemfile CHANGED
@@ -4,6 +4,12 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
- gem "rake", "~> 13.0"
8
- gem "rspec", "~> 3.0"
9
- gem "rubocop", "~> 1.0"
7
+ group :development, :test do
8
+ gem "bundler", "~> 2.0"
9
+ gem "rake", "~> 13.0"
10
+ gem "rspec", "~> 3.0"
11
+ gem "rubocop", "~> 1.0"
12
+ gem "pg"
13
+ gem "pry"
14
+ gem "simplecov", require: false
15
+ end
data/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # Newshound 🐕
2
2
 
3
- A Ruby gem that sniffs out exceptions and job statuses in your Rails app and reports them daily to Slack.
3
+ A Ruby gem that sniffs out exceptions and job statuses in your Rails app and reports them daily to Slack or other notification services.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - 📊 Daily Que job status reports (counts by job type, queue health)
8
8
  - 🚨 Last 4 exceptions from exception-track
9
- - 💬 Slack integration via webhook or Web API
9
+ - 💬 Multiple transport options: Direct Slack or Amazon SNS
10
10
  - ⏰ Automatic daily scheduling with que-scheduler
11
11
  - 🔧 Configurable report times and limits
12
+ - 🔄 Environment-based transport switching (SNS for production, Slack for development)
12
13
 
13
14
  ## Installation
14
15
 
@@ -16,6 +17,9 @@ Add to your Gemfile:
16
17
 
17
18
  ```ruby
18
19
  gem 'newshound', path: 'path/to/newshound' # or from git/rubygems when published
20
+
21
+ # Optional: Add AWS SDK if using SNS transport
22
+ gem 'aws-sdk-sns', '~> 1.0' # Only needed for SNS transport
19
23
  ```
20
24
 
21
25
  Then:
@@ -28,12 +32,17 @@ bundle install
28
32
 
29
33
  Create an initializer `config/initializers/newshound.rb`:
30
34
 
35
+ ### Basic Configuration (Slack Direct)
36
+
31
37
  ```ruby
32
38
  Newshound.configure do |config|
39
+ # Transport selection (optional, defaults to :slack)
40
+ config.transport_adapter = :slack
41
+
33
42
  # Slack configuration (choose one method)
34
43
  config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL'] # Option 1: Webhook
35
44
  # OR set ENV['SLACK_API_TOKEN'] for Web API # Option 2: Web API
36
-
45
+
37
46
  config.slack_channel = "#ops-alerts" # Default: "#general"
38
47
  config.report_time = "09:00" # Default: "09:00" (24-hour format)
39
48
  config.exception_limit = 4 # Default: 4 (last N exceptions)
@@ -42,7 +51,37 @@ Newshound.configure do |config|
42
51
  end
43
52
  ```
44
53
 
45
- ### Slack Setup
54
+ ### Production Configuration with SNS
55
+
56
+ ```ruby
57
+ Newshound.configure do |config|
58
+ if Rails.env.production?
59
+ # Use SNS in production to route through AWS
60
+ config.transport_adapter = :sns
61
+ config.sns_topic_arn = ENV['SNS_TOPIC_ARN']
62
+ config.aws_region = ENV['AWS_REGION'] || 'us-east-1'
63
+
64
+ # Optional: Explicit AWS credentials (uses IAM role by default)
65
+ config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
66
+ config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
67
+ else
68
+ # Use direct Slack in development/staging
69
+ config.transport_adapter = :slack
70
+ config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL']
71
+ end
72
+
73
+ # Common settings
74
+ config.slack_channel = "#ops-alerts"
75
+ config.report_time = "09:00"
76
+ config.exception_limit = 4
77
+ config.time_zone = "America/New_York"
78
+ config.enabled = true
79
+ end
80
+ ```
81
+
82
+ ## Transport Setup
83
+
84
+ ### Slack Setup (Direct Integration)
46
85
 
47
86
  #### Option 1: Webhook URL (Simpler)
48
87
  1. Go to https://api.slack.com/apps
@@ -58,6 +97,39 @@ end
58
97
  4. Copy the Bot User OAuth Token
59
98
  5. Set as `ENV['SLACK_API_TOKEN']`
60
99
 
100
+ ### AWS SNS Setup (Production Routing)
101
+
102
+ 1. **Create SNS Topic**:
103
+ ```bash
104
+ aws sns create-topic --name newshound-notifications
105
+ ```
106
+
107
+ 2. **Subscribe Slack Webhook to Topic**:
108
+ ```bash
109
+ aws sns subscribe \
110
+ --topic-arn arn:aws:sns:us-east-1:123456789:newshound-notifications \
111
+ --protocol https \
112
+ --notification-endpoint YOUR_SLACK_WEBHOOK_URL
113
+ ```
114
+
115
+ 3. **Configure IAM Permissions**:
116
+ ```json
117
+ {
118
+ "Version": "2012-10-17",
119
+ "Statement": [{
120
+ "Effect": "Allow",
121
+ "Action": "sns:Publish",
122
+ "Resource": "arn:aws:sns:us-east-1:*:newshound-*"
123
+ }]
124
+ }
125
+ ```
126
+
127
+ 4. **Set Environment Variables**:
128
+ ```bash
129
+ export SNS_TOPIC_ARN="arn:aws:sns:us-east-1:123456789:newshound-notifications"
130
+ export AWS_REGION="us-east-1"
131
+ ```
132
+
61
133
  ## Usage
62
134
 
63
135
  ### Automatic Daily Reports
@@ -128,8 +200,16 @@ bin/console
128
200
 
129
201
  ```ruby
130
202
  # In rails console
203
+
204
+ # Test Slack transport
205
+ Newshound.configuration.transport_adapter = :slack
131
206
  Newshound.configuration.slack_webhook_url = "your-webhook"
132
207
  Newshound.report! # Should post to Slack immediately
208
+
209
+ # Test SNS transport
210
+ Newshound.configuration.transport_adapter = :sns
211
+ Newshound.configuration.sns_topic_arn = "your-topic-arn"
212
+ Newshound.report! # Should publish to SNS
133
213
  ```
134
214
 
135
215
  ## Troubleshooting
@@ -138,6 +218,8 @@ Newshound.report! # Should post to Slack immediately
138
218
  - **No exceptions showing**: Ensure `exception-track` gem is installed and logging
139
219
  - **No job data**: Verify Que is configured and `que_jobs` table exists
140
220
  - **Slack not receiving**: Verify webhook URL or API token is correct
221
+ - **SNS not publishing**: Check IAM permissions and topic ARN configuration
222
+ - **AWS SDK errors**: Ensure `aws-sdk-sns` gem is installed when using SNS transport
141
223
 
142
224
  ## License
143
225
 
@@ -0,0 +1,117 @@
1
+ # Transport Layer Usage
2
+
3
+ The Newshound gem now supports multiple transport adapters for sending notifications. You can choose between Slack (direct) and Amazon SNS based on your environment needs.
4
+
5
+ ## Configuration
6
+
7
+ ### Using Slack Transport (Default)
8
+
9
+ ```ruby
10
+ # config/initializers/newshound.rb
11
+ Newshound.configure do |config|
12
+ config.transport_adapter = :slack # This is the default
13
+ config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL']
14
+ config.slack_channel = '#your-channel'
15
+ # ... other configurations
16
+ end
17
+ ```
18
+
19
+ ### Using SNS Transport
20
+
21
+ ```ruby
22
+ # config/initializers/newshound.rb
23
+ Newshound.configure do |config|
24
+ config.transport_adapter = :sns
25
+ config.sns_topic_arn = ENV['SNS_TOPIC_ARN']
26
+ config.aws_region = ENV['AWS_REGION'] || 'us-east-1'
27
+
28
+ # Optional: Provide AWS credentials explicitly
29
+ # If not provided, will use default AWS credential chain
30
+ config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
31
+ config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
32
+
33
+ # ... other configurations
34
+ end
35
+ ```
36
+
37
+ ### Environment-based Configuration
38
+
39
+ You can switch transport based on environment:
40
+
41
+ ```ruby
42
+ # config/initializers/newshound.rb
43
+ Newshound.configure do |config|
44
+ if Rails.env.production?
45
+ # Use SNS in production to route through AWS infrastructure
46
+ config.transport_adapter = :sns
47
+ config.sns_topic_arn = ENV['SNS_TOPIC_ARN']
48
+ config.aws_region = ENV['AWS_REGION']
49
+ else
50
+ # Use direct Slack in development/staging
51
+ config.transport_adapter = :slack
52
+ config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL']
53
+ config.slack_channel = '#dev-notifications'
54
+ end
55
+
56
+ # Common configurations
57
+ config.report_time = "09:00"
58
+ config.exception_limit = 4
59
+ config.time_zone = "America/New_York"
60
+ end
61
+ ```
62
+
63
+ ## Custom Transport Adapters
64
+
65
+ You can also create your own transport adapter:
66
+
67
+ ```ruby
68
+ class MyCustomTransport < Newshound::Transport::Base
69
+ def deliver(message)
70
+ # Your custom delivery logic here
71
+ # Return true on success, false on failure
72
+
73
+ # Example: Send to custom API endpoint
74
+ response = HTTParty.post(
75
+ 'https://api.example.com/notifications',
76
+ body: message.to_json,
77
+ headers: { 'Content-Type' => 'application/json' }
78
+ )
79
+
80
+ response.success?
81
+ rescue StandardError => e
82
+ logger.error "Failed to deliver: #{e.message}"
83
+ false
84
+ end
85
+ end
86
+
87
+ # Use the custom transport
88
+ Newshound.configure do |config|
89
+ config.transport_adapter = MyCustomTransport
90
+ # ... other configurations
91
+ end
92
+ ```
93
+
94
+ ## AWS SNS Setup
95
+
96
+ If using SNS transport, ensure you have:
97
+
98
+ 1. Created an SNS topic in AWS
99
+ 2. Subscribed your Slack webhook URL to the SNS topic
100
+ 3. Configured proper IAM permissions for publishing to the topic
101
+ 4. Installed the aws-sdk-sns gem (it's optional):
102
+
103
+ ```ruby
104
+ # Add to your Gemfile if using SNS
105
+ gem 'aws-sdk-sns', '~> 1.0'
106
+ ```
107
+
108
+ ## Testing
109
+
110
+ The transport layer is fully testable. You can inject mock transports in your tests:
111
+
112
+ ```ruby
113
+ # In your tests
114
+ mock_transport = double('Transport', deliver: true)
115
+ notifier = Newshound::SlackNotifier.new(transport: mock_transport)
116
+ notifier.post({ text: "Test message" })
117
+ ```
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "yaml"
5
+
6
+ module Newshound
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Configures Newshound for your Rails application"
12
+
13
+ def create_initializer
14
+ template "newshound.rb", "config/initializers/newshound.rb"
15
+ end
16
+
17
+ def add_to_que_schedule
18
+ que_schedule_path = "config/que_schedule.yml"
19
+
20
+ if File.exist?(que_schedule_path)
21
+ say_status :info, "Adding Newshound job to #{que_schedule_path}", :blue
22
+
23
+ # Read existing YAML
24
+ existing_config = YAML.load_file(que_schedule_path) || {}
25
+
26
+ # Add Newshound job if not already present
27
+ unless existing_config.key?("Newshound::DailyReportJob")
28
+ # Append the configuration to the file
29
+ append_to_file que_schedule_path do
30
+ <<~YAML
31
+
32
+ # Newshound daily report job - sends exception and queue reports to Slack
33
+ Newshound::DailyReportJob:
34
+ cron: "0 9 * * *" # Daily at 9:00 AM - adjust as needed
35
+ queue: default
36
+ args: []
37
+ YAML
38
+ end
39
+
40
+ say_status :success, "Added Newshound::DailyReportJob to que_schedule.yml", :green
41
+ else
42
+ say_status :skip, "Newshound::DailyReportJob already exists in que_schedule.yml", :yellow
43
+ end
44
+ else
45
+ say_status :warning, "#{que_schedule_path} not found. Creating it with Newshound job.", :yellow
46
+ create_file que_schedule_path do
47
+ <<~YAML
48
+ # Que-scheduler configuration
49
+ # See https://github.com/hlascelles/que-scheduler for more information
50
+
51
+ # Newshound daily report job - sends exception and queue reports to Slack
52
+ Newshound::DailyReportJob:
53
+ cron: "0 9 * * *" # Daily at 9:00 AM - adjust as needed
54
+ queue: default
55
+ args: []
56
+ YAML
57
+ end
58
+ end
59
+ end
60
+
61
+ def display_post_install_message
62
+ say ""
63
+ say "===============================================================================", :green
64
+ say " Newshound has been successfully installed!", :green
65
+ say ""
66
+ say " Next steps:", :yellow
67
+ say " 1. Configure your Slack webhook URL in config/initializers/newshound.rb"
68
+ say " 2. Adjust the report schedule in config/que_schedule.yml if needed"
69
+ say " 3. Restart your Rails server and Que workers"
70
+ say ""
71
+ say " To test your configuration, run:", :cyan
72
+ say " rails runner 'Newshound.report!'"
73
+ say ""
74
+ say " For more information, visit:", :blue
75
+ say " https://github.com/salbanez/newshound"
76
+ say "===============================================================================", :green
77
+ say ""
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ Newshound.configure do |config|
4
+ # Slack webhook URL for sending reports (required)
5
+ # You can get this from your Slack app settings
6
+ config.slack_webhook_url = ENV.fetch("NEWSHOUND_SLACK_WEBHOOK_URL", nil)
7
+
8
+ # Channel to post reports to
9
+ # Default is "#general"
10
+ config.slack_channel = "#engineering"
11
+
12
+ # Time to send daily report (24-hour format)
13
+ # Default is "09:00" (9:00 AM)
14
+ config.report_time = "09:00"
15
+
16
+ # Maximum number of exceptions to include in report
17
+ # Default is 4
18
+ config.exception_limit = 4
19
+
20
+ # Time zone for scheduling reports
21
+ # Default is "America/New_York"
22
+ config.time_zone = "America/New_York"
23
+
24
+ # Enable or disable Newshound completely
25
+ # Useful for disabling in development/test environments
26
+ # Default is true
27
+ config.enabled = Rails.env.production?
28
+
29
+ # Transport adapter (:slack or :sns)
30
+ # Default is :slack
31
+ config.transport_adapter = :slack
32
+
33
+ # AWS SNS Configuration (only needed if transport_adapter is :sns)
34
+ # config.sns_topic_arn = ENV.fetch("NEWSHOUND_SNS_TOPIC_ARN", nil)
35
+ # config.aws_region = ENV.fetch("AWS_REGION", "us-east-1")
36
+ # config.aws_access_key_id = ENV.fetch("AWS_ACCESS_KEY_ID", nil)
37
+ # config.aws_secret_access_key = ENV.fetch("AWS_SECRET_ACCESS_KEY", nil)
38
+ end
@@ -3,7 +3,9 @@
3
3
  module Newshound
4
4
  class Configuration
5
5
  attr_accessor :slack_webhook_url, :slack_channel, :report_time,
6
- :exception_limit, :time_zone, :enabled
6
+ :exception_limit, :time_zone, :enabled,
7
+ :transport_adapter, :sns_topic_arn, :aws_region,
8
+ :aws_access_key_id, :aws_secret_access_key
7
9
 
8
10
  def initialize
9
11
  @slack_webhook_url = nil
@@ -12,6 +14,11 @@ module Newshound
12
14
  @exception_limit = 4
13
15
  @time_zone = "America/New_York"
14
16
  @enabled = true
17
+ @transport_adapter = :slack
18
+ @sns_topic_arn = nil
19
+ @aws_region = nil
20
+ @aws_access_key_id = nil
21
+ @aws_secret_access_key = nil
15
22
  end
16
23
 
17
24
  def valid?
@@ -1,19 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Newshound
4
- class DailyReportJob < ::Que::Job
5
- def run
6
- return unless Newshound.configuration.valid?
7
-
8
- Newshound.report!
9
-
10
- destroy
11
- rescue StandardError => e
12
- Rails.logger.error "Newshound::DailyReportJob failed: #{e.message}"
13
- Rails.logger.error e.backtrace.join("\n")
14
-
15
- # Re-raise to let Que handle retry logic
16
- raise
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
17
29
  end
18
30
  end
19
31
  end
@@ -2,9 +2,17 @@
2
2
 
3
3
  module Newshound
4
4
  class ExceptionReporter
5
+ attr_reader :exception_source, :configuration, :time_range
6
+
7
+ def initialize(exception_source: nil, configuration: nil, time_range: 24.hours)
8
+ @exception_source = exception_source || (defined?(ExceptionTrack::Log) ? ExceptionTrack::Log : nil)
9
+ @configuration = configuration || Newshound.configuration
10
+ @time_range = time_range
11
+ end
12
+
5
13
  def generate_report
6
14
  return no_exceptions_block if recent_exceptions.empty?
7
-
15
+
8
16
  [
9
17
  {
10
18
  type: "section",
@@ -24,12 +32,12 @@ module Newshound
24
32
  end
25
33
 
26
34
  def fetch_recent_exceptions
27
- return [] unless defined?(ExceptionTrack::Log)
28
-
29
- ExceptionTrack::Log
30
- .where("created_at >= ?", 24.hours.ago)
35
+ return [] unless exception_source
36
+
37
+ exception_source
38
+ .where("created_at >= ?", time_range.ago)
31
39
  .order(created_at: :desc)
32
- .limit(Newshound.configuration.exception_limit)
40
+ .limit(configuration.exception_limit)
33
41
  end
34
42
 
35
43
  def format_exceptions
@@ -62,9 +70,11 @@ module Newshound
62
70
  end
63
71
 
64
72
  def exception_count(exception)
65
- ExceptionTrack::Log
73
+ return 0 unless exception_source
74
+
75
+ exception_source
66
76
  .where(exception_class: exception.exception_class)
67
- .where("created_at >= ?", 24.hours.ago)
77
+ .where("created_at >= ?", time_range.ago)
68
78
  .count
69
79
  end
70
80
 
@@ -2,6 +2,13 @@
2
2
 
3
3
  module Newshound
4
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
+
5
12
  def generate_report
6
13
  [
7
14
  {
@@ -33,9 +40,9 @@ module Newshound
33
40
  end
34
41
 
35
42
  def job_counts_by_type
36
- return {} unless defined?(::Que::Job)
37
-
38
- ::Que::Job
43
+ return {} unless job_source
44
+
45
+ job_source
39
46
  .group(:job_class)
40
47
  .group(:error_count)
41
48
  .count
@@ -76,16 +83,19 @@ module Newshound
76
83
  end
77
84
 
78
85
  def queue_statistics
79
- return default_stats unless defined?(::Que::Job)
80
-
86
+ return default_stats unless job_source
87
+
88
+ current_time = Time.now
89
+ beginning_of_day = Date.today.to_time
90
+
81
91
  {
82
- ready: ::Que::Job.where(finished_at: nil, expired_at: nil).where("run_at <= ?", Time.current).count,
83
- scheduled: ::Que::Job.where(finished_at: nil, expired_at: nil).where("run_at > ?", Time.current).count,
84
- failed: ::Que::Job.where.not(error_count: 0).where(finished_at: nil).count,
85
- finished_today: ::Que::Job.where("finished_at >= ?", Date.current.beginning_of_day).count
92
+ ready: job_source.where(finished_at: nil, expired_at: nil).where("run_at <= ?", current_time).count,
93
+ scheduled: job_source.where(finished_at: nil, expired_at: nil).where("run_at > ?", current_time).count,
94
+ failed: job_source.where.not(error_count: 0).where(finished_at: nil).count,
95
+ finished_today: job_source.where("finished_at >= ?", beginning_of_day).count
86
96
  }
87
97
  rescue StandardError => e
88
- Rails.logger.error "Failed to fetch Que statistics: #{e.message}"
98
+ logger.error "Failed to fetch Que statistics: #{e.message}"
89
99
  default_stats
90
100
  end
91
101
 
@@ -4,12 +4,13 @@ module Newshound
4
4
  class Scheduler
5
5
  def self.schedule_daily_report
6
6
  return unless defined?(::Que::Scheduler)
7
-
7
+
8
8
  config = Newshound.configuration
9
9
  return unless config.valid?
10
-
11
- # Schedule the job using que-scheduler
12
- # This will be picked up by que-scheduler's configuration
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
13
14
  schedule_config = {
14
15
  "newshound_daily_report" => {
15
16
  "class" => "Newshound::DailyReportJob",
@@ -18,12 +19,12 @@ module Newshound
18
19
  "args" => []
19
20
  }
20
21
  }
21
-
22
- # Merge with existing schedule if any
23
- if defined?(::Que::Scheduler.configuration)
24
- ::Que::Scheduler.configuration.merge!(schedule_config)
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']})"
25
26
  end
26
-
27
+
27
28
  schedule_config
28
29
  end
29
30
 
@@ -4,58 +4,41 @@ require "slack-ruby-client"
4
4
 
5
5
  module Newshound
6
6
  class SlackNotifier
7
- def initialize
8
- configure_slack_client
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
9
13
  end
10
14
 
11
15
  def post(message)
12
- return unless valid_configuration?
13
-
14
- if webhook_configured?
15
- post_via_webhook(message)
16
- elsif web_api_configured?
17
- post_via_web_api(message)
18
- else
19
- Rails.logger.error "Newshound: No valid Slack configuration found"
20
- end
16
+ return unless configuration.valid?
17
+
18
+ transport.deliver(message)
21
19
  rescue StandardError => e
22
- Rails.logger.error "Newshound: Failed to send Slack notification: #{e.message}"
20
+ logger.error "Newshound: Failed to send notification: #{e.message}"
23
21
  end
24
22
 
25
23
  private
26
24
 
27
- def configure_slack_client
28
- Slack.configure do |config|
29
- config.token = ENV["SLACK_API_TOKEN"] if ENV["SLACK_API_TOKEN"]
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
30
41
  end
31
42
  end
32
-
33
- def valid_configuration?
34
- return false unless Newshound.configuration.valid?
35
-
36
- webhook_configured? || web_api_configured?
37
- end
38
-
39
- def webhook_configured?
40
- Newshound.configuration.slack_webhook_url.present?
41
- end
42
-
43
- def web_api_configured?
44
- ENV["SLACK_API_TOKEN"].present?
45
- end
46
-
47
- def post_via_webhook(message)
48
- client = Slack::Incoming::Webhook.new(Newshound.configuration.slack_webhook_url)
49
- client.post(message)
50
- end
51
-
52
- def post_via_web_api(message)
53
- client = Slack::Web::Client.new
54
- client.chat_postMessage(
55
- channel: Newshound.configuration.slack_channel,
56
- blocks: message[:blocks],
57
- text: "Daily Newshound Report"
58
- )
59
- end
60
43
  end
61
44
  end
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,67 @@
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
@@ -0,0 +1,115 @@
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Newshound
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
data/newshound.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/newshound/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "newshound"
7
+ spec.version = Newshound::VERSION
8
+ spec.authors = ["salbanez"]
9
+ spec.email = ["salbanez@example.com"]
10
+
11
+ spec.summary = "Daily Slack reporter for Que jobs status and exception tracking"
12
+ spec.description = "Newshound sniffs out exceptions and job statuses in your Rails app and reports them daily to Slack"
13
+ spec.homepage = "https://github.com/salbanez/newshound"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.7.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ # Runtime dependencies
31
+ spec.add_dependency "rails", ">= 6.0"
32
+ spec.add_dependency "slack-ruby-client", "~> 2.0"
33
+ spec.add_dependency "que", ">= 1.0"
34
+ spec.add_dependency "que-scheduler", ">= 4.0"
35
+ 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
+ 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.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - salbanez
@@ -80,49 +80,7 @@ dependencies:
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0.1'
82
82
  - !ruby/object:Gem::Dependency
83
- name: bundler
84
- requirement: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: '2.0'
89
- type: :development
90
- prerelease: false
91
- version_requirements: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '2.0'
96
- - !ruby/object:Gem::Dependency
97
- name: rake
98
- requirement: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
101
- - !ruby/object:Gem::Version
102
- version: '13.0'
103
- type: :development
104
- prerelease: false
105
- version_requirements: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - "~>"
108
- - !ruby/object:Gem::Version
109
- version: '13.0'
110
- - !ruby/object:Gem::Dependency
111
- name: rspec
112
- requirement: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - "~>"
115
- - !ruby/object:Gem::Version
116
- version: '3.0'
117
- type: :development
118
- prerelease: false
119
- version_requirements: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - "~>"
122
- - !ruby/object:Gem::Version
123
- version: '3.0'
124
- - !ruby/object:Gem::Dependency
125
- name: rubocop
83
+ name: aws-sdk-sns
126
84
  requirement: !ruby/object:Gem::Requirement
127
85
  requirements:
128
86
  - - "~>"
@@ -135,20 +93,6 @@ dependencies:
135
93
  - - "~>"
136
94
  - !ruby/object:Gem::Version
137
95
  version: '1.0'
138
- - !ruby/object:Gem::Dependency
139
- name: pg
140
- requirement: !ruby/object:Gem::Requirement
141
- requirements:
142
- - - ">="
143
- - !ruby/object:Gem::Version
144
- version: '0'
145
- type: :development
146
- prerelease: false
147
- version_requirements: !ruby/object:Gem::Requirement
148
- requirements:
149
- - - ">="
150
- - !ruby/object:Gem::Version
151
- version: '0'
152
96
  description: Newshound sniffs out exceptions and job statuses in your Rails app and
153
97
  reports them daily to Slack
154
98
  email:
@@ -157,9 +101,14 @@ executables: []
157
101
  extensions: []
158
102
  extra_rdoc_files: []
159
103
  files:
104
+ - ".rspec"
105
+ - ".tool-versions"
160
106
  - Gemfile
161
107
  - README.md
162
108
  - Rakefile
109
+ - TRANSPORT_USAGE.md
110
+ - lib/generators/newshound/install/install_generator.rb
111
+ - lib/generators/newshound/install/templates/newshound.rb
163
112
  - lib/newshound.rb
164
113
  - lib/newshound/configuration.rb
165
114
  - lib/newshound/daily_report_job.rb
@@ -168,7 +117,11 @@ files:
168
117
  - lib/newshound/railtie.rb
169
118
  - lib/newshound/scheduler.rb
170
119
  - lib/newshound/slack_notifier.rb
120
+ - lib/newshound/transport/base.rb
121
+ - lib/newshound/transport/slack.rb
122
+ - lib/newshound/transport/sns.rb
171
123
  - lib/newshound/version.rb
124
+ - newshound.gemspec
172
125
  homepage: https://github.com/salbanez/newshound
173
126
  licenses:
174
127
  - MIT