hubble 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in hubble-client.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,15 @@
1
+ hubble
2
+ ======
3
+
4
+ You need a few environmental variables set.
5
+
6
+ * `export HUBBLE_ENV="production`"
7
+ * `export HUBBLE_USER="<myuser>"`
8
+ * `export HUBBLE_PASSWORD="<mypassword>"`
9
+ * `export HUBBLE_ENDPOINT="http://my-haystack.herokuapp.com/async"`
10
+
11
+ Test posting from a console trivially:
12
+
13
+ $ bundle exec irb
14
+ irb(main):001:0> require "hubble"; Hubble.setup; Hubble.boomtown!
15
+ => #<Net::HTTPOK 200 OK readbody=true>
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/hubble.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "hubble/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "hubble"
7
+ s.email = ["github.com"]
8
+ s.version = Hubble::VERSION
9
+ s.authors = ["GitHub Inc."]
10
+ s.homepage = "https://github.com/github/hubble"
11
+ s.summary = "Ruby client that posts to Haystack"
12
+ s.description = "A simple ruby client for posting exceptions to Haystack"
13
+
14
+ s.rubyforge_project = "hubble"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "yajl-ruby", "~> 1.1"
22
+
23
+ s.add_development_dependency "rake", "~>0.8.7"
24
+ end
@@ -0,0 +1,45 @@
1
+ module Hubble::Backend
2
+ class Haystack
3
+ def initialize(url)
4
+ @url = URI.parse(url)
5
+ end
6
+
7
+ def report(data)
8
+ send_data(data)
9
+ end
10
+
11
+ def reports
12
+ []
13
+ end
14
+
15
+ def user
16
+ ENV["HUBBLE_USER"] || "hubble"
17
+ end
18
+
19
+ def password
20
+ ENV["HUBBLE_PASSWORD"] || "unknown"
21
+ end
22
+
23
+ def password?
24
+ password != "unknown"
25
+ end
26
+
27
+ def send_data(data)
28
+ # make a post
29
+
30
+ post = Net::HTTP::Post.new(@url.path)
31
+ post.set_form_data('json' => Yajl.dump(data))
32
+
33
+ post.basic_auth(user, password) if password?
34
+
35
+ # make request
36
+ req = Net::HTTP.new(@url.host, @url.port)
37
+
38
+ # use SSL if applicable
39
+ req.use_ssl = true if @url.scheme == "https"
40
+
41
+ # push it through
42
+ req.start { |http| http.request(post) }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,22 @@
1
+ module Hubble::Client
2
+ class MemoryBackend
3
+ def initialize
4
+ @reports = []
5
+ @fail = false
6
+ end
7
+
8
+ attr_accessor :reports
9
+
10
+ def fail!
11
+ @fail = true
12
+ end
13
+
14
+ def report(data)
15
+ if @fail
16
+ fail
17
+ end
18
+
19
+ @reports << data
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ development:
2
+ backend: memory
3
+ host: localhost
4
+ port: 6666
5
+ haystack: http://localhost:9393/async
6
+ workers: 0
7
+ raise_errors: true
8
+
9
+ test:
10
+ backend: memory
11
+ host: localhost
12
+ port: 6666
13
+ haystack: http://localhost:9393/async
14
+ workers: 0
15
+ raise_errors: true
16
+
17
+ production:
18
+ backend: haystack
19
+ workers: 1
20
+ raise_errors: false
21
+
@@ -0,0 +1,34 @@
1
+ module Hubble
2
+ # Rack middleware that rescues exceptions raised from the downstream app and
3
+ # reports to Hubble::Client. The exception is reraised after being sent to
4
+ # hubble so upstream middleware can still display an error page or
5
+ # whathaveyou.
6
+ class Rescuer
7
+ def initialize(app, other={})
8
+ @app = app
9
+ @other = other
10
+ end
11
+
12
+ def call(env)
13
+ start = Time.now
14
+ @app.call(env)
15
+ rescue Object => boom
16
+ elapsed = Time.now - start
17
+ self.class.report(boom, env, @other.merge(:time => elapsed.to_s))
18
+ raise
19
+ end
20
+
21
+ def self.report(boom, env, other={})
22
+ request = Rack::Request.new(env)
23
+ Hubble::Client.report(boom, other.merge({
24
+ :method => request.request_method,
25
+ :user_agent => env['HTTP_USER_AGENT'],
26
+ :params => (request.params.inspect rescue nil),
27
+ :session => (request.session.inspect rescue nil),
28
+ :referrer => request.referrer,
29
+ :remote_ip => request.ip,
30
+ :url => request.url
31
+ }))
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Hubble
2
+ VERSION = "0.1.0"
3
+ end
data/lib/hubble.rb ADDED
@@ -0,0 +1,309 @@
1
+ require 'yaml'
2
+ require 'yajl'
3
+ require 'net/https'
4
+ require 'digest/md5'
5
+ require 'logger'
6
+ require 'socket'
7
+
8
+ require 'hubble/version'
9
+
10
+ module Hubble
11
+ # Backends for local testing or hitting a Haystack Endpoint
12
+ module Backend
13
+ autoload :Memory, 'hubble/backend/memory'
14
+ autoload :Haystack, 'hubble/backend/haystack'
15
+ end
16
+
17
+ # Reset the backend and optionally override the environment configuration.
18
+ #
19
+ # config - The optional configuration Hash.
20
+ #
21
+ # Returns nothing.
22
+ def setup(_config={})
23
+ config.merge!(_config)
24
+ @backend = nil
25
+ @raise_errors = nil
26
+ end
27
+
28
+ # Hash of configuration data from lib/hubble/config.yml.
29
+ def config
30
+ @config ||= YAML.load_file(config_file)[environment]
31
+ end
32
+
33
+ # Location of config.yml config file.
34
+ def config_file
35
+ File.expand_path('../hubble/config.yml', __FILE__)
36
+ end
37
+
38
+ # The current "environment". This dictates which section will be read
39
+ # from the config.yml config file.
40
+ def environment
41
+ @environment ||= ENV['HUBBLE_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
42
+ end
43
+
44
+ # The name of the backend that should be used to post exceptions to the
45
+ # exceptions-collection service. The fellowing backends are available:
46
+ #
47
+ # memory - Dummy backend that simply save exceptions in memory. Typically
48
+ # used in testing environments.
49
+ #
50
+ # heroku - In-process posting for outside of vpn apps
51
+ #
52
+ # Returns the String backend name. See also `Hubble.backend`.
53
+ def backend_name
54
+ config['backend']
55
+ end
56
+
57
+ # Determines whether exceptions are raised instead of being reported to
58
+ # the exception tracking service. This is typically enabled in development
59
+ # and test environments. When set true, no exception information is reported
60
+ # and the exception is raised instead. When false (default in production
61
+ # environments), the exception is reported to the exception tracking service
62
+ # but not raised.
63
+ def raise_errors?
64
+ if @raise_errors.nil?
65
+ config['raise_errors']
66
+ else
67
+ @raise_errors
68
+ end
69
+ end
70
+ attr_writer :raise_errors
71
+
72
+ # The URL where exceptions should be posted. Each exception is converted into
73
+ # JSON and posted to this URL.
74
+ def haystack
75
+ ENV['HUBBLE_ENDPOINT'] || config['haystack']
76
+ end
77
+
78
+ # Stack of context information to include in the next Hubble report. These
79
+ # hashes are condensed down into one and included in the next report. Don't
80
+ # mess with this structure directly - use the #push and #pop methods.
81
+ def context
82
+ @context ||= [{'server' => hostname, 'type' => 'exception'}]
83
+ end
84
+
85
+ # Add info to be sent in the next Hubble report, should one occur.
86
+ #
87
+ # info - Hash of name => value pairs to include in the exception report.
88
+ # block - When given, the info is removed from the current context after the
89
+ # block is executed.
90
+ #
91
+ # Returns the value returned by the block when given; otherwise, returns nil.
92
+ def push(info={})
93
+ context.push(info)
94
+ yield if block_given?
95
+ ensure
96
+ pop if block_given?
97
+ end
98
+
99
+ # Remove the last info hash from the context stack.
100
+ def pop
101
+ context.pop if context.size > 1
102
+ end
103
+
104
+ # Reset the context stack to a pristine state.
105
+ def reset!
106
+ @context = [context[0]]
107
+ end
108
+
109
+ # Public: Sends an exception to the exception tracking service along
110
+ # with a hash of custom attributes to be included with the report. When the
111
+ # raise_errors option is set, this method raises the exception instead of
112
+ # reporting to the exception tracking service.
113
+ #
114
+ # e - The Exception object. Must respond to #message and #backtrace.
115
+ # other - Hash of additional attributes to include with the report.
116
+ #
117
+ # Examples
118
+ #
119
+ # begin
120
+ # my_code
121
+ # rescue => e
122
+ # Hubble.report(e, :user => current_user)
123
+ # end
124
+ #
125
+ # Returns nothing.
126
+ def report(e, other = {})
127
+ if raise_errors?
128
+ squash_context(exception_info(e), other) # surface problems squashing
129
+ raise e
130
+ else
131
+ report!(e, other)
132
+ end
133
+ end
134
+
135
+ def report!(e, other = {})
136
+ data = squash_context(exception_info(e), other)
137
+ cast(data)
138
+ rescue Object => i
139
+ # don't fail for any reason
140
+ logger.debug "Hubble: #{data.inspect}" rescue nil
141
+ logger.debug e.message rescue nil
142
+ logger.debug e.backtrace.join("\n") rescue nil
143
+ logger.debug i.message rescue nil
144
+ logger.debug i.backtrace.join("\n") rescue nil
145
+ end
146
+
147
+ # Public: exceptions that were reported. Only available when using the
148
+ # memory and file backends.
149
+ #
150
+ # Returns an Array of exceptions data Hash.
151
+ def reports
152
+ backend.reports
153
+ end
154
+
155
+ # Combines all context hashes into a single hash converting non-standard
156
+ # data types in values to strings, then combines the result with a custom
157
+ # info hash provided in the other argument.
158
+ #
159
+ # other - Optional array of hashes to also squash in on top of the context
160
+ # stack hashes.
161
+ #
162
+ # Returns a Hash with all keys and values.
163
+ def squash_context(*other)
164
+ merged = {}
165
+ (context + other).each do |hash|
166
+ hash.each do |key, value|
167
+ value = (value.call rescue nil) if value.kind_of?(Proc)
168
+ merged[key.to_s] =
169
+ case value
170
+ when String, Numeric, true, false
171
+ value.to_s
172
+ else
173
+ value.inspect
174
+ end
175
+ end
176
+ end
177
+ merged
178
+ end
179
+
180
+ # Extract exception info into a simple Hash.
181
+ #
182
+ # e - The exception object to turn into a Hash.
183
+ #
184
+ # Returns a Hash including a 'class', 'message', 'backtrace', and 'rollup'
185
+ # keys. The rollup value is a MD5 hash of the exception class, file, and line
186
+ # number and is used to group exceptions.
187
+ def exception_info(e)
188
+ backtrace = Array(e.backtrace)[0, 500]
189
+
190
+ res = {
191
+ 'class' => e.class.to_s,
192
+ 'message' => e.message,
193
+ 'backtrace' => backtrace.join("\n"),
194
+ 'rollup' => Digest::MD5.hexdigest("#{e.class}#{backtrace[0]}")
195
+ }
196
+
197
+ if original = (e.respond_to?(:original_exception) && e.original_exception)
198
+ remote_backtrace = []
199
+ remote_backtrace << original.message
200
+ if original.backtrace
201
+ remote_backtrace.concat(Array(original.backtrace)[0,500])
202
+ end
203
+ res['remote_backtrace'] = remote_backtrace.join("\n")
204
+ end
205
+
206
+ res
207
+ end
208
+
209
+ # Send the exception data to the relay service using
210
+ # a non-waiting cast call.
211
+ #
212
+ # data - Hash of string key => string value pairs.
213
+ #
214
+ # Returns nothing.
215
+ def cast(data)
216
+ backend.report(data)
217
+ end
218
+
219
+ # Load and initialize the exception reporting backend as specified by
220
+ # the 'backend' configuration option.
221
+ #
222
+ # Raises ArgumentError for invalid backends.
223
+ def backend
224
+ @backend ||= backend!
225
+ end
226
+ attr_writer :backend
227
+
228
+ def backend!
229
+ case backend_name
230
+ when 'memory'
231
+ Hubble::Backend::Memory.new
232
+ when 'haystack'
233
+ Hubble::Backend::Haystack.new(haystack)
234
+ else
235
+ raise ArgumentError, "Unknown backend: #{backend_name.inspect}"
236
+ end
237
+ end
238
+
239
+ # Installs an at_exit hook to report exceptions that raise all the way out of
240
+ # the stack and halt the interpreter. This is useful for catching boot time
241
+ # errors as well and even signal kills.
242
+ #
243
+ # To use, call this method very early during the program's boot to cover as
244
+ # much code as possible:
245
+ #
246
+ # require 'hubble'
247
+ # Hubble.install_unhandled_exception_hook!
248
+ #
249
+ # Returns true when the hook was installed, nil when the hook had previously
250
+ # been installed by another component.
251
+ def install_unhandled_exception_hook!
252
+ # only install the hook once, even when called from multiple locations
253
+ return if @unhandled_exception_hook_installed
254
+
255
+ # the $! is set when the interpreter is exiting due to an exception
256
+ at_exit do
257
+ boom = $!
258
+ if boom && !raise_errors? && !boom.is_a?(SystemExit)
259
+ report(boom, 'argv' => ([$0]+ARGV).join(" "), 'halting' => true)
260
+ end
261
+ end
262
+
263
+ @unhandled_exception_hook_installed = true
264
+ end
265
+
266
+ def logger
267
+ @logger ||= Logger.new($stderr)
268
+ end
269
+
270
+ def logger=(logger)
271
+ @logger = logger
272
+ end
273
+
274
+ def hostname
275
+ @hostname ||= Socket.gethostname
276
+ end
277
+
278
+ # Public: Trigger an Exception
279
+ #
280
+ # Returns nothing.
281
+ def boomtown!
282
+ e = ArgumentError.new("BOOMTOWN")
283
+ report(e)
284
+ end
285
+
286
+ extend self
287
+
288
+ ##
289
+ # Deprecated
290
+
291
+ def service
292
+ warn "Hubble.service is deprecated. #{caller[0]}"
293
+ @service ||= BERTRPC::Service.new(config['host'], config['port'])
294
+ end
295
+
296
+ alias svc service
297
+
298
+ def fail
299
+ raise "failure failure!"
300
+ end
301
+
302
+ def default_options
303
+ context[0]
304
+ end
305
+
306
+ def default_options=(hash)
307
+ context[0] = hash
308
+ end
309
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hubble
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - GitHub Inc.
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-01-04 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: yajl-ruby
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 13
30
+ segments:
31
+ - 1
32
+ - 1
33
+ version: "1.1"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 49
45
+ segments:
46
+ - 0
47
+ - 8
48
+ - 7
49
+ version: 0.8.7
50
+ type: :development
51
+ version_requirements: *id002
52
+ description: A simple ruby client for posting exceptions to Haystack
53
+ email:
54
+ - github.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files: []
60
+
61
+ files:
62
+ - .gitignore
63
+ - Gemfile
64
+ - README.md
65
+ - Rakefile
66
+ - hubble.gemspec
67
+ - lib/hubble.rb
68
+ - lib/hubble/backend/haystack.rb
69
+ - lib/hubble/backend/memory.rb
70
+ - lib/hubble/config.yml
71
+ - lib/hubble/middleware.rb
72
+ - lib/hubble/version.rb
73
+ has_rdoc: true
74
+ homepage: https://github.com/github/hubble
75
+ licenses: []
76
+
77
+ post_install_message:
78
+ rdoc_options: []
79
+
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ hash: 3
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ hash: 3
97
+ segments:
98
+ - 0
99
+ version: "0"
100
+ requirements: []
101
+
102
+ rubyforge_project: hubble
103
+ rubygems_version: 1.6.2
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Ruby client that posts to Haystack
107
+ test_files: []
108
+