Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding GitHub Notifier #376

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion CHANGELOG.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
* Add normalize_subject option to remove numbers from email so that they thread (by @jjb)
* Allow the user to provide a custom message and hash of data (by @jjb)
* Add support for configurable background sections and a data partial (by @jeffrafter)
* Include timestamp of exception in notification body
* Include timestamp of exception in notification body
* Add support for rack based session management (by @phoet)
* Add ignore_crawlers option to ignore exceptions generated by crawlers
* Add verbode_subject option to exclude exception message from subject (by @amishyn)
Expand Down
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ ExceptionNotification relies on notifiers to deliver notifications when errors o
* [Slack notifier](#slack-notifier)
* [Mattermost notifier](#mattermost-notifier)
* [WebHook notifier](#webhook-notifier)
* [GitHub notifier](#github-notifier)

But, you also can easily implement your own [custom notifier](#custom-notifier).

Expand Down Expand Up @@ -773,6 +774,68 @@ Rails.application.config.middleware.use ExceptionNotification::Rack,

For more HTTParty options, check out the [documentation](https://github.com/jnunemaker/httparty).

### GitHub notifier

This notifier sends notifications, creating issues on GitHub.

#### Usage

Just add the [octokit](https://github.com/github/octokit) gem to your `Gemfile`:

```ruby
gem 'octokit'
```

To configure it, you need to set the `repo`, `login` and `password` options, like this:

```ruby
Rails.application.config.middleware.use ExceptionNotification::Rack,
:email => {
:email_prefix => "[PREFIX] ",
:sender_address => %{"notifier" <[email protected]>},
:exception_recipients => %w{[email protected]}
},
:github => {
:prefix => "[PREFIX] ",
:repo => 'owner/repo',
:login => ENV['GITHUB_LOGIN'],
:password => ENV['GITHUB_PASSWORD']
}
```

#### Options

##### repo

*String, required*

The repo owner and repo name, separated by a forward slash.

##### login

*String, required*

A GitHub username with access rights to the repo

##### password

*String, required*

The username's password.

##### prefix

*String, optional*

A prefix prepended to the issue title.

##### other options

Authentication using OAuth tokens are not (yet) supported.

Assignee, milestone and labels are not (yet) supported.


### Custom notifier

Simply put, notifiers are objects which respond to `#call(exception, options)` method. Thus, a lambda can be used as a notifier as follow:
Expand Down
1 change: 1 addition & 0 deletions exception_notification.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ Gem::Specification.new do |s|
s.add_development_dependency "hipchat", ">= 1.0.0"
s.add_development_dependency "carrier-pigeon", ">= 0.7.0"
s.add_development_dependency "slack-notifier", ">= 1.0.0"
s.add_development_dependency "octokit", ">= 4.3.0"
end
1 change: 1 addition & 0 deletions lib/exception_notifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module ExceptionNotifier
autoload :WebhookNotifier, 'exception_notifier/webhook_notifier'
autoload :IrcNotifier, 'exception_notifier/irc_notifier'
autoload :SlackNotifier, 'exception_notifier/slack_notifier'
autoload :GithubNotifier, 'exception_notifier/github_notifier'
autoload :MattermostNotifier, 'exception_notifier/mattermost_notifier'

class UndefinedNotifierError < StandardError; end
Expand Down
148 changes: 148 additions & 0 deletions lib/exception_notifier/github_notifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
require 'action_dispatch'
require 'pp'

module ExceptionNotifier
class GithubNotifier < BaseNotifier
attr_accessor :body, :client, :title, :repo

def initialize(options)
super
begin
@client = Octokit::Client.new(login: options.delete(:login),
password: options.delete(:password))
@repo = options.delete(:repo)
@prefix = options.delete(:prefix) || '[Error] '
end
end

def call(exception, options = {})
@exception = exception
@env = options[:env]
@data = (@env && @env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
unless @env.nil?
@kontroller = @env['action_controller.instance']
@request = ActionDispatch::Request.new(@env)
@request_hash = hash_from_request
@session = @request.session
@environment = @request.filtered_env
end
@title = compose_title
@body = compose_body
issue_options = { title: @title, body: @body }
send_notice(@exception, options, nil, issue_options) do |_msg, opts|
@client.create_issue(@repo, opts[:title], opts[:body]) if @client.basic_authenticated?
end
end

private

def compose_backtrace_section
return '' if @exception.backtrace.empty?
out = sub_title('Backtrace')
out << "<pre>#{@exception.backtrace.join("\n")}</pre>\n"
end

def compose_body
body = compose_header
if @env.nil?
body << compose_backtrace_section
else
body << compose_request_section
body << compose_session_section
body << compose_environment_section
body << compose_backtrace_section
end
body << compose_data_section
end

def compose_data_section
return '' if @data.empty?
out = sub_title('Data')
out << "<pre>#{PP.pp(@data, '')}</pre>"
end

def compose_environment_section
out = sub_title('Environment')
max = @environment.keys.map(&:to_s).max { |a, b| a.length <=> b.length }
out << "<pre>"
@environment.keys.map(&:to_s).sort.each do |key|
out << "* #{"%-*s: %s" % [max.length, key, inspect_object(@environment[key])]}\n"
end
out << "</pre>"
end

def compose_header
header = @exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'
header << format(" %s occurred", @exception.class.to_s)
if @kontroller
header << format(" in %s#%s",
@kontroller.controller_name,
@kontroller.action_name)
end
header << format(":\n\n")
header << "<pre>#{@exception.message}\n"
header << "#{@exception.backtrace.first}</pre>"
end

def compose_request_section
return '' if @request_hash.empty?
out = sub_title('Request')
out << "<pre>* URL : #{@request_hash[:url]}\n"
out << "* Referer : #{@request_hash[:referer]}\n"
out << "* HTTP Method: #{@request_hash[:http_method]}\n"
out << "* IP address : #{@request_hash[:ip_address]}\n"
out << "* Parameters : #{@request_hash[:parameters].inspect}\n"
out << "* Timestamp : #{@request_hash[:timestamp]}\n"
out << "* Server : #{Socket.gethostname}\n"
if defined?(Rails) && Rails.respond_to?(:root)
out << "* Rails root : #{Rails.root}\n"
end
out << "* Process : #{$$}</pre>"
end

def compose_session_section
out = sub_title('Session')
id = if @request.ssl?
'[FILTERED]'
else
rack_session_id = (@request.env["rack.session.options"] and @request.env["rack.session.options"][:id])
(@request.session['session_id'] || rack_session_id).inspect
end
out << format("<pre>* session id: %s\n", id)
out << "* data : #{PP.pp(@request.session.to_hash, '')}</pre>"
end

def compose_title
subject = "#{@prefix}"
if @kontroller
subject << "#{@kontroller.controller_name}##{@kontroller.action_name}"
end
subject << " (#{@exception.class.to_s})"
subject.length > 120 ? subject[0...120] + "..." : subject
end

def hash_from_request
{
referer: @request.referer,
http_method: @request.method,
ip_address: @request.remote_ip,
parameters: @request.filtered_parameters,
timestamp: Time.current,
url: @request.original_url
}
end

def inspect_object(object)
case object
when Hash, Array
object.inspect
else
object.to_s
end
end

def sub_title(text)
"\n\n-------------------- #{text} --------------------\n\n"
end
end
end
2 changes: 1 addition & 1 deletion test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

ActiveRecord::Schema.define(version: 20110729022608) do

create_table "posts", force: true do |t|
create_table "posts", force: :cascade do |t|
t.string "title"
t.text "body"
t.string "secret"
Expand Down
2 changes: 1 addition & 1 deletion test/exception_notifier/email_notifier_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class EmailNotifierTest < ActiveSupport::TestCase
ExceptionNotifier::EmailNotifier.normalize_digits('1 foo 12 bar 123 baz 1234')
end

test "mail should be plain text and UTF-8 enconded by default" do
test "mail should be plain text and UTF-8 encoded by default" do
assert_equal @mail.content_type, "text/plain; charset=UTF-8"
end

Expand Down
73 changes: 73 additions & 0 deletions test/exception_notifier/github_notifier_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
require 'test_helper'
require 'octokit'

class GithubNotifierTest < ActiveSupport::TestCase

test "should create github issue if properly configured" do
Octokit::Client.any_instance.expects(:create_issue)

options = {
:prefix => '[Prefix] ',
:repo => 'some/repo',
:login => 'login',
:password => 'password'
}

github = ExceptionNotifier::GithubNotifier.new(options)
github.call(fake_exception,
:env => { "REQUEST_METHOD" => "GET", "rack.input" => "" },
:data => {})
end

test "does not create an authenticated github client if badly configured" do
incomplete_options = {
:prefix => '[Prefix] ',
:repo => 'some/repo',
:login => nil,
:password => 'password'
}

github = ExceptionNotifier::GithubNotifier.new(incomplete_options)
github.call(fake_exception,
:env => { "REQUEST_METHOD" => "GET", "rack.input" => "" },
:data => {})

refute github.client.basic_authenticated?
end

test "github issue is formed with data" do
Octokit::Client.any_instance.expects(:create_issue)

options = {
:prefix => '[Prefix] ',
:repo => 'some/repo',
:login => 'login',
:password => 'password'
}

github = ExceptionNotifier::GithubNotifier.new(options)
github.call(fake_exception,
:env => { "REQUEST_METHOD" => "GET", "rack.input" => "" },
:data => {})

assert_includes github.title, '[Prefix] (ZeroDivisionError)'
assert_includes github.body, 'A ZeroDivisionError occurred:'
assert_includes github.body, 'divided by 0'
assert_includes github.body, '-------------------- Request --------------------'
assert_includes github.body, "* HTTP Method: GET"
assert_includes github.body, "-------------------- Session --------------------"
assert_includes github.body, "* session id: nil"
assert_includes github.body, "-------------------- Environment --------------------"
assert_includes github.body, "* REQUEST_METHOD : GET"
assert_includes github.body, "-------------------- Backtrace --------------------"
assert_includes github.body, "`fake_exception'"
end

private

def fake_exception
5/0
rescue Exception => e
e
end
end