Skip to content

Instantly share code, notes, and snippets.

@samsonjs
Last active June 11, 2024 16:45
Show Gist options
  • Save samsonjs/55313b8af154024e16a6bc55dc9d8f1b to your computer and use it in GitHub Desktop.
Save samsonjs/55313b8af154024e16a6bc55dc9d8f1b to your computer and use it in GitHub Desktop.
Honeybadger logger for Ruby on Rails
require 'json'
require 'logger'
require 'net/http'
require 'stringio'
require 'zlib'
module Pronto
# Logs messages to Honeybadger's events API (Insights). The logger buffers messages and sends
# them to the API in batches. The logs are currently not structured and are sent as plain text,
# but if/when we actually move off of Papertrail then we can consider sending structured logs as
# JSON instead using lograge.
class HoneybadgerLogger < ::Logger
API_KEY = 'hbp_abc123'.freeze
MIN_FLUSH_INTERVAL = 300.seconds
MAX_BATCH_SIZE = 100_000 # in bytes, Honeybadger's limit is 102,400 bytes
USER_AGENT = "Pronto 1.0; #{RUBY_VERSION}; #{RUBY_PLATFORM}".freeze
def initialize(*)
super
@buffer = []
@buffer_size = 0
@mutex = Mutex.new
@timer_thread = start_flush_timer
end
def add(severity, message = nil, progname = nil)
super
message ||=
if block_given?
yield
else
progname
end
log_entry = {
ts: Time.now.utc.iso8601,
level: format_severity(severity).downcase,
message:,
}.to_json
buffer_copy = []
@mutex.synchronize do
@buffer << log_entry
@buffer_size += log_entry.bytesize
if @buffer_size >= MAX_BATCH_SIZE
Kernel.warn("[INFO] Flushing logs to Honeybadger because buffer size #{@buffer_size} " \
"exceeds #{MAX_BATCH_SIZE} bytes")
buffer_copy = @buffer.dup
@buffer.clear
@buffer_size = 0
end
end
flush(buffer_copy) unless buffer_copy.empty?
end
private
def start_flush_timer
Thread.new do
loop do
sleep MIN_FLUSH_INTERVAL
Kernel.warn("[INFO] Flushing logs to Honeybadger because timer expired")
buffer_copy = []
@mutex.synchronize do
buffer_copy = @buffer.dup
@buffer.clear
@buffer_size = 0
end
flush(buffer_copy)
end
end
end
def reset_flush_timer
@timer_thread.kill if @timer_thread.alive?
@timer_thread = start_flush_timer
end
def flush(buffer)
reset_flush_timer
if buffer.empty?
return
end
uri = URI('https://api.honeybadger.io/v1/events')
request = Net::HTTP::Post.new(uri, {
'Accept' => 'application/json',
'Content-Encoding' => 'deflate',
'User-Agent' => USER_AGENT,
'X-API-Key' => API_KEY,
})
json_lines = buffer.join("\n")
compressed_data = compress(json_lines)
request.body = compressed_data
Kernel.warn("[INFO] Sending #{json_lines.bytesize} bytes of logs to Honeybadger, " \
"compressed to #{compressed_data.bytesize} bytes")
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
Kernel.warn("[ERROR] Failed to send logs to Honeybadger: #{response}")
end
end
def compress(data)
output = StringIO.new
Zlib::Deflate.new.deflate(data, Zlib::FINISH) { |chunk| output << chunk }
output.string
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment