appygram 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/appygram.gemspec +16 -0
- data/init.rb +39 -0
- data/lib/appygram/alert_data.rb +15 -0
- data/lib/appygram/application_environment.rb +63 -0
- data/lib/appygram/catcher.rb +34 -0
- data/lib/appygram/config.rb +87 -0
- data/lib/appygram/controller_exception_data.rb +65 -0
- data/lib/appygram/exception_data.rb +108 -0
- data/lib/appygram/integration/alerter.rb +11 -0
- data/lib/appygram/integration/debug_exceptions.rb +16 -0
- data/lib/appygram/integration/dj.rb +15 -0
- data/lib/appygram/integration/rack.rb +28 -0
- data/lib/appygram/integration/rack_rails.rb +26 -0
- data/lib/appygram/integration/rails.rb +27 -0
- data/lib/appygram/integration/sinatra.rb +6 -0
- data/lib/appygram/log_factory.rb +39 -0
- data/lib/appygram/monkeypatches.rb +10 -0
- data/lib/appygram/rack_exception_data.rb +29 -0
- data/lib/appygram/railtie.rb +23 -0
- data/lib/appygram/remote.rb +52 -0
- data/lib/appygram/startup.rb +14 -0
- data/lib/appygram/version.rb +3 -0
- data/lib/appygram.rb +71 -0
- data/rails/init.rb +1 -0
- metadata +82 -0
data/appygram.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/appygram/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = %q{appygram}
|
6
|
+
gem.version = Appygram::VERSION
|
7
|
+
gem.authors = ["rfc2616"]
|
8
|
+
gem.summary = %q{ appygram is a hosted service for sending messages from mobile/web apps }
|
9
|
+
gem.description = %q{Appygram is the Ruby gem for communicating with http://appygram.com (hosted messaging service). It supports an exception reporting mechanism similar to http://exceptional.io, and this Ruby gem, forked from the Exceptional gem, emulates Exceptional functionality.}
|
10
|
+
gem.email = %q{heittman.rob@gmail.com}
|
11
|
+
gem.files = Dir['lib/**/*'] + Dir['spec/**/*'] + Dir['spec/**/*'] + Dir['rails/**/*'] + Dir['tasks/**/*'] + Dir['*.rb'] + ["appygram.gemspec"]
|
12
|
+
gem.homepage = %q{http://appygram.com/}
|
13
|
+
gem.require_paths = ["lib"]
|
14
|
+
gem.requirements << "json_pure, json-jruby or json gem required"
|
15
|
+
gem.add_dependency 'rack'
|
16
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'appygram'
|
2
|
+
|
3
|
+
# If old plugin still installed then we don't want to install this one.
|
4
|
+
# In production environments we should continue to work as before, but in development/test we should
|
5
|
+
# advise how to correct the problem and exit
|
6
|
+
if (defined?(Appygram::VERSION::STRING) rescue nil) && %w(development test).include?(RAILS_ENV)
|
7
|
+
message = %Q(
|
8
|
+
***********************************************************************
|
9
|
+
You seem to still have an old version of the Appygram plugin installed.
|
10
|
+
Remove it from /vendor/plugins and try again.
|
11
|
+
***********************************************************************
|
12
|
+
)
|
13
|
+
puts message
|
14
|
+
exit -1
|
15
|
+
else
|
16
|
+
begin
|
17
|
+
|
18
|
+
if (Rails::VERSION::MAJOR < 3)
|
19
|
+
Appygram::Config.load(File.join(RAILS_ROOT, "/config/Appygram.yml"))
|
20
|
+
if Appygram::Config.should_send_to_api?
|
21
|
+
Appygram.logger.info("Loading Appygram #{Appygram::VERSION} for #{Rails::VERSION::STRING}")
|
22
|
+
require File.join('Appygram', 'integration', 'rails')
|
23
|
+
require File.join('Appygram', 'integration', 'dj') if defined?(Delayed::Job)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
Appygram::Config.load(File.join(Rails.root, "/config/Appygram.yml"))
|
27
|
+
|
28
|
+
if Appygram::Config.should_send_to_api?
|
29
|
+
Appygram.logger.info("Loading Appygram #{Appygram::VERSION} for #{Rails::VERSION::STRING}")
|
30
|
+
Rails.configuration.middleware.use "Rack::RailsAppygram"
|
31
|
+
require File.join('Appygram', 'integration', 'dj') if defined?(Delayed::Job)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
rescue => e
|
35
|
+
STDERR.puts "Problem starting Appygram Plugin. Your app will run as normal. #{e.message}"
|
36
|
+
Appygram.logger.error(e.message)
|
37
|
+
Appygram.logger.error(e.backtrace)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Appygram
|
2
|
+
class AlertData < ExceptionData
|
3
|
+
# Overwrite backtrace, since it is irrelevant
|
4
|
+
def extra_data
|
5
|
+
{
|
6
|
+
'exception' => {
|
7
|
+
'exception_class' => @exception.class.to_s,
|
8
|
+
'message' => @exception.message,
|
9
|
+
'backtrace' => "",
|
10
|
+
'occurred_at' => Time.now
|
11
|
+
}
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Appygram
|
4
|
+
class ApplicationEnvironment
|
5
|
+
def self.to_hash(framework)
|
6
|
+
{
|
7
|
+
'client' => {
|
8
|
+
'name' => Appygram::CLIENT_NAME,
|
9
|
+
'version' => Appygram::VERSION,
|
10
|
+
'protocol_version' => Appygram::PROTOCOL_VERSION
|
11
|
+
},
|
12
|
+
'application_environment' => {
|
13
|
+
'environment' => environment,
|
14
|
+
'env' => extract_environment(ENV),
|
15
|
+
'host' => get_hostname,
|
16
|
+
'run_as_user' => get_username,
|
17
|
+
'application_root_directory' => (application_root.to_s.respond_to?(:force_encoding) ? application_root.to_s.force_encoding("UTF-8") : application_root),
|
18
|
+
'language' => 'ruby',
|
19
|
+
'language_version' => language_version_string,
|
20
|
+
'framework' => framework,
|
21
|
+
'libraries_loaded' => libraries_loaded
|
22
|
+
}
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.environment
|
27
|
+
Config.application_environment
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.application_root
|
31
|
+
Config.application_root
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.extract_environment(env)
|
35
|
+
env.reject do |k, v|
|
36
|
+
(k =~ /^HTTP_/) || Appygram::ENVIRONMENT_FILTER.include?(k)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.get_hostname
|
41
|
+
require 'socket' unless defined?(Socket)
|
42
|
+
Socket.gethostname
|
43
|
+
rescue
|
44
|
+
'UNKNOWN'
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.language_version_string
|
48
|
+
"#{RUBY_VERSION rescue '?.?.?'} p#{RUBY_PATCHLEVEL rescue '???'} #{RUBY_RELEASE_DATE rescue '????-??-??'} #{RUBY_PLATFORM rescue '????'}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.get_username
|
52
|
+
ENV['LOGNAME'] || ENV['USER'] || ENV['USERNAME'] || ENV['APACHE_RUN_USER'] || 'UNKNOWN'
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.libraries_loaded
|
56
|
+
begin
|
57
|
+
return Hash[*Gem.loaded_specs.map{|name, gem_specification| [name, gem_specification.version.to_s]}.flatten]
|
58
|
+
rescue
|
59
|
+
end
|
60
|
+
{}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Appygram
|
2
|
+
class Catcher
|
3
|
+
class << self
|
4
|
+
def handle_with_controller(exception, controller=nil, request=nil)
|
5
|
+
if Config.should_send_to_api?
|
6
|
+
data = ControllerExceptionData.new(exception, controller, request)
|
7
|
+
Remote.error(data)
|
8
|
+
else
|
9
|
+
raise exception
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# unspeced
|
14
|
+
def handle_with_rack(exception, environment, request)
|
15
|
+
if Config.should_send_to_api?
|
16
|
+
data = RackExceptionData.new(exception, environment, request)
|
17
|
+
Remote.error(data)
|
18
|
+
else
|
19
|
+
raise exception
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# unspeced
|
24
|
+
def handle(exception, name=nil)
|
25
|
+
if Config.should_send_to_api?
|
26
|
+
data = ExceptionData.new(exception, name)
|
27
|
+
Remote.error(data)
|
28
|
+
else
|
29
|
+
raise exception
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Appygram
|
4
|
+
class Config
|
5
|
+
class ConfigurationException < StandardError; end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
DEFAULTS = {
|
9
|
+
:ssl => false,
|
10
|
+
:remote_host_http => 'api.appygram.com',
|
11
|
+
:http_open_timeout => 2,
|
12
|
+
:http_read_timeout => 4,
|
13
|
+
:disabled_by_default => %w(development test)
|
14
|
+
}
|
15
|
+
|
16
|
+
attr_accessor :api_key, :enabled
|
17
|
+
attr_accessor :http_proxy_host, :http_proxy_port, :http_proxy_username, :http_proxy_password
|
18
|
+
attr_writer :ssl
|
19
|
+
|
20
|
+
def load(config_file=nil)
|
21
|
+
if (config_file && File.file?(config_file))
|
22
|
+
begin
|
23
|
+
config = YAML::load_file(config_file)
|
24
|
+
env_config = config[application_environment] || {}
|
25
|
+
@api_key = config['api-key'] || env_config['api-key']
|
26
|
+
|
27
|
+
@http_proxy_host = config['http-proxy-host']
|
28
|
+
@http_proxy_port = config['http-proxy-port']
|
29
|
+
@http_proxy_username = config['http-proxy-username']
|
30
|
+
@http_proxy_password = config['http-proxy-password']
|
31
|
+
@http_open_timeout = config['http-open-timeout']
|
32
|
+
@http_read_timeout = config['http-read-timeout']
|
33
|
+
|
34
|
+
@ssl = config['ssl'] || env_config['ssl']
|
35
|
+
@enabled = env_config['enabled']
|
36
|
+
@remote_port = config['remote-port'].to_i unless config['remote-port'].nil?
|
37
|
+
@remote_host = config['remote-host'] unless config['remote-host'].nil?
|
38
|
+
rescue Exception => e
|
39
|
+
raise ConfigurationException.new("Unable to load configuration #{config_file} for environment #{application_environment} : #{e.message}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def api_key
|
45
|
+
return @api_key unless @api_key.nil?
|
46
|
+
@api_key ||= ENV['EXCEPTIONAL_API_KEY'] unless ENV['EXCEPTIONAL_API_KEY'].nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
def application_environment
|
50
|
+
ENV['RACK_ENV'] || ENV['RAILS_ENV']|| 'development'
|
51
|
+
end
|
52
|
+
|
53
|
+
def should_send_to_api?
|
54
|
+
return @enabled unless @enabled.nil?
|
55
|
+
@enabled = !(DEFAULTS[:disabled_by_default].include?(application_environment))
|
56
|
+
end
|
57
|
+
|
58
|
+
def application_root
|
59
|
+
(defined?(Rails) && Rails.respond_to?(:root)) ? Rails.root : Dir.pwd
|
60
|
+
end
|
61
|
+
|
62
|
+
def ssl?
|
63
|
+
@ssl ||= DEFAULTS[:ssl]
|
64
|
+
end
|
65
|
+
|
66
|
+
def remote_host
|
67
|
+
@remote_host ||= DEFAULTS[:remote_host_http]
|
68
|
+
end
|
69
|
+
|
70
|
+
def remote_port
|
71
|
+
@remote_port ||= ssl? ? 443 : 80
|
72
|
+
end
|
73
|
+
|
74
|
+
def reset
|
75
|
+
@enabled = @ssl = @remote_host = @remote_port = @api_key = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def http_open_timeout
|
79
|
+
@http_open_timeout ||= DEFAULTS[:http_open_timeout]
|
80
|
+
end
|
81
|
+
|
82
|
+
def http_read_timeout
|
83
|
+
@http_read_timeout ||= DEFAULTS[:http_read_timeout]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Appygram
|
4
|
+
class ControllerExceptionData < ExceptionData
|
5
|
+
def initialize(exception, controller=nil, request=nil)
|
6
|
+
super(exception)
|
7
|
+
@request = request
|
8
|
+
@controller = controller
|
9
|
+
end
|
10
|
+
|
11
|
+
def framework
|
12
|
+
"rails"
|
13
|
+
end
|
14
|
+
|
15
|
+
def extra_stuff
|
16
|
+
return {} if @request.nil?
|
17
|
+
{
|
18
|
+
'request' => {
|
19
|
+
'url' => (@request.respond_to?(:url) ? @request.url : "#{@request.protocol}#{@request.host}#{@request.request_uri}"),
|
20
|
+
'controller' => @controller.class.to_s,
|
21
|
+
'action' => (@request.respond_to?(:parameters) ? @request.parameters['action'] : @request.params['action']),
|
22
|
+
'parameters' => filter_parameters(@request.respond_to?(:parameters) ? @request.parameters : @request.params),
|
23
|
+
'request_method' => @request.request_method.to_s,
|
24
|
+
'remote_ip' => (@request.respond_to?(:remote_ip) ? @request.remote_ip : @request.ip),
|
25
|
+
'headers' => extract_http_headers(@request.env),
|
26
|
+
'session' => self.class.sanitize_session(@request)
|
27
|
+
}
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def filter_hash(keys_to_filter, hash)
|
32
|
+
keys_to_filter.map! {|x| x.to_s}
|
33
|
+
if keys_to_filter.is_a?(Array) && !keys_to_filter.empty?
|
34
|
+
hash.each do |key, value|
|
35
|
+
if key_match?(key, keys_to_filter)
|
36
|
+
hash[key] = "[FILTERED]"
|
37
|
+
elsif value.respond_to?(:to_hash)
|
38
|
+
filter_hash(keys_to_filter, hash[key])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
hash
|
43
|
+
end
|
44
|
+
|
45
|
+
# Closer alignment to latest filtered_params:
|
46
|
+
# https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/parameter_filter.rb
|
47
|
+
# https://github.com/exceptional/exceptional/issues/20
|
48
|
+
def key_match?(key, keys_to_filter)
|
49
|
+
keys_to_filter.any? { |k|
|
50
|
+
regexp = k.is_a?(Regexp)? k : Regexp.new(k, true)
|
51
|
+
key =~ regexp
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def filter_parameters(hash)
|
56
|
+
if @request.respond_to?(:env) && @request.env["action_dispatch.parameter_filter"]
|
57
|
+
filter_hash(@request.env["action_dispatch.parameter_filter"], hash)
|
58
|
+
elsif @controller.respond_to?(:filter_parameters)
|
59
|
+
@controller.send(:filter_parameters, hash)
|
60
|
+
else
|
61
|
+
hash
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Appygram
|
5
|
+
class ExceptionData
|
6
|
+
|
7
|
+
def initialize(exception, name=nil)
|
8
|
+
@exception = exception
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_hash
|
13
|
+
hash = ::Appygram::ApplicationEnvironment.to_hash(framework)
|
14
|
+
hash.merge!({
|
15
|
+
'exception' => {
|
16
|
+
'exception_class' => @exception.class.to_s,
|
17
|
+
'message' => @exception.message,
|
18
|
+
'backtrace' => @exception.backtrace,
|
19
|
+
'occurred_at' => Time.now.utc.iso8601
|
20
|
+
}
|
21
|
+
})
|
22
|
+
hash.merge!(extra_stuff)
|
23
|
+
hash.merge!(context_stuff)
|
24
|
+
self.class.sanitize_hash(hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
def extra_stuff
|
28
|
+
{ 'rescue_block' => { 'name' => @name} }
|
29
|
+
end
|
30
|
+
|
31
|
+
def context_stuff
|
32
|
+
context = Thread.current[:appygram_context]
|
33
|
+
(context.nil? || context.empty?) ? {} : {'context' => context}
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_json
|
37
|
+
begin
|
38
|
+
to_hash.to_json
|
39
|
+
rescue NoMethodError
|
40
|
+
begin
|
41
|
+
require 'json'
|
42
|
+
return to_hash.to_json
|
43
|
+
rescue StandardError => e
|
44
|
+
Appygram.logger.error(e.message)
|
45
|
+
Appygram.logger.error(e.backtrace)
|
46
|
+
raise StandardError.new("You need a json gem/library installed to send errors to Appygram (Object.to_json not defined). \nInstall json_pure, yajl-ruby, json-jruby, or the c-based json gem")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def framework
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def uniqueness_hash
|
56
|
+
return nil if (@exception.backtrace.nil? || @exception.backtrace.empty?)
|
57
|
+
Digest::MD5.hexdigest(@exception.backtrace.join)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.sanitize_hash(hash)
|
61
|
+
|
62
|
+
case hash
|
63
|
+
when Hash
|
64
|
+
hash.inject({}) do |result, (key, value)|
|
65
|
+
result.update(key => sanitize_hash(value))
|
66
|
+
end
|
67
|
+
when Array
|
68
|
+
hash.collect{|value| sanitize_hash(value)}
|
69
|
+
when Fixnum, String, Bignum
|
70
|
+
hash
|
71
|
+
else
|
72
|
+
hash.to_s
|
73
|
+
end
|
74
|
+
rescue Exception => e
|
75
|
+
Appygram.logger.error(hash)
|
76
|
+
Appygram.logger.error(e.message)
|
77
|
+
Appygram.logger.error(e.backtrace)
|
78
|
+
{}
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_http_headers(env)
|
82
|
+
headers = {}
|
83
|
+
env.select{|k, v| k =~ /^HTTP_/}.each do |name, value|
|
84
|
+
proper_name = name.sub(/^HTTP_/, '').split('_').map{|upper_case| upper_case.capitalize}.join('-')
|
85
|
+
headers[proper_name] = value
|
86
|
+
end
|
87
|
+
unless headers['Cookie'].nil?
|
88
|
+
headers['Cookie'] = headers['Cookie'].sub(/_session=\S+/, '_session=[FILTERED]')
|
89
|
+
end
|
90
|
+
headers
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.sanitize_session(request)
|
94
|
+
session_hash = {'session_id' => "", 'data' => {}}
|
95
|
+
|
96
|
+
if request.respond_to?(:session)
|
97
|
+
session = request.session
|
98
|
+
session_hash['session_id'] = request.session_options ? request.session_options[:id] : nil
|
99
|
+
session_hash['session_id'] ||= session.respond_to?(:session_id) ? session.session_id : session.instance_variable_get("@session_id")
|
100
|
+
session_hash['data'] = session.respond_to?(:to_hash) ? session.to_hash : session.instance_variable_get("@data") || {}
|
101
|
+
session_hash['session_id'] ||= session_hash['data'][:session_id]
|
102
|
+
session_hash['data'].delete(:session_id)
|
103
|
+
end
|
104
|
+
|
105
|
+
self.sanitize_hash(session_hash)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Appygram
|
2
|
+
module DebugExceptions
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.send(:alias_method_chain,:render_exception,:appygram)
|
6
|
+
end
|
7
|
+
|
8
|
+
def render_exception_with_appygram(env,exception)
|
9
|
+
::Appygram::Catcher.handle_with_controller(exception,
|
10
|
+
env['action_controller.instance'],
|
11
|
+
Rack::Request.new(env))
|
12
|
+
render_exception_without_appygram(env,exception)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
if Delayed::Worker.method_defined? :handle_failed_job
|
2
|
+
class Delayed::Worker
|
3
|
+
def handle_failed_job_with_appygram(job, e)
|
4
|
+
Appygram.handle(e, "Delayed::Job #{job.name}")
|
5
|
+
handle_failed_job_without_appygram(job, e)
|
6
|
+
Appygram.context.clear!
|
7
|
+
end
|
8
|
+
alias_method_chain :handle_failed_job, :appygram
|
9
|
+
Appygram.logger.info "DJ integration enabled"
|
10
|
+
end
|
11
|
+
else
|
12
|
+
message = "\n\n\nThe Appygram gem does not support Delayed Job 1.8.4 or earlier.\n\n\n"
|
13
|
+
STDERR.puts(message)
|
14
|
+
Appygram.logger.error(message)
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
class Appygram
|
6
|
+
|
7
|
+
def initialize(app, api_key = nil)
|
8
|
+
@app = app
|
9
|
+
if api_key.nil?
|
10
|
+
appygram_config = "config/appygram.yml"
|
11
|
+
::Appygram::Config.load(appygram_config)
|
12
|
+
else
|
13
|
+
::Appygram.configure(api_key)
|
14
|
+
::Appygram::Config.enabled = true
|
15
|
+
::Appygram.logger.info "Enabling Appygram for Rack"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
begin
|
21
|
+
status, headers, body = @app.call(env)
|
22
|
+
rescue Exception => e
|
23
|
+
::Appygram::Catcher.handle_with_rack(e,env, Rack::Request.new(env))
|
24
|
+
raise(e)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
class RailsAppygram
|
6
|
+
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
begin
|
13
|
+
body = @app.call(env)
|
14
|
+
rescue Exception => e
|
15
|
+
::Appygram::Catcher.handle_with_controller(e,env['action_controller.instance'], Rack::Request.new(env))
|
16
|
+
raise
|
17
|
+
end
|
18
|
+
|
19
|
+
if env['rack.exception']
|
20
|
+
::Appygram::Catcher.handle_with_controller(env['rack.exception'],env['action_controller.instance'], Rack::Request.new(env))
|
21
|
+
end
|
22
|
+
|
23
|
+
body
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# force Rails < 2.0 to use quote keys as per the JSON standard...
|
2
|
+
if defined?(ActiveSupport) && defined?(ActiveSupport::JSON) && ActiveSupport::JSON.respond_to?(:unquote_hash_key_identifiers)
|
3
|
+
ActiveSupport::JSON.unquote_hash_key_identifiers = false
|
4
|
+
end
|
5
|
+
|
6
|
+
if defined? ActionController
|
7
|
+
module ActionController
|
8
|
+
class Base
|
9
|
+
def rescue_action_with_appygram(exception)
|
10
|
+
unless exception_handled_by_rescue_from?(exception)
|
11
|
+
Appygram::Catcher.handle_with_controller(exception, self, request)
|
12
|
+
Appygram.context.clear!
|
13
|
+
end
|
14
|
+
rescue_action_without_appygram exception
|
15
|
+
end
|
16
|
+
|
17
|
+
alias_method :rescue_action_without_appygram, :rescue_action
|
18
|
+
alias_method :rescue_action, :rescue_action_with_appygram
|
19
|
+
protected :rescue_action
|
20
|
+
|
21
|
+
private
|
22
|
+
def exception_handled_by_rescue_from?(exception)
|
23
|
+
respond_to?(:handler_for_rescue) && handler_for_rescue(exception)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Appygram
|
4
|
+
class LogFactory
|
5
|
+
def self.logger
|
6
|
+
@logger ||= create_logger_with_fallback
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def self.create_logger_with_fallback
|
11
|
+
begin
|
12
|
+
log_dir = File.join(Config.application_root, 'log')
|
13
|
+
Dir.mkdir(log_dir) unless File.directory?(log_dir)
|
14
|
+
log_path = File.join(log_dir, "/appygram.log")
|
15
|
+
log = Logger.new(log_path)
|
16
|
+
log.level = Logger::INFO
|
17
|
+
def log.format_message(severity, timestamp, progname, msg)
|
18
|
+
"[#{severity.upcase}] (#{[Kernel.caller[2].split('/').last]}) #{timestamp.utc.to_s} - #{msg2str(msg).gsub(/\n/, '').lstrip}\n"
|
19
|
+
end
|
20
|
+
def log.msg2str(msg)
|
21
|
+
case msg
|
22
|
+
when ::String
|
23
|
+
msg
|
24
|
+
when ::Exception
|
25
|
+
"#{ msg.message } (#{ msg.class }): " <<
|
26
|
+
(msg.backtrace || []).join(" | ")
|
27
|
+
else
|
28
|
+
msg.inspect
|
29
|
+
end
|
30
|
+
end
|
31
|
+
log
|
32
|
+
rescue
|
33
|
+
return Rails.logger if defined?(Rails) && defined?(Rails.logger)
|
34
|
+
return RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)
|
35
|
+
return Logger.new(STDERR)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Appygram
|
4
|
+
class RackExceptionData < ExceptionData
|
5
|
+
def initialize(exception, environment, request)
|
6
|
+
super(exception)
|
7
|
+
@environment = environment
|
8
|
+
@request = request
|
9
|
+
end
|
10
|
+
|
11
|
+
def framework
|
12
|
+
"rack"
|
13
|
+
end
|
14
|
+
|
15
|
+
def extra_stuff
|
16
|
+
return {} if @request.nil?
|
17
|
+
{
|
18
|
+
'request' => {
|
19
|
+
'url' => "#{@request.url}",
|
20
|
+
'parameters' => @request.params,
|
21
|
+
'request_method' => @request.request_method.to_s,
|
22
|
+
'remote_ip' => @request.ip,
|
23
|
+
'headers' => extract_http_headers(@environment),
|
24
|
+
'session' => self.class.sanitize_session(@request)
|
25
|
+
}
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'appygram'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module Appygram
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
|
7
|
+
initializer "appygram.middleware" do |app|
|
8
|
+
|
9
|
+
config_file = File.join(Rails.root, "/config/appygram.yml")
|
10
|
+
Appygram::Config.load config_file if File.exist?(config_file)
|
11
|
+
# On Heroku config is loaded via the ENV so no need to load it from the file
|
12
|
+
|
13
|
+
if Appygram::Config.should_send_to_api?
|
14
|
+
Appygram.logger.info("Loading Appygram #{Appygram::VERSION} for #{Rails::VERSION::STRING}")
|
15
|
+
if defined?(ActionDispatch::DebugExceptions)
|
16
|
+
ActionDispatch::DebugExceptions.send(:include,Appygram::DebugExceptions)
|
17
|
+
else
|
18
|
+
app.config.middleware.use "Rack::RailsAppygram"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'cgi'
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/https'
|
5
|
+
require 'digest/md5'
|
6
|
+
|
7
|
+
module Appygram
|
8
|
+
class Remote
|
9
|
+
class << self
|
10
|
+
def startup_announce(startup_data)
|
11
|
+
url = "/api/announcements?api_key=#{::Appygram::Config.api_key}&protocol_version=#{::Appygram::PROTOCOL_VERSION}"
|
12
|
+
compressed = Zlib::Deflate.deflate(startup_data.to_json, Zlib::BEST_SPEED)
|
13
|
+
call_remote(url, compressed)
|
14
|
+
end
|
15
|
+
|
16
|
+
def error(exception_data)
|
17
|
+
uniqueness_hash = exception_data.uniqueness_hash
|
18
|
+
hash_param = uniqueness_hash.nil? ? nil: "&hash=#{uniqueness_hash}"
|
19
|
+
url = "/api/errors?api_key=#{::Appygram::Config.api_key}&protocol_version=#{::Appygram::PROTOCOL_VERSION}#{hash_param}"
|
20
|
+
compressed = Zlib::Deflate.deflate(exception_data.to_json, Zlib::BEST_SPEED)
|
21
|
+
call_remote(url, compressed)
|
22
|
+
end
|
23
|
+
|
24
|
+
def call_remote(url, data)
|
25
|
+
config = Appygram::Config
|
26
|
+
optional_proxy = Net::HTTP::Proxy(config.http_proxy_host,
|
27
|
+
config.http_proxy_port,
|
28
|
+
config.http_proxy_username,
|
29
|
+
config.http_proxy_password)
|
30
|
+
client = optional_proxy.new(config.remote_host, config.remote_port)
|
31
|
+
client.open_timeout = config.http_open_timeout
|
32
|
+
client.read_timeout = config.http_read_timeout
|
33
|
+
client.use_ssl = config.ssl?
|
34
|
+
client.verify_mode = OpenSSL::SSL::VERIFY_NONE if config.ssl?
|
35
|
+
begin
|
36
|
+
response = client.post(url, data)
|
37
|
+
case response
|
38
|
+
when Net::HTTPSuccess
|
39
|
+
Appygram.logger.info( "#{url} - #{response.message}")
|
40
|
+
return true
|
41
|
+
else
|
42
|
+
Appygram.logger.error("#{url} - #{response.code} - #{response.message}")
|
43
|
+
end
|
44
|
+
rescue Exception => e
|
45
|
+
Appygram.logger.error('Problem notifying Appygram about the error')
|
46
|
+
Appygram.logger.error(e)
|
47
|
+
end
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Appygram
|
2
|
+
class StartupException < StandardError;
|
3
|
+
end
|
4
|
+
class Startup
|
5
|
+
class << self
|
6
|
+
def announce
|
7
|
+
if Config.api_key.blank?
|
8
|
+
raise StartupException, 'API Key must be configured (/config/appygram.yml)'
|
9
|
+
end
|
10
|
+
Remote.startup_announce(::Appygram::ApplicationEnvironment.to_hash('rails'))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/appygram.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'appygram/monkeypatches'
|
4
|
+
require 'appygram/catcher'
|
5
|
+
require 'appygram/startup'
|
6
|
+
require 'appygram/log_factory'
|
7
|
+
require 'appygram/config'
|
8
|
+
require 'appygram/application_environment'
|
9
|
+
require 'appygram/exception_data'
|
10
|
+
require 'appygram/controller_exception_data'
|
11
|
+
require 'appygram/rack_exception_data'
|
12
|
+
require 'appygram/alert_data'
|
13
|
+
require 'appygram/remote'
|
14
|
+
require 'appygram/integration/rack'
|
15
|
+
require 'appygram/integration/rack_rails'
|
16
|
+
require 'appygram/integration/alerter'
|
17
|
+
require 'appygram/version'
|
18
|
+
require 'appygram/integration/debug_exceptions'
|
19
|
+
|
20
|
+
require 'appygram/railtie' if defined?(Rails::Railtie)
|
21
|
+
|
22
|
+
module Appygram
|
23
|
+
PROTOCOL_VERSION = 5
|
24
|
+
CLIENT_NAME = 'getappygram-gem'
|
25
|
+
ENVIRONMENT_FILTER = []
|
26
|
+
|
27
|
+
def self.logger
|
28
|
+
::Appygram::LogFactory.logger
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.configure(api_key)
|
32
|
+
Appygram::Config.api_key = api_key
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.handle(exception, name=nil)
|
36
|
+
Appygram::Catcher.handle(exception, name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.rescue(name=nil, context=nil, &block)
|
40
|
+
begin
|
41
|
+
self.context(context) unless context.nil?
|
42
|
+
block.call
|
43
|
+
rescue Exception => e
|
44
|
+
Appygram::Catcher.handle(e,name)
|
45
|
+
ensure
|
46
|
+
self.clear!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.rescue_and_reraise(name=nil, context=nil, &block)
|
51
|
+
begin
|
52
|
+
self.context(context) unless context.nil?
|
53
|
+
block.call
|
54
|
+
rescue Exception => e
|
55
|
+
Appygram::Catcher.handle(e,name)
|
56
|
+
raise(e)
|
57
|
+
ensure
|
58
|
+
self.clear!
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.clear!
|
63
|
+
Thread.current[:appygram_context] = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.context(hash = {})
|
67
|
+
Thread.current[:appygram_context] ||= {}
|
68
|
+
Thread.current[:appygram_context].merge!(hash)
|
69
|
+
self
|
70
|
+
end
|
71
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__) , '../init.rb')
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: appygram
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- rfc2616
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: &70157985912000 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70157985912000
|
25
|
+
description: Appygram is the Ruby gem for communicating with http://appygram.com (hosted
|
26
|
+
messaging service). It supports an exception reporting mechanism similar to http://exceptional.io,
|
27
|
+
and this Ruby gem, forked from the Exceptional gem, emulates Exceptional functionality.
|
28
|
+
email: heittman.rob@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/appygram/alert_data.rb
|
34
|
+
- lib/appygram/application_environment.rb
|
35
|
+
- lib/appygram/catcher.rb
|
36
|
+
- lib/appygram/config.rb
|
37
|
+
- lib/appygram/controller_exception_data.rb
|
38
|
+
- lib/appygram/exception_data.rb
|
39
|
+
- lib/appygram/integration/alerter.rb
|
40
|
+
- lib/appygram/integration/debug_exceptions.rb
|
41
|
+
- lib/appygram/integration/dj.rb
|
42
|
+
- lib/appygram/integration/rack.rb
|
43
|
+
- lib/appygram/integration/rack_rails.rb
|
44
|
+
- lib/appygram/integration/rails.rb
|
45
|
+
- lib/appygram/integration/sinatra.rb
|
46
|
+
- lib/appygram/log_factory.rb
|
47
|
+
- lib/appygram/monkeypatches.rb
|
48
|
+
- lib/appygram/rack_exception_data.rb
|
49
|
+
- lib/appygram/railtie.rb
|
50
|
+
- lib/appygram/remote.rb
|
51
|
+
- lib/appygram/startup.rb
|
52
|
+
- lib/appygram/version.rb
|
53
|
+
- lib/appygram.rb
|
54
|
+
- rails/init.rb
|
55
|
+
- init.rb
|
56
|
+
- appygram.gemspec
|
57
|
+
homepage: http://appygram.com/
|
58
|
+
licenses: []
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements:
|
76
|
+
- json_pure, json-jruby or json gem required
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 1.8.17
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: appygram is a hosted service for sending messages from mobile/web apps
|
82
|
+
test_files: []
|