require 'json'
require 'digest/md5'
# An ExceptionReport contains all of the information we have on an
# exception, which can then be transformed into whatever format is
# needed for further investigation. Some data stores may override this
# class, but they should be able to be treated as instances of this
# class regardless.
class ErrorStalker::ExceptionReport
# The name of the application that caused this exception.
attr_reader :application
# The name of the machine that raised this exception.
attr_reader :machine
# The time that this exception occurred
attr_reader :timestamp
# The class name of +exception+
attr_reader :type
# The exception object (or string message) this report represents
attr_reader :exception
# The backtrace corresponding to this exception. Should be an array
# of strings, each referring to a single stack frame.
attr_reader :backtrace
# A unique identifier for this exception
attr_accessor :id
# Build a new ExceptionReport. params[:application] is a
# string identifying the application or component the exception was
# sent from, params[:exception] is the exception object you
# want to report (or a string error message), and
# params[:data] is any extra arbitrary data you want to log
# along with this report.
def initialize(params = {})
params = symbolize_keys(params)
@id = params[:id]
@application = params[:application]
@machine = params[:machine] || machine_name
@timestamp = params[:timestamp] || Time.now
@type = params[:type] || params[:exception].class.name
if params[:exception].is_a?(Exception)
@exception = params[:exception].to_s
else
@exception = params[:exception]
end
@data = params[:data]
if params[:backtrace]
@backtrace = params[:backtrace]
else
@backtrace = params[:exception].backtrace if params[:exception].is_a?(Exception)
end
@digest = params[:digest] if params[:digest]
end
# The number of characters in this exception's stacktrace that
# should be used to uniquify this exception. Exceptions raised from
# the same place should have the same stacktrace, up to
# +STACK_DIGEST_LENGTH+ characters.
STACK_DIGEST_LENGTH = 4096
# Generate a 'mostly-unique' hash code for this exception, that
# should be the same for similar exceptions and different for
# different exceptions. This is used to group similar exceptions
# together.
def digest
@digest ||= Digest::MD5.hexdigest((backtrace ? backtrace.to_s[0,STACK_DIGEST_LENGTH] : exception.to_s) + type.to_s)
end
# A hash of extra data logged along with this exception.
def data
@data_with_fixed_encoding ||= JSON.parse(raw_data.to_json.encode('utf-8', 'ascii-8bit', :invalid => :replace, :undef => :replace))
end
# The extra data associated with this object, without fixing any
# broken encodings.
def raw_data
@data
end
# Serialize this object to json, so we can send it over the wire.
def to_json
{
:application => application,
:machine => machine,
:timestamp => timestamp,
:type => type,
:exception => exception,
:data => raw_data,
:backtrace => backtrace
}.to_json
end
private
# Shamelessly stolen from rails. Converts the keys in +hash+ from
# strings to symbols.
def symbolize_keys(hash)
hash.inject({}) do |options, (key, value)|
options[(key.to_sym rescue key) || key] = value
options
end
end
# Determine the name of this machine. Should work on Windows, Linux,
# and Mac OS X, but only tested on Debian, Ubuntu, and Mac OS X.
def machine_name
machine_name = 'unknown'
if RUBY_PLATFORM =~ /win32/
machine_name = ENV['COMPUTERNAME']
elsif RUBY_PLATFORM =~ /linux/ || RUBY_PLATFORM =~ /darwin/
machine_name = `/bin/hostname`.chomp
end
machine_name
end
end