PageRenderTime 48ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/td/logger/td_logger.rb

https://gitlab.com/vectorci/td-logger-ruby
Ruby | 388 lines | 306 code | 65 blank | 17 comment | 22 complexity | c71af847ab2d230489643f162f89911e MD5 | raw file
  1. module TreasureData
  2. module Logger
  3. class TreasureDataLogger < Fluent::Logger::LoggerBase
  4. def initialize(tag_prefix, options={})
  5. defaults = {
  6. :auto_create_table => false,
  7. }
  8. options = defaults.merge!(options)
  9. @tag_prefix = tag_prefix
  10. @auto_create_table = !!options[:auto_create_table]
  11. apikey = options[:apikey]
  12. unless apikey
  13. raise ArgumentError, ":apikey options is required"
  14. end
  15. debug = !!options[:debug]
  16. require 'thread'
  17. require 'stringio'
  18. require 'zlib'
  19. require 'msgpack'
  20. require 'json'
  21. require 'time'
  22. require 'net/http'
  23. require 'cgi'
  24. require 'logger'
  25. require 'td-client'
  26. @logger = ::Logger.new(STDERR)
  27. if debug
  28. @logger.level = ::Logger::DEBUG
  29. else
  30. @logger.level = ::Logger::INFO
  31. end
  32. # translate :use_ssl to :ssl for backwards compatibility
  33. options[:ssl] = options[:use_ssl] unless options[:use_ssl].nil?
  34. @client = TreasureData::Client.new(apikey, options)
  35. @mutex = Mutex.new
  36. @cond = ConditionVariable.new
  37. @map = {} # (db,table) => buffer:String
  38. @queue = []
  39. @chunk_limit = options[:chunk_limit] || 8 * 1024 * 1024
  40. @queue_limit = 50
  41. @flush_interval = options[:flush_interval] || 2
  42. @max_flush_interval = options[:max_flush_interval] || 300
  43. @retry_wait = 1.0
  44. @retry_limit = 12
  45. @finish = false
  46. @next_time = Time.now.to_i + @flush_interval
  47. @error_count = 0
  48. # start thread when the first post() is called for
  49. # Unicorn and Passenger.
  50. @upload_thread = nil
  51. @use_unique_key = options[:use_unique_key]
  52. # The calling order of finalizer registered by define_finalizer is indeterminate,
  53. # so we should use at_exit instead for memory safety.
  54. at_exit { close }
  55. end
  56. attr_accessor :logger
  57. def close
  58. unless @finish
  59. @finish = true
  60. @mutex.synchronize {
  61. @flush_now = true
  62. @cond.signal
  63. }
  64. @upload_thread.join if @upload_thread
  65. @queue.reject! {|db,table,data|
  66. begin
  67. upload(db, table, data)
  68. true
  69. rescue
  70. @logger.error "Failed to upload event logs to Treasure Data, trashed: #{$!}"
  71. false
  72. end
  73. }
  74. @map.reject! {|(db,table),buffer|
  75. data = buffer.flush!
  76. begin
  77. upload(db, table, data)
  78. true
  79. rescue
  80. @logger.error "Failed to upload event logs to Treasure Data, trashed: #{$!}"
  81. false
  82. end
  83. }
  84. end
  85. end
  86. def flush
  87. @mutex.lock
  88. # Move small chunks into queue to flush all events.
  89. # See try_flush routine for more detail
  90. @map.reject! {|(db,table),buffer|
  91. data = buffer.flush!
  92. @queue << [db, table, data]
  93. }
  94. try_flush
  95. rescue => e
  96. @logger.error "Unexpected error at flush: #{e}"
  97. e.backtrace.each {|bt|
  98. @logger.info bt
  99. }
  100. ensure
  101. @mutex.unlock
  102. end
  103. def post_with_time(tag, record, time)
  104. @logger.debug { "event: #{tag} #{record.to_json}" rescue nil }
  105. record[:time] ||= time.to_i
  106. tag = "#{@tag_prefix}.#{tag}" if @tag_prefix
  107. db, table = tag.split('.')[-2, 2]
  108. add(db, table, record)
  109. end
  110. def upload_main
  111. @mutex.lock
  112. until @finish
  113. now = Time.now.to_i
  114. if @next_time <= now || (@flush_now && @error_count == 0)
  115. flushed = try_flush
  116. @flush_now = false
  117. end
  118. if @error_count == 0
  119. if flushed && @flush_interval < @max_flush_interval
  120. @flush_interval = [@flush_interval ** 2, @max_flush_interval].min
  121. end
  122. next_wait = @flush_interval
  123. else
  124. next_wait = @retry_wait * (2 ** (@error_count-1))
  125. end
  126. @next_time = next_wait + now
  127. cond_wait(next_wait)
  128. end
  129. rescue
  130. @logger.error "Unexpected error: #{$!}"
  131. $!.backtrace.each {|bt|
  132. @logger.info bt
  133. }
  134. ensure
  135. @mutex.unlock
  136. end
  137. private
  138. MAX_KEY_CARDINALITY = 512
  139. WARN_KEY_CARDINALITY = 256
  140. class Buffer
  141. def initialize
  142. @key_set = {}
  143. @data = StringIO.new
  144. @gz = Zlib::GzipWriter.new(@data)
  145. end
  146. def key_set_size
  147. @key_set.size
  148. end
  149. def update_key_set(record)
  150. record.each_key {|key|
  151. @key_set[key] = true
  152. }
  153. @key_set.size
  154. end
  155. def append(data)
  156. @gz << data
  157. end
  158. def size
  159. @data.size
  160. end
  161. def flush!
  162. close
  163. @data.string
  164. end
  165. def close
  166. @gz.close unless @gz.closed?
  167. end
  168. end
  169. def to_msgpack(msg)
  170. begin
  171. msg.to_msgpack
  172. rescue NoMethodError
  173. JSON.load(JSON.dump(msg)).to_msgpack
  174. end
  175. end
  176. def add(db, table, msg)
  177. # NOTE: TreasureData::API is defined at td-client-ruby gem
  178. # https://github.com/treasure-data/td-client-ruby/blob/master/lib/td/client/api.rb
  179. begin
  180. TreasureData::API.validate_database_name(db)
  181. rescue
  182. @logger.error "TreasureDataLogger: Invalid database name #{db.inspect}: #{$!}"
  183. raise "Invalid database name #{db.inspect}: #{$!}"
  184. end
  185. begin
  186. TreasureData::API.validate_table_name(table)
  187. rescue
  188. @logger.error "TreasureDataLogger: Invalid table name #{table.inspect}: #{$!}"
  189. raise "Invalid table name #{table.inspect}: #{$!}"
  190. end
  191. begin
  192. data = to_msgpack(msg)
  193. rescue
  194. @logger.error("TreasureDataLogger: Can't convert to msgpack: #{msg.inspect}: #{$!}")
  195. return false
  196. end
  197. key = [db, table]
  198. @mutex.synchronize do
  199. if @queue.length > @queue_limit
  200. @logger.error("TreasureDataLogger: queue length exceeds limit. can't add new event log: #{msg.inspect}")
  201. return false
  202. end
  203. buffer = (@map[key] ||= Buffer.new)
  204. record = MessagePack.unpack(data)
  205. unless record.is_a?(Hash)
  206. @logger.error("TreasureDataLogger: record must be a Hash: #{msg.inspect}")
  207. return false
  208. end
  209. before = buffer.key_set_size
  210. after = buffer.update_key_set(record)
  211. if after > MAX_KEY_CARDINALITY
  212. @logger.error("TreasureDataLogger: kind of keys in a buffer exceeds #{MAX_KEY_CARDINALITY}.")
  213. @map.delete(key)
  214. return false
  215. end
  216. if before <= WARN_KEY_CARDINALITY && after > WARN_KEY_CARDINALITY
  217. @logger.warn("TreasureDataLogger: kind of keys in a buffer exceeds #{WARN_KEY_CARDINALITY} which is too large. please check the schema design.")
  218. end
  219. buffer.append(data)
  220. if buffer.size > @chunk_limit
  221. # flush this buffer
  222. data = buffer.flush!
  223. @queue << [db, table, data]
  224. @map.delete(key)
  225. @cond.signal
  226. end
  227. # stat upload thread if it's not run
  228. unless @upload_thread
  229. @upload_thread = Thread.new(&method(:upload_main))
  230. end
  231. end
  232. true
  233. end
  234. # assume @mutex is locked
  235. def try_flush
  236. # force flush small buffers if queue is empty
  237. if @queue.empty?
  238. @map.reject! {|(db,table),buffer|
  239. data = buffer.flush!
  240. @queue << [db, table, data]
  241. }
  242. end
  243. if @queue.empty?
  244. return false
  245. end
  246. flushed = false
  247. @mutex.unlock
  248. begin
  249. until @queue.empty?
  250. db, table, data = @queue.first
  251. begin
  252. upload(db, table, data)
  253. @queue.shift
  254. @error_count = 0
  255. flushed = true
  256. rescue
  257. if @error_count < @retry_limit
  258. @logger.error "Failed to upload event logs to Treasure Data, retrying: #{$!}"
  259. @error_count += 1
  260. else
  261. @logger.error "Failed to upload event logs to Treasure Data, trashed: #{$!}"
  262. $!.backtrace.each {|bt|
  263. @logger.info bt
  264. }
  265. @error_count = 0
  266. @queue.clear
  267. end
  268. return nil
  269. end
  270. end
  271. ensure
  272. @mutex.lock
  273. end
  274. return flushed
  275. end
  276. def upload(db, table, data)
  277. unique_key = @use_unique_key ? generate_unique_key : nil
  278. begin
  279. stream = StringIO.new(data)
  280. @logger.info "Uploading event logs to #{db}.#{table} table on Treasure Data (#{stream.size} bytes)"
  281. @client.import(db, table, "msgpack.gz", stream, stream.size, unique_key)
  282. rescue TreasureData::NotFoundError
  283. unless @auto_create_table
  284. raise $!
  285. end
  286. @logger.info "Creating table #{db}.#{table} on Treasure Data"
  287. begin
  288. @client.create_log_table(db, table)
  289. rescue TreasureData::NotFoundError
  290. @client.create_database(db)
  291. @client.create_log_table(db, table)
  292. end
  293. retry
  294. end
  295. end
  296. # NOTE fluentd unique_id and fluent-plugin-td unique_str in reference.
  297. # https://github.com/fluent/fluentd/blob/v0.12.15/lib/fluent/plugin/buf_memory.rb#L22
  298. # https://github.com/treasure-data/fluent-plugin-td/blob/v0.10.27/lib/fluent/plugin/out_tdlog.rb#L225
  299. def generate_unique_key(now = Time.now)
  300. u1 = ((now.to_i*1000*1000+now.usec) << 12 | rand(0xfff))
  301. uid = [u1 >> 32, u1 & 0xffffffff, rand(0xffffffff), rand(0xffffffff)].pack('NNNN')
  302. uid.unpack('C*').map { |x| "%02x" % x }.join
  303. end
  304. require 'thread' # ConditionVariable
  305. if ConditionVariable.new.method(:wait).arity == 1
  306. # "WARNING: Running on Ruby 1.8. Ruby 1.9 is recommended."
  307. require 'timeout'
  308. def cond_wait(sec)
  309. Timeout.timeout(sec) {
  310. @cond.wait(@mutex)
  311. }
  312. rescue Timeout::Error
  313. end
  314. else
  315. def cond_wait(sec)
  316. @cond.wait(@mutex, sec)
  317. end
  318. end
  319. end
  320. end
  321. end