Last active
June 11, 2024 16:45
-
-
Save samsonjs/55313b8af154024e16a6bc55dc9d8f1b to your computer and use it in GitHub Desktop.
Honeybadger logger for Ruby on Rails
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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