lib/timber/log_devices/http.rb in timber-1.0.3 vs lib/timber/log_devices/http.rb in timber-1.0.4

- old
+ new

@@ -1,83 +1,121 @@ -require "monitor" -require "msgpack" +require "timber/log_devices/http/triggered_buffer" module Timber module LogDevices - # A log device that buffers and sends logs to the Timber API over HTTP in intervals. The buffer - # uses MessagePack::Buffer, which is fast, efficient with memory, and reduces - # the payload size sent to Timber. + # A log device that buffers and delivers log messages over HTTPS to the Timber API in batches. + # The buffer and delivery strategy are very efficient and the log messages will be delivered in + # msgpack format. + # + # See {#initialize} for options and more details. class HTTP - class DeliveryError < StandardError; end - API_URI = URI.parse("https://api.timber.io/http_frames") - CONTENT_TYPE = "application/json".freeze + CONTENT_TYPE = "application/x-timber-msgpack-frame-1".freeze CONNECTION_HEADER = "keep-alive".freeze USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze - HTTPS = Net::HTTP.new(API_URI.host, API_URI.port).tap do |https| https.use_ssl = true https.read_timeout = 30 https.ssl_timeout = 10 if https.respond_to?(:keep_alive_timeout=) https.keep_alive_timeout = 60 end https.open_timeout = 10 end + DELIVERY_FREQUENCY_SECONDS = 2.freeze + RETRY_LIMIT = 3.freeze + BACKOFF_RATE_SECONDS = 3.freeze - DEFAULT_DELIVERY_FREQUENCY = 2.freeze - # Instantiates a new HTTP log device. + # Instantiates a new HTTP log device that can be passed to {Timber::Logger#initialize}. # # @param api_key [String] The API key provided to you after you add your application to # [Timber](https://timber.io). # @param [Hash] options the options to create a HTTP log device with. - # @option attributes [Symbol] :frequency_seconds (2) How often the client should + # @option attributes [Symbol] :payload_limit_bytes Determines the maximum size in bytes that + # and HTTP payload can be. Please see {TriggereBuffer#initialize} for the default. + # @option attributes [Symbol] :buffer_limit_bytes Determines the maximum size of the total + # buffer. This should be many times larger than the `:payload_limit_bytes`. + # Please see {TriggereBuffer#initialize} for the default. + # @option attributes [Symbol] :buffer_overflow_handler (nil) When a single message exceeds + # `:payload_limit_bytes` or the entire buffer exceeds `:buffer_limit_bytes`, the Proc + # passed to this option will be called with the msg that would overflow the buffer. See + # the examples on how to use this properly. + # @option attributes [Symbol] :delivery_frequency_seconds (2) How often the client should # attempt to deliver logs to the Timber API. The HTTP client buffers logs between calls. + # + # @example Basic usage + # Timber::Logger.new(Timber::LogDevices::HTTP.new("my_timber_api_key")) + # + # @example Handling buffer overflows + # # Persist overflowed lines to a file + # # Note: You could write these to any permanent storage. + # overflow_log_path = "/path/to/my/overflow_log.log" + # overflow_handler = Proc.new { |log_line_msg| File.write(overflow_log_path, log_line_ms) } + # http_log_device = Timber::LogDevices::HTTP.new("my_timber_api_key", + # buffer_overflow_handler: overflow_handler) + # Timber::Logger.new(http_log_device) def initialize(api_key, options = {}) @api_key = api_key - @buffer = [] - @monitor = Monitor.new - @delivery_thread = Thread.new do - at_exit { deliver } + @buffer = TriggeredBuffer.new( + payload_limit_bytes: options[:payload_limit_bytes], + limit_bytes: options[:buffer_limit_bytes], + overflow_handler: options[:buffer_overflow_handler] + ) + @delivery_interval_thread = Thread.new do loop do - sleep options[:frequency_seconds] || DEFAULT_DELIVERY_FREQUENCY - deliver + sleep(options[:delivery_frequency_seconds] || DELIVERY_FREQUENCY_SECONDS) + buffer_for_delivery = @buffer.reserve + if buffer_for_delivery + deliver(buffer_for_delivery) + end end end end + # Write a new log line message to the buffer, and deliver if the msg exceeds the + # payload limit. def write(msg) - @monitor.synchronize { - @buffer << msg - } + buffer_for_delivery = @buffer.write(msg) + if buffer_for_delivery + deliver(buffer_for_delivery) + end + true end + # Closes the log device, cleans up, and attempts one last delivery. def close - @delivery_thread.kill + @delivery_interval_thread.kill + buffer_for_delivery = @buffer.reserve + if buffer_for_delivery + deliver(buffer_for_delivery) + end end private - def deliver - body = @buffer.read + def deliver(body) + Thread.new do + RETRY_LIMIT.times do |try_index| + request = Net::HTTP::Post.new(API_URI.request_uri).tap do |req| + req['Authorization'] = authorization_payload + req['Connection'] = CONNECTION_HEADER + req['Content-Type'] = CONTENT_TYPE + req['User-Agent'] = USER_AGENT + req.body = body + end - request = Net::HTTP::Post.new(API_URI.request_uri).tap do |req| - req['Authorization'] = authorization_payload - req['Connection'] = CONNECTION_HEADER - req['Content-Type'] = CONTENT_TYPE - req['User-Agent'] = USER_AGENT - req.body = body - end - - HTTPS.request(request).tap do |res| - code = res.code.to_i - if code < 200 || code >= 300 - raise DeliveryError.new("Bad response from Timber API - #{res.code}: #{res.body}") + res = HTTPS.request(request) + code = res.code.to_i + if code < 200 || code >= 300 + Config.instance.logger.debug("Timber HTTP delivery failed - #{res.code}: #{res.body}") + sleep((try_index + 1) * BACKOFF_RATE_SECONDS) + else + @buffer.remove(body) + Config.instance.logger.debug("Timber HTTP delivery successful - #{code}") + break # exit the loop + end end - Config.instance.logger.debug("Success! #{code}: #{res.body}") end - - @buffer.clear end def authorization_payload @authorization_payload ||= "Basic #{Base64.strict_encode64(@api_key).chomp}" end \ No newline at end of file