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 ADDED
@@ -0,0 +1,5 @@
1
+
2
+ Release 0.1.0 - 2011/08/21
3
+
4
+ * First release
5
+
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
+
@@ -0,0 +1,7 @@
1
+ module TreasureData
2
+ module Logger
3
+
4
+ VERSION = '0.1.0'
5
+
6
+ end
7
+ end
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
+