td-logger 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/ChangeLog +5 -0
- data/README.rdoc +60 -0
- data/lib/td/logger/tdlog.rb +187 -0
- data/lib/td/logger/version.rb +7 -0
- data/lib/td/logger.rb +266 -0
- metadata +119 -0
data/README.rdoc
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
= Treasure Data logging library for Rails
|
2
|
+
|
3
|
+
== Getting Started
|
4
|
+
|
5
|
+
Add the following line to your Gemfile:
|
6
|
+
|
7
|
+
gem 'td-logger'
|
8
|
+
|
9
|
+
For Rails 2.x (not tested) without Bundler,
|
10
|
+
edit +environment.rb+ and add to the initalizer block:
|
11
|
+
|
12
|
+
config.gem "td-logger"
|
13
|
+
|
14
|
+
And then add +config/treasure_data.yml+ file as following:
|
15
|
+
|
16
|
+
# logging to Treasure Data directry
|
17
|
+
development:
|
18
|
+
apikey: "YOUR_API_KEY"
|
19
|
+
database: myapp
|
20
|
+
auto_create_table: true
|
21
|
+
|
22
|
+
# logging via td-agent
|
23
|
+
production:
|
24
|
+
agent: "localhost:24224"
|
25
|
+
tag: myapp
|
26
|
+
|
27
|
+
# disable logging
|
28
|
+
test:
|
29
|
+
|
30
|
+
=== Logging actions
|
31
|
+
|
32
|
+
Add 'add_td_tracer' line to your controller classes:
|
33
|
+
|
34
|
+
class YourController < ApplicationController
|
35
|
+
def myaction
|
36
|
+
# ...
|
37
|
+
end
|
38
|
+
add_td_tracer :myaction, 'mytable'
|
39
|
+
end
|
40
|
+
|
41
|
+
It logs {"controller":"your","action":"myaction"}. Additionally, routing parameters are included automatically.
|
42
|
+
|
43
|
+
You can add environment variables by adding 'extra' option as follows:
|
44
|
+
|
45
|
+
# logs client address on the 'addr' column
|
46
|
+
add_td_tracer :myaction, 'mytable', :extra=>{:REMOTE_ADDR=>:addr}
|
47
|
+
|
48
|
+
# logs referer and path info
|
49
|
+
add_td_tracer :myaction, 'mytable', :extra=>{:HTTP_REFERER=>:referer, :PATH_INFO=>:path}
|
50
|
+
|
51
|
+
Add 'static' option to add constant key-value pairs:
|
52
|
+
|
53
|
+
add_td_tracer :myaction, 'mytable', :static=>{:version=>1}
|
54
|
+
|
55
|
+
|
56
|
+
== Copyright
|
57
|
+
|
58
|
+
Copyright:: Copyright (c) 2011 Treasure Data Inc.
|
59
|
+
License:: Apache License, Version 2.0
|
60
|
+
|
@@ -0,0 +1,187 @@
|
|
1
|
+
|
2
|
+
module TreasureData
|
3
|
+
module Logger
|
4
|
+
|
5
|
+
# TODO shutdown handler
|
6
|
+
|
7
|
+
class TreasureDataLogger < Fluent::Logger::LoggerBase
|
8
|
+
def initialize(apikey, tag, auto_create_table)
|
9
|
+
require 'thread'
|
10
|
+
require 'stringio'
|
11
|
+
require 'zlib'
|
12
|
+
require 'msgpack'
|
13
|
+
require 'time'
|
14
|
+
require 'net/http'
|
15
|
+
require 'cgi'
|
16
|
+
require 'logger'
|
17
|
+
|
18
|
+
@tag = tag
|
19
|
+
@auto_create_table = auto_create_table
|
20
|
+
@logger = ::Logger.new(STDOUT)
|
21
|
+
|
22
|
+
# TODO
|
23
|
+
gem 'td'
|
24
|
+
require 'td/client'
|
25
|
+
@client = TreasureData::Client.new(apikey)
|
26
|
+
|
27
|
+
@mutex = Mutex.new
|
28
|
+
@cond = ConditionVariable.new
|
29
|
+
@map = {} # (db,table) => buffer:String
|
30
|
+
@queue = []
|
31
|
+
|
32
|
+
@chunk_limit = 8*1024*1024
|
33
|
+
@flush_interval = 60
|
34
|
+
@retry_wait = 1.0
|
35
|
+
@retry_limit = 8
|
36
|
+
|
37
|
+
@finish = false
|
38
|
+
@next_time = Time.now.to_i + @flush_interval
|
39
|
+
@error_count = 0
|
40
|
+
@upload_thread = Thread.new(&method(:upload_main))
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_accessor :logger
|
44
|
+
|
45
|
+
def close
|
46
|
+
@finish = true
|
47
|
+
@mutex.synchronize {
|
48
|
+
@flush_now = true
|
49
|
+
@cond.signal
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def post(tag, record)
|
54
|
+
tag = "#{@tag}.#{tag}"
|
55
|
+
db, table = tag.split('.')[-2, 2]
|
56
|
+
|
57
|
+
record['time'] ||= Time.now.to_i
|
58
|
+
|
59
|
+
key = [db, table]
|
60
|
+
@mutex.synchronize do
|
61
|
+
buffer = (@map[key] ||= '')
|
62
|
+
record.to_msgpack(buffer)
|
63
|
+
|
64
|
+
if buffer.size > @chunk_limit
|
65
|
+
@queue << [db, table, buffer]
|
66
|
+
@map.delete(key)
|
67
|
+
@cond.signal
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def upload_main
|
75
|
+
@mutex.lock
|
76
|
+
until @finish
|
77
|
+
now = Time.now.to_i
|
78
|
+
|
79
|
+
if @next_time <= now || (@flush_now && @error_count == 0)
|
80
|
+
@mutex.unlock
|
81
|
+
begin
|
82
|
+
try_flush
|
83
|
+
ensure
|
84
|
+
@mutex.lock
|
85
|
+
end
|
86
|
+
@flush_now = false
|
87
|
+
end
|
88
|
+
|
89
|
+
if @error_count == 0
|
90
|
+
next_wait = @flush_interval
|
91
|
+
else
|
92
|
+
next_wait = @retry_wait * (2 ** (@error_count-1))
|
93
|
+
end
|
94
|
+
@next_time = next_wait + now
|
95
|
+
|
96
|
+
cond_wait(next_wait)
|
97
|
+
end
|
98
|
+
|
99
|
+
rescue
|
100
|
+
@logger.error "Unexpected error: #{$!}"
|
101
|
+
$!.backtrace.each {|bt|
|
102
|
+
@logger.info bt
|
103
|
+
}
|
104
|
+
ensure
|
105
|
+
@mutex.unlock
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
def try_flush
|
110
|
+
@mutex.synchronize do
|
111
|
+
if @queue.empty?
|
112
|
+
@map.reject! {|(db,table),buffer|
|
113
|
+
@queue << [db, table, buffer]
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
until @queue.empty?
|
119
|
+
tuple = @queue.first
|
120
|
+
|
121
|
+
begin
|
122
|
+
upload(*tuple)
|
123
|
+
@queue.shift
|
124
|
+
@error_count = 0
|
125
|
+
rescue
|
126
|
+
if @error_count < @retry_limit
|
127
|
+
@logger.error "Failed to upload logs to Treasure Data, retrying: #{$!}"
|
128
|
+
@error_count += 1
|
129
|
+
else
|
130
|
+
@logger.error "Failed to upload logs to Treasure Data, trashed: #{$!}"
|
131
|
+
$!.backtrace.each {|bt|
|
132
|
+
@logger.info bt
|
133
|
+
}
|
134
|
+
@error_count = 0
|
135
|
+
@queue.clear
|
136
|
+
end
|
137
|
+
return
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def upload(db, table, buffer)
|
143
|
+
out = StringIO.new
|
144
|
+
Zlib::GzipWriter.wrap(out) {|gz| gz.write buffer }
|
145
|
+
stream = StringIO.new(out.string)
|
146
|
+
|
147
|
+
begin
|
148
|
+
@client.import(db, table, "msgpack.gz", stream, stream.size)
|
149
|
+
rescue TreasureData::NotFoundError
|
150
|
+
unless @auto_create_table
|
151
|
+
raise $!
|
152
|
+
end
|
153
|
+
@logger.info "Creating table #{db}.#{table} on TreasureData"
|
154
|
+
begin
|
155
|
+
@client.create_log_table(db, table)
|
156
|
+
rescue TreasureData::NotFoundError
|
157
|
+
@client.create_database(db)
|
158
|
+
@client.create_log_table(db, table)
|
159
|
+
end
|
160
|
+
retry
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def e(s)
|
165
|
+
CGI.escape(s.to_s)
|
166
|
+
end
|
167
|
+
|
168
|
+
if ConditionVariable.new.method(:wait).arity == 1
|
169
|
+
#$log.warn "WARNING: Running on Ruby 1.8. Ruby 1.9 is recommended."
|
170
|
+
require 'timeout'
|
171
|
+
def cond_wait(sec)
|
172
|
+
Timeout.timeout(sec) {
|
173
|
+
@cond.wait(@mutex)
|
174
|
+
}
|
175
|
+
rescue Timeout::Error
|
176
|
+
end
|
177
|
+
else
|
178
|
+
def cond_wait(sec)
|
179
|
+
@cond.wait(@mutex, sec)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
data/lib/td/logger.rb
ADDED
@@ -0,0 +1,266 @@
|
|
1
|
+
|
2
|
+
module TreasureData
|
3
|
+
module Logger
|
4
|
+
|
5
|
+
|
6
|
+
class Config
|
7
|
+
def initialize(conf)
|
8
|
+
if agent = conf['agent']
|
9
|
+
host, port = agent.split(':',2)
|
10
|
+
port = (port || 24224).to_i
|
11
|
+
@agent_host = host
|
12
|
+
@agent_port = port
|
13
|
+
|
14
|
+
@tag = conf['tag']
|
15
|
+
@tag ||= conf['database']
|
16
|
+
raise "'tag' nor 'database' options are not set" unless @tag
|
17
|
+
|
18
|
+
else
|
19
|
+
@apikey = conf['apikey']
|
20
|
+
raise "'apikey' option is not set" unless @apikey
|
21
|
+
|
22
|
+
@database = conf['database']
|
23
|
+
raise "'database' option is not set" unless @database
|
24
|
+
|
25
|
+
@auto_create_table = !!conf['auto_create_table']
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :agent_host, :agent_port, :tag
|
30
|
+
attr_reader :apikey, :database, :auto_create_table
|
31
|
+
|
32
|
+
def agent_mode?
|
33
|
+
@agent_host != nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
module TreasureData
|
43
|
+
|
44
|
+
|
45
|
+
def self.log(tag, record)
|
46
|
+
record['time'] ||= Time.now.to_i
|
47
|
+
Fluent::Logger.post(tag, record)
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
module Logger
|
52
|
+
module RailsAgent
|
53
|
+
|
54
|
+
CONFIG_SAMPLE = <<EOF
|
55
|
+
defaults: &defaults
|
56
|
+
apikey: "YOUR_API_KEY"
|
57
|
+
database: myapp
|
58
|
+
table: access
|
59
|
+
|
60
|
+
test:
|
61
|
+
<<: *defaults
|
62
|
+
|
63
|
+
development:
|
64
|
+
<<: *defaults
|
65
|
+
|
66
|
+
production:
|
67
|
+
<<: *defaults
|
68
|
+
EOF
|
69
|
+
|
70
|
+
CONFIG_PATH = 'config/treasure_data.yml'
|
71
|
+
|
72
|
+
def self.init(rails)
|
73
|
+
c = read_config(rails)
|
74
|
+
return unless c
|
75
|
+
|
76
|
+
require 'fluent/logger'
|
77
|
+
if c.agent_mode?
|
78
|
+
Fluent::Logger::FluentLogger.open(c.tag, c.agent_host, c.agent_port)
|
79
|
+
else
|
80
|
+
require 'td/logger/tdlog'
|
81
|
+
TreasureDataLogger.open(c.apikey, c.database, c.auto_create_table)
|
82
|
+
end
|
83
|
+
|
84
|
+
rails.middleware.use Middleware
|
85
|
+
ActionController::Base.class_eval do
|
86
|
+
include ::TreasureData::Logger::RailsAgent::ControllerLogger
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.read_config(rails)
|
91
|
+
logger = Rails.logger || ::Logger.new(STDOUT)
|
92
|
+
begin
|
93
|
+
yaml = YAML.load_file("#{RAILS_ROOT}/#{CONFIG_PATH}")
|
94
|
+
rescue
|
95
|
+
logger.warn "Can't load #{CONFIG_PATH} file."
|
96
|
+
logger.warn " #{$!}"
|
97
|
+
logger.warn "Put the following file:"
|
98
|
+
logger.warn sample
|
99
|
+
return
|
100
|
+
end
|
101
|
+
|
102
|
+
conf = yaml[RAILS_ENV]
|
103
|
+
unless conf
|
104
|
+
logger.warn "#{CONFIG_PATH} doesn't include setting for current environment (#{RAILS_ENV})."
|
105
|
+
logger.warn "Disabling Treasure Data logger."
|
106
|
+
return
|
107
|
+
end
|
108
|
+
|
109
|
+
begin
|
110
|
+
return Config.new(conf)
|
111
|
+
rescue
|
112
|
+
logger.warn "#{CONFIG_PATH}: #{$!}."
|
113
|
+
logger.warn "Disabling Treasure Data logger."
|
114
|
+
return
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class Middleware
|
119
|
+
def initialize(app, options={})
|
120
|
+
@app = app
|
121
|
+
end
|
122
|
+
|
123
|
+
def call(env)
|
124
|
+
r = @app.call(env)
|
125
|
+
|
126
|
+
if m = env['treasure_data.log_method']
|
127
|
+
m.call(env)
|
128
|
+
end
|
129
|
+
|
130
|
+
r
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.set_log_method(env, method)
|
134
|
+
env['treasure_data.log_method'] = method
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
module ControllerLogger
|
139
|
+
def self.included(mod)
|
140
|
+
mod.extend(ModuleMethods)
|
141
|
+
end
|
142
|
+
|
143
|
+
class ActionLogger
|
144
|
+
PARAM_KEY = if defined? Rails
|
145
|
+
if Rails.respond_to?(:version) && Rails.version =~ /^3/
|
146
|
+
# Rails 3
|
147
|
+
'action_dispatch.request.path_parameters'
|
148
|
+
else
|
149
|
+
# Rails 2
|
150
|
+
'action_controller.request.path_parameters'
|
151
|
+
end
|
152
|
+
else
|
153
|
+
# Rack default
|
154
|
+
'rack.routing_args'
|
155
|
+
end
|
156
|
+
|
157
|
+
def initialize(method, tag, options)
|
158
|
+
@method = method
|
159
|
+
@tag = tag
|
160
|
+
|
161
|
+
@only = nil
|
162
|
+
@except = nil
|
163
|
+
@extra = nil
|
164
|
+
@static = {}
|
165
|
+
|
166
|
+
if o = options[:only_params]
|
167
|
+
@only = case o
|
168
|
+
when Array
|
169
|
+
o
|
170
|
+
else
|
171
|
+
[o]
|
172
|
+
end.map {|e| e.to_s }
|
173
|
+
end
|
174
|
+
|
175
|
+
if o = options[:except_params]
|
176
|
+
@except = case o
|
177
|
+
when Array
|
178
|
+
o
|
179
|
+
else
|
180
|
+
[o]
|
181
|
+
end.map {|e| e.to_s }
|
182
|
+
end
|
183
|
+
|
184
|
+
if o = options[:extra]
|
185
|
+
@extra = case o
|
186
|
+
when Hash
|
187
|
+
m = {}
|
188
|
+
o.each_pair {|k,v| m[k.to_s] = v.to_s }
|
189
|
+
m
|
190
|
+
when Array
|
191
|
+
o.map {|e|
|
192
|
+
case e
|
193
|
+
when Hash
|
194
|
+
m = {}
|
195
|
+
e.each_pair {|k,v| m[k.to_s] = v.to_s }
|
196
|
+
m
|
197
|
+
else
|
198
|
+
{e.to_s => e.to_s}
|
199
|
+
end
|
200
|
+
}.inject({}) {|r,e| r.merge!(e) }
|
201
|
+
else
|
202
|
+
{o.to_s => o.to_s}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
if o = options[:static]
|
207
|
+
o.each_pair {|k,v| @static[k] = v }
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def call(env)
|
212
|
+
m = env[PARAM_KEY].dup || {}
|
213
|
+
|
214
|
+
if @only
|
215
|
+
m.reject! {|k,v| !@only.include?(k) }
|
216
|
+
end
|
217
|
+
if @except
|
218
|
+
m.reject! {|k,v| @except.include?(k) }
|
219
|
+
end
|
220
|
+
if @extra
|
221
|
+
@extra.each_pair {|k,v| m[v] = env[k] }
|
222
|
+
end
|
223
|
+
|
224
|
+
m.merge!(@static)
|
225
|
+
|
226
|
+
::TreasureData.log(@tag, m)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
module ModuleMethods
|
231
|
+
def add_td_tracer(method, tag, options={})
|
232
|
+
al = ActionLogger.new(method, tag, options)
|
233
|
+
module_eval <<-EOF
|
234
|
+
def #{method}_td_action_tracer_(*args, &block)
|
235
|
+
::TreasureData::Logger::RailsAgent::Middleware.set_log_method(request.env, method(:#{method}_td_action_trace_))
|
236
|
+
#{method}_td_action_tracer_orig_(*args, &block)
|
237
|
+
end
|
238
|
+
EOF
|
239
|
+
module_eval do
|
240
|
+
define_method(:"#{method}_td_action_trace_", &al.method(:call))
|
241
|
+
end
|
242
|
+
alias_method "#{method}_td_action_tracer_orig_", method
|
243
|
+
alias_method method, "#{method}_td_action_tracer_"
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
|
253
|
+
if defined? Rails
|
254
|
+
if Rails.respond_to?(:version) && Rails.version =~ /^3/
|
255
|
+
module TreasureData
|
256
|
+
class Railtie < Rails::Railtie
|
257
|
+
initializer "treasure_data_agent.start_plugin" do |app|
|
258
|
+
TreasureData::Logger::RailsAgent.init(app.config)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
else
|
263
|
+
TreasureData::Logger::RailsAgent.init(Rails.configuration)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: td-logger
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Sadayuki Furuhashi
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-08-21 00:00:00 +09:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: msgpack
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 7
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 4
|
33
|
+
- 4
|
34
|
+
version: 0.4.4
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: td
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 9
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
- 7
|
49
|
+
- 5
|
50
|
+
version: 0.7.5
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: fluent-logger
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 19
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 3
|
65
|
+
- 0
|
66
|
+
version: 0.3.0
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
executables: []
|
72
|
+
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files:
|
76
|
+
- ChangeLog
|
77
|
+
- README.rdoc
|
78
|
+
files:
|
79
|
+
- lib/td/logger.rb
|
80
|
+
- lib/td/logger/tdlog.rb
|
81
|
+
- lib/td/logger/version.rb
|
82
|
+
- ChangeLog
|
83
|
+
- README.rdoc
|
84
|
+
has_rdoc: true
|
85
|
+
homepage:
|
86
|
+
licenses: []
|
87
|
+
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options:
|
90
|
+
- --charset=UTF-8
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
hash: 3
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
hash: 3
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
version: "0"
|
111
|
+
requirements: []
|
112
|
+
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 1.3.7
|
115
|
+
signing_key:
|
116
|
+
specification_version: 3
|
117
|
+
summary: Treasure Data command line tool
|
118
|
+
test_files: []
|
119
|
+
|