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 +4 -4
- data/.rspec +3 -0
- data/.tool-versions +1 -0
- data/Gemfile +9 -3
- data/README.md +86 -4
- data/TRANSPORT_USAGE.md +117 -0
- data/lib/generators/newshound/install/install_generator.rb +81 -0
- data/lib/generators/newshound/install/templates/newshound.rb +38 -0
- data/lib/newshound/configuration.rb +8 -1
- data/lib/newshound/daily_report_job.rb +25 -13
- data/lib/newshound/exception_reporter.rb +18 -8
- data/lib/newshound/que_reporter.rb +20 -10
- data/lib/newshound/scheduler.rb +10 -9
- data/lib/newshound/slack_notifier.rb +26 -43
- data/lib/newshound/transport/base.rb +28 -0
- data/lib/newshound/transport/slack.rb +67 -0
- data/lib/newshound/transport/sns.rb +115 -0
- data/lib/newshound/version.rb +1 -1
- data/newshound.gemspec +39 -0
- metadata +11 -58
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5541adbc8e4ccb311bcce9a185946affc52cddc3d6c253779722a4901627401
|
4
|
+
data.tar.gz: 06c86ac9f83a00ce6e6e77063131a29eaf2fb7e6643528e65e6f0b3c329d1309
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d3e875cb30e1e3c27cd932f5f66fa8f9a6269d7e03c7cbb5428ce5ba47dd53acacb80721d5f51452b0d97a3d46a7005e93fec24c9954a96452ee861df6c962a
|
7
|
+
data.tar.gz: a5041975aab52ff2c50527f9f5050014d086908923b7f15830dce201f43a234a9086b3ba2d7747de90797ef07af0d769badbe431bd92105abd8b9e254f6b4499
|
data/.rspec
ADDED
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
|
-
|
8
|
-
gem "
|
9
|
-
gem "
|
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
|
-
- 💬
|
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
|
-
###
|
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
|
|
data/TRANSPORT_USAGE.md
ADDED
@@ -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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
28
|
-
|
29
|
-
|
30
|
-
.where("created_at >= ?",
|
35
|
+
return [] unless exception_source
|
36
|
+
|
37
|
+
exception_source
|
38
|
+
.where("created_at >= ?", time_range.ago)
|
31
39
|
.order(created_at: :desc)
|
32
|
-
.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
|
-
|
73
|
+
return 0 unless exception_source
|
74
|
+
|
75
|
+
exception_source
|
66
76
|
.where(exception_class: exception.exception_class)
|
67
|
-
.where("created_at >= ?",
|
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
|
37
|
-
|
38
|
-
|
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
|
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:
|
83
|
-
scheduled:
|
84
|
-
failed:
|
85
|
-
finished_today:
|
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
|
-
|
98
|
+
logger.error "Failed to fetch Que statistics: #{e.message}"
|
89
99
|
default_stats
|
90
100
|
end
|
91
101
|
|
data/lib/newshound/scheduler.rb
CHANGED
@@ -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
|
-
#
|
12
|
-
# This
|
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
|
-
#
|
23
|
-
if defined?(
|
24
|
-
|
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
|
-
|
8
|
-
|
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
|
13
|
-
|
14
|
-
|
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
|
-
|
20
|
+
logger.error "Newshound: Failed to send notification: #{e.message}"
|
23
21
|
end
|
24
22
|
|
25
23
|
private
|
26
24
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
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
|
data/lib/newshound/version.rb
CHANGED
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.
|
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:
|
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
|