kuende-opbeat 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +36 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +205 -0
  7. data/Makefile +3 -0
  8. data/README.md +267 -0
  9. data/Rakefile +19 -0
  10. data/gemfiles/rails30.gemfile +9 -0
  11. data/gemfiles/rails31.gemfile +9 -0
  12. data/gemfiles/rails32.gemfile +9 -0
  13. data/gemfiles/rails40.gemfile +9 -0
  14. data/gemfiles/rails41.gemfile +9 -0
  15. data/gemfiles/rails42.gemfile +9 -0
  16. data/gemfiles/ruby192_rails31.gemfile +10 -0
  17. data/gemfiles/ruby192_rails32.gemfile +10 -0
  18. data/gemfiles/sidekiq31.gemfile +11 -0
  19. data/lib/opbeat.rb +132 -0
  20. data/lib/opbeat/better_attr_accessor.rb +44 -0
  21. data/lib/opbeat/capistrano.rb +9 -0
  22. data/lib/opbeat/capistrano/capistrano2.rb +47 -0
  23. data/lib/opbeat/capistrano/capistrano3.rb +26 -0
  24. data/lib/opbeat/client.rb +122 -0
  25. data/lib/opbeat/configuration.rb +90 -0
  26. data/lib/opbeat/error.rb +6 -0
  27. data/lib/opbeat/event.rb +223 -0
  28. data/lib/opbeat/filter.rb +63 -0
  29. data/lib/opbeat/integrations/delayed_job.rb +32 -0
  30. data/lib/opbeat/integrations/resque.rb +22 -0
  31. data/lib/opbeat/integrations/sidekiq.rb +32 -0
  32. data/lib/opbeat/interfaces.rb +35 -0
  33. data/lib/opbeat/interfaces/exception.rb +16 -0
  34. data/lib/opbeat/interfaces/http.rb +57 -0
  35. data/lib/opbeat/interfaces/message.rb +19 -0
  36. data/lib/opbeat/interfaces/stack_trace.rb +50 -0
  37. data/lib/opbeat/linecache.rb +25 -0
  38. data/lib/opbeat/logger.rb +21 -0
  39. data/lib/opbeat/rack.rb +44 -0
  40. data/lib/opbeat/rails/middleware/debug_exceptions_catcher.rb +22 -0
  41. data/lib/opbeat/railtie.rb +26 -0
  42. data/lib/opbeat/tasks.rb +24 -0
  43. data/lib/opbeat/version.rb +3 -0
  44. data/opbeat.gemspec +28 -0
  45. data/spec/opbeat/better_attr_accessor_spec.rb +99 -0
  46. data/spec/opbeat/client_spec.rb +35 -0
  47. data/spec/opbeat/configuration_spec.rb +50 -0
  48. data/spec/opbeat/event_spec.rb +138 -0
  49. data/spec/opbeat/filter_spec.rb +101 -0
  50. data/spec/opbeat/integrations/delayed_job_spec.rb +38 -0
  51. data/spec/opbeat/logger_spec.rb +55 -0
  52. data/spec/opbeat/opbeat_spec.rb +89 -0
  53. data/spec/opbeat/rack_spec.rb +116 -0
  54. data/spec/spec_helper.rb +22 -0
  55. metadata +218 -0
