Skip to content

Instantly share code, notes, and snippets.

@ahoward
Last active July 11, 2025 19:33
Show Gist options
  • Save ahoward/d25e2a39dbd2cd1c12dcb4510cea9531 to your computer and use it in GitHub Desktop.
Save ahoward/d25e2a39dbd2cd1c12dcb4510cea9531 to your computer and use it in GitHub Desktop.
why add an exception tracker to your rails (or ruby) app's dependency list when you already have s3?
# file: app/controllers/concerns/s3_errors.rb
=begin
TL;DR;
class ApplicationController
include S3Errors
end
=end
module S3Errors
extend ActiveSupport::Concern
included do
rescue_from StandardError, with: :upload_error_to_s3
end
def S3Errors.rotate!(cutoff: nil)
cutoff ||= 1.week.ago.beginning_of_day
re = %r{errors/(\d{4})/(\d{2})/(\d{2})/}
n = 0
S3.list('errors/') do |key|
if((match = key.match(re)))
yyyy, mm, dd = match[1].to_i, match[2].to_i, match[3].to_i
date = Date.new(yyyy, mm, dd)
if date < cutoff.to_date
S3.delete(key)
n += 1
end
end
end
return n
end
AutoRotater = Thread.new do
loop do
begin
sleep 1.minute
S3Errors.rotate!
rescue => error
Rails.logger.error(error)
end
sleep 59.minutes
end
end
private
def upload_error_to_s3(error)
uuid = SecureRandom.uuid_v7.to_s
now = Time.now.utc
path = "errors/#{now.strftime('%Y/%m/%d')}/#{uuid}.json"
data = {
class: error.class.name,
message: error.message,
backtrace: error.backtrace,
created_at: now.iso8601(2)
}
json = data.to_json
S3.write(path, json)
raise error
end
end
# file: lib/s3.rb
require 'aws-sdk-s3'
require 'mime/types'
module S3
def config
@config ||= Hash.new
end
def config=(config)
@config = config
end
def config_for(key)
config.fetch(key.to_s) { config.fetch(key.to_sym) }
end
def region
config_for(:region)
end
def access_key_id
config_for(:access_key_id)
end
def secret_access_key
config_for(:secret_access_key)
end
def bucket
config_for(:bucket)
end
def client
Aws::S3::Client.new(
region:,
access_key_id:,
secret_access_key:,
)
end
def url_for(key)
"https://#{bucket}.s3.amazonaws.com/#{key}"
end
def write(key, body, **kws)
kws[:content_type] ||= MIME::Types.type_for(key).first.to_s
kws[:content_disposition] ||= 'inline'
args = {
bucket:,
key:,
body:,
**kws
}
obj = client.put_object(**args)
url = url_for(key)
{ key:, url: }.update(args)
end
alias_method :put, :write
def read(key, &block)
obj = client.get_object(bucket:, key: key)
if block
while (chunk = obj.body.read(1024))
block.call(chunk)
end
else
obj.body.string
end
end
alias_method :get, :read
def list(prefix = '', limit: nil, &block)
args = { bucket:, prefix: }
accum = []
n = 0
client.list_objects_v2(**args).each do |response|
response.contents.each do |obj|
key = obj.key
block ? block.call(key) : accum.push(key)
n += 1
break if limit && (n >= limit)
end
break if limit && (n >= limit)
end
block ? nil : accum
end
alias_method :ls, :list
def delete(key, *keys)
keys.unshift(key)
keys.flatten!
keys.compact!
to_delete = keys.map { { key: } }
to_delete.each_slice(1000) do |objects|
args = { bucket:, delete: { objects: } }
client.delete_objects(args)
end
keys
end
alias_method :rm, :delete
def exist(key)
begin
client.head_object(bucket:, key: key)
true
rescue Aws::S3::Errors::NotFound => e
false
end
end
alias_method :exist?, :exist
alias_method :exists?, :exist
def fetch(key, &block)
begin
read(key)
rescue Aws::S3::Errors::NoSuchKey => _error
block.call.tap { |obj| write(key, obj) }
end
end
extend self
end
# file: config/initializers/s3.rb
require Rails.root.join('lib/s3.rb')
S3.config = Rails.application.credentials.fetch(:aws)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment