hubble 0.1.0

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.
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
+