@@ -0,0 +1,90 @@
1
+ module Opbeat
2
+ class Configuration
3
+
4
+ # Base URL of the Opbeat server
5
+ attr_accessor :server
6
+
7
+ # Secret access token for authentication with Opbeat
8
+ attr_accessor :secret_token
9
+
10
+ # Organization ID to use with Opbeat
11
+ attr_accessor :organization_id
12
+
13
+ # App ID to use with Opbeat
14
+ attr_accessor :app_id
15
+
16
+ # Logger to use internally
17
+ attr_accessor :logger
18
+
19
+ # Number of lines of code context to capture, or nil for none
20
+ attr_accessor :context_lines
21
+
22
+ # Whitelist of environments that will send notifications to Opbeat
23
+ attr_accessor :environments
24
+
25
+ # Which exceptions should never be sent
26
+ attr_accessor :excluded_exceptions
27
+
28
+ # An array of parameters whould should be filtered from the log
29
+ attr_accessor :filter_parameters
30
+
31
+ # Timeout when waiting for the server to return data in seconds
32
+ attr_accessor :timeout
33
+
34
+ # Timout when opening connection to the server
35
+ attr_accessor :open_timeout
36
+
37
+ # Backoff multipler
38
+ attr_accessor :backoff_multiplier
39
+
40
+ # Should the SSL certificate of the server be verified?
41
+ attr_accessor :ssl_verification
42
+
43
+ attr_reader :current_environment
44
+
45
+ attr_accessor :user_controller_method
46
+
47
+ # Optional Proc to be used to send events asynchronously
48
+ attr_reader :async
49
+
50
+ def initialize
51
+ self.server = ENV['OPBEAT_SERVER'] || "https://intake.opbeat.com"
52
+ self.secret_token = ENV['OPBEAT_SECRET_TOKEN'] if ENV['OPBEAT_SECRET_TOKEN']
53
+ self.organization_id = ENV['OPBEAT_ORGANIZATION_ID'] if ENV['OPBEAT_ORGANIZATION_ID']
54
+ self.app_id = ENV['OPBEAT_APP_ID'] if ENV['OPBEAT_APP_ID']
55
+ @context_lines = 3
56
+ self.environments = %w[ development production default ]
57
+ self.current_environment = (defined?(::Rails) && ::Rails.env) || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'default'
58
+ self.excluded_exceptions = []
59
+ self.timeout = 1
60
+ self.open_timeout = 1
61
+ self.backoff_multiplier = 2
62
+ self.ssl_verification = true
63
+ self.user_controller_method = 'current_user'
64
+ self.async = false
65
+ end
66
+
67
+ # Allows config options to be read like a hash
68
+ #
69
+ # @param [Symbol] option Key for a given attribute
70
+ def [](option)
71
+ send(option)
72
+ end
73
+
74
+ def current_environment=(environment)
75
+ @current_environment = environment.to_s
76
+ end
77
+
78
+ def send_in_current_environment?
79
+ environments.include? current_environment
80
+ end
81
+
82
+ def async=(value)
83
+ raise ArgumentError.new("async must be callable (or false to disable)") unless (value == false || value.respond_to?(:call))
84
+ @async = value
85
+ end
86
+
87
+ alias_method :async?, :async
88
+
89
+ end
90
+ end
@@ -0,0 +1,6 @@
1
+ module Opbeat
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ end
@@ -0,0 +1,223 @@
1
+ require 'rubygems'
2
+ require 'socket'
3
+
4
+ require 'opbeat/error'
5
+ require 'opbeat/linecache'
6
+
7
+ module Opbeat
8
+
9
+ class Event
10
+
11
+ LOG_LEVELS = {
12
+ "debug" => "debug",
13
+ "info" => "info",
14
+ "warn" => "warn",
15
+ "warning" => "warn",
16
+ "error" => "error",
17
+ }
18
+
19
+ BACKTRACE_RE = /^(.+?):(\d+)(?::in `(.+?)')?$/
20
+
21
+ attr_reader :id
22
+ attr_accessor :organization, :app, :message, :timestamp, :level
23
+ attr_accessor :logger, :culprit, :hostname, :modules, :extra, :user
24
+ attr_accessor :environment
25
+
26
+ def initialize(options={}, configuration=nil, &block)
27
+ @configuration = configuration || Opbeat.configuration
28
+ @interfaces = {}
29
+
30
+ @id = options[:id]
31
+ @message = options[:message]
32
+ @timestamp = options[:timestamp] || Time.now.utc
33
+ @level = options[:level] || :error
34
+ @logger = options[:logger] || 'root'
35
+ @culprit = options[:culprit]
36
+ @environment = @configuration[:current_environment]
37
+ @extra = options[:extra]
38
+ @user = options[:user]
39
+
40
+ # Try to resolve the hostname to an FQDN, but fall back to whatever the load name is
41
+ hostname = Socket.gethostname
42
+ hostname = Socket.gethostbyname(hostname).first rescue hostname
43
+ @hostname = options[:hostname] || hostname
44
+
45
+ block.call(self) if block
46
+
47
+ # Some type coercion
48
+ @timestamp = @timestamp.strftime('%Y-%m-%dT%H:%M:%S') if @timestamp.is_a?(Time)
49
+ @level = LOG_LEVELS[@level.to_s.downcase] if @level.is_a?(String) || @level.is_a?(Symbol)
50
+
51
+ # Basic sanity checking
52
+ raise Error.new('A message is required for all events') unless @message && !@message.empty?
53
+ raise Error.new('A timestamp is required for all events') unless @timestamp
54
+ end
55
+
56
+ def interface(name, value=nil, &block)
57
+ int = Opbeat::find_interface(name)
58
+ Opbeat.logger.info "Unknown interface: #{name}" unless int
59
+ raise Error.new("Unknown interface: #{name}") unless int
60
+ @interfaces[int.name] = int.new(value, &block) if value || block
61
+ @interfaces[int.name]
62
+ end
63
+
64
+ def [](key)
65
+ interface(key)
66
+ end
67
+
68
+ def []=(key, value)
69
+ interface(key, value)
70
+ end
71
+
72
+ def to_hash
73
+ data = {
74
+ 'message' => self.message,
75
+ 'timestamp' => self.timestamp,
76
+ 'level' => self.level,
77
+ 'logger' => self.logger,
78
+ }
79
+ data['client_supplied_id'] = self.id if self.id
80
+ data['culprit'] = self.culprit if self.culprit
81
+ data['machine'] = {'hostname' => self.hostname } if self.hostname
82
+ data['environment'] = self.environment if self.environment # Future proofing: Environment is currently not used by the Opbeat API
83
+ data['extra'] = self.extra || {}
84
+ data['extra']['ruby'] = "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}" unless data['extra']['ruby']
85
+ data['extra']['rails'] = ::Rails::VERSION::STRING if defined?(::Rails::VERSION::STRING) && !data['extra']['rails']
86
+ if self.user
87
+ data['user'] = self.user
88
+ if self.user[:id] or self.user[:email] or self.user[:username]
89
+ data['user'][:is_authenticated] = true
90
+ end
91
+ data['user'][:is_authenticated] = false if !data['user'][:is_authenticated]
92
+ end
93
+ @interfaces.each_pair do |name, int_data|
94
+ data[name] = int_data.to_hash
95
+ end
96
+ data
97
+ end
98
+
99
+ def self.from_exception(exc, options={}, &block)
100
+ configuration = Opbeat.configuration
101
+ if exc.is_a?(Opbeat::Error)
102
+ # Try to prevent error reporting loops
103
+ Opbeat.logger.info "Refusing to capture Opbeat error: #{exc.inspect}"
104
+ return nil
105
+ end
106
+ if configuration[:excluded_exceptions].include? exc.class.name
107
+ Opbeat.logger.info "User excluded error: #{exc.inspect}"
108
+ return nil
109
+ end
110
+ options = self.merge_context(options)
111
+ self.new(options, configuration) do |evt|
112
+ evt.message = "#{exc.class.to_s}: #{exc.message}"
113
+ evt.level = :error
114
+ evt.parse_exception(exc)
115
+ evt.interface :stack_trace do |int|
116
+ int.frames = exc.backtrace.reverse.map do |trace_line|
117
+ int.frame {|frame| evt.parse_backtrace_line(trace_line, frame) }
118
+ end
119
+ evt.culprit = evt.get_culprit(int.frames)
120
+ end
121
+ block.call(evt) if block
122
+ end
123
+ end
124
+
125
+ def self.from_rack_exception(exc, rack_env, options={}, &block)
126
+ from_exception(exc, options) do |evt|
127
+ evt.interface :http do |int|
128
+ int.from_rack(rack_env)
129
+ end
130
+
131
+ if not evt.user
132
+ controller = rack_env["action_controller.instance"]
133
+ user_method_name = Opbeat.configuration.user_controller_method
134
+ if controller and controller.respond_to? user_method_name
135
+ user_obj = controller.send user_method_name
136
+ evt.from_user_object(user_obj)
137
+ end
138
+ end
139
+
140
+ block.call(evt) if block
141
+ end
142
+ end
143
+
144
+ def self.from_message(message, stack, options={})
145
+ configuration = Opbeat.configuration
146
+ options = self.merge_context(options)
147
+ self.new(options, configuration) do |evt|
148
+ evt.message = message
149
+ evt.level = :error
150
+ evt.interface :message do |int|
151
+ int.message = message
152
+ end
153
+ evt.interface :stack_trace do |int|
154
+ int.frames = stack.reverse.map do |trace_line|
155
+ int.frame {|frame| evt.parse_backtrace_line(trace_line, frame) }
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ def self.set_context(options={})
162
+ Thread.current["_opbeat_context"] = options
163
+ end
164
+
165
+ def get_culprit(frames)
166
+ lastframe = frames[-2]
167
+ "#{lastframe.filename} in #{lastframe.function}" if lastframe
168
+ end
169
+
170
+ def parse_exception(exception)
171
+ interface(:exception) do |int|
172
+ int.type = exception.class.to_s
173
+ int.value = exception.message
174
+ int.module = exception.class.to_s.split('::')[0...-1].join('::')
175
+ end
176
+ end
177
+
178
+ def parse_backtrace_line(line, frame)
179
+ md = BACKTRACE_RE.match(line)
180
+ raise Error.new("Unable to parse backtrace line: #{line.inspect}") unless md
181
+ frame.abs_path = md[1]
182
+ frame.lineno = md[2].to_i
183
+ frame.function = md[3] if md[3]
184
+ frame.filename = strip_load_path_from(frame.abs_path)
185
+ if context_lines = @configuration[:context_lines]
186
+ frame.pre_context, frame.context_line, frame.post_context = \
187
+ get_contextlines(frame.abs_path, frame.lineno, context_lines)
188
+ end
189
+ frame
190
+ end
191
+
192
+ def from_user_object(user_obj)
193
+ @user = {} if not @user
194
+ @user[:id] = user_obj.send(:id) rescue nil
195
+ @user[:email] = user_obj.send(:email) rescue nil
196
+ @user[:username] = user_obj.send(:username) rescue nil
197
+ end
198
+
199
+ private
200
+
201
+ def self.merge_context(options={})
202
+ context_options = Thread.current["_opbeat_context"] || {}
203
+ context_options.merge(options)
204
+ end
205
+
206
+
207
+ # Because linecache can go to hell
208
+ def self._source_lines(path, from, to)
209
+ end
210
+
211
+ def get_contextlines(path, line, context)
212
+ lines = (2 * context + 1).times.map do |i|
213
+ Opbeat::LineCache::getline(path, line - context + i)
214
+ end
215
+ [lines[0..(context-1)], lines[context], lines[(context+1)..-1]]
216
+ end
217
+
218
+ def strip_load_path_from(path)
219
+ prefix = $:.select {|s| path.start_with?(s.to_s)}.sort_by {|s| s.to_s.length}.last
220
+ prefix ? path[prefix.to_s.chomp(File::SEPARATOR).length+1..-1] : path
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,63 @@
1
+ module Opbeat
2
+ class Filter
3
+ MASK = '[FILTERED]'
4
+ DEFAULT_FILTER = [/(authorization|password|passwd|secret)/i]
5
+
6
+ def initialize(filters=nil)
7
+ if defined?(::Rails)
8
+ rails_filters = ::Rails.application.config.filter_parameters
9
+ rails_filters = nil if rails_filters.count == 0
10
+ end
11
+ @filters = filters || rails_filters || DEFAULT_FILTER
12
+ end
13
+
14
+ def apply(value, key=nil, &block)
15
+ if value.is_a?(Hash)
16
+ value.each.inject({}) do |memo, (k, v)|
17
+ memo[k] = apply(v, k, &block)
18
+ memo
19
+ end
20
+ elsif value.is_a?(Array)
21
+ value.map do |value|
22
+ apply(value, key, &block)
23
+ end
24
+ else
25
+ block.call(key, value)
26
+ end
27
+ end
28
+
29
+ def sanitize(key, value)
30
+ if !value.is_a?(String) || value.empty?
31
+ value
32
+ elsif @filters.any? { |filter| filter.is_a?(Regexp) ? filter.match(key) : filter.to_s == key.to_s }
33
+ MASK
34
+ else
35
+ value
36
+ end
37
+ end
38
+
39
+ def process_event_hash(data)
40
+ return data unless data.has_key? 'http'
41
+ if data['http'].has_key? 'data'
42
+ data['http']['data'] = process_hash(data['http']['data'])
43
+ end
44
+ if data['http'].has_key? 'query_string'
45
+ data['http']['query_string'] = process_string(data['http']['query_string'], '&')
46
+ end
47
+ if data['http'].has_key? 'cookies'
48
+ data['http']['cookies'] = process_string(data['http']['cookies'], ';')
49
+ end
50
+ data
51
+ end
52
+
53
+ def process_hash(data)
54
+ apply(data) do |key, value|
55
+ sanitize(key, value)
56
+ end
57
+ end
58
+
59
+ def process_string(str, separator='&')
60
+ str.split(separator).map { |s| s.split('=') }.map { |a| a[0]+'='+sanitize(a[0], a[1]) }.join(separator)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'delayed_job'
3
+ rescue LoadError
4
+ end
5
+
6
+ # Based on the Sentry equivalent.
7
+ if defined?(Delayed)
8
+
9
+ module Delayed
10
+ module Plugins
11
+ class Opbeat < ::Delayed::Plugin
12
+ callbacks do |lifecycle|
13
+ lifecycle.around(:invoke_job) do |job, *args, &block|
14
+ begin
15
+ # Forward the call to the next callback in the callback chain
16
+ block.call(job, *args)
17
+
18
+ rescue Exception => exception
19
+ # Log error to Opbeat
20
+ ::Opbeat.capture_exception(exception)
21
+ # Make sure we propagate the failure!
22
+ raise exception
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ # Register DelayedJob Opbeat plugin
31
+ Delayed::Worker.plugins << Delayed::Plugins::Opbeat
32
+ end
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'resque'
3
+ rescue LoadError
4
+ end
5
+
6
+ if defined?(Resque)
7
+
8
+ module Resque
9
+ module Failure
10
+ # Failure backend for Opbeat
11
+ class Opbeat < Base
12
+ # @override (see Resque::Failure::Base#save)
13
+ # @param (see Resque::Failure::Base#save)
14
+ # @return (see Resque::Failure::Base#save)
15
+ def save
16
+ ::Opbeat.capture_exception(exception)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'sidekiq'
3
+ rescue LoadError
4
+ end
5
+
6
+ if defined? Sidekiq
7
+ module Opbeat
8
+ module Integrations
9
+ class Sidekiq
10
+ def call(worker, msg, queue)
11
+ begin
12
+ yield
13
+ rescue Exception => ex
14
+ raise ex if [Interrupt, SystemExit, SignalException].include? ex.class
15
+ ::Opbeat.capture_exception(ex)
16
+ raise
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ ::Sidekiq.configure_server do |config|
24
+ if ::Sidekiq::VERSION < '3'
25
+ config.server_middleware do |chain|
26
+ chain.add ::Opbeat::Integrations::Sidekiq
27
+ end
28
+ else
29
+ config.error_handlers << Proc.new { |ex, ctx| ::Opbeat.capture_exception(ex) }
30
+ end
31
+ end
32
+ end