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 +4 -0
- data/Gemfile +4 -0
- data/README.md +15 -0
- data/Rakefile +1 -0
- data/hubble.gemspec +24 -0
- data/lib/hubble/backend/haystack.rb +45 -0
- data/lib/hubble/backend/memory.rb +22 -0
- data/lib/hubble/config.yml +21 -0
- data/lib/hubble/middleware.rb +34 -0
- data/lib/hubble/version.rb +3 -0
- data/lib/hubble.rb +309 -0
- metadata +108 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
|
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
|
+
|