canvas-lms/app/models/error_report.rb

205 lines
6.4 KiB
Ruby

#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require 'iconv'
class ErrorReport < ActiveRecord::Base
belongs_to :user
belongs_to :account
serialize :http_env
# misc key/value pairs with more details on the error
serialize :data, Hash
before_save :guess_email
# Define a custom callback for external notification of an error report.
define_callbacks :on_send_to_external
# Setup callback to default behavior.
on_send_to_external :send_via_email_or_post
attr_accessible
def send_to_external
run_callbacks(:on_send_to_external)
end
class Reporter
include ActiveSupport::Callbacks
define_callbacks :on_log_error
attr_reader :opts, :exception
def log_error(category, opts)
opts[:category] = category.to_s
@opts = opts
# sanitize invalid encodings
@opts[:message] = TextHelper.strip_invalid_utf8(@opts[:message]) if @opts[:message]
@opts[:exception_message] = TextHelper.strip_invalid_utf8(@opts[:exception_message]) if @opts[:exception_message]
Canvas::Statsd.increment("errors.all")
Canvas::Statsd.increment("errors.#{category}")
run_callbacks :on_log_error
create_error_report(opts)
end
def log_exception(category, exception, opts)
category ||= exception.class.name
opts[:message] ||= exception.to_s
opts[:backtrace] = exception.backtrace.try(:join, "\n")
opts[:exception_message] = exception.to_s
@exception = exception
log_error(category, opts)
end
def create_error_report(opts)
report = ErrorReport.new
report.assign_data(opts)
report.save
report
end
end
# returns the new error report
def self.log_error(category, opts = {})
Reporter.new.log_error(category, opts)
end
# returns the new error report
def self.log_exception(category, exception, opts = {})
Reporter.new.log_exception(category, exception, opts)
end
# assigns data attributes to the column if there's a column with that name,
# otherwise goes into the general data hash
def assign_data(data = {})
self.data ||= {}
data.each do |k,v|
if respond_to?(:"#{k}=")
self.send(:"#{k}=", v)
else
self.data[k.to_s] = v
end
end
end
def backtrace=(val)
if !val || val.length < self.class.maximum_text_length
write_attribute(:backtrace, val)
else
write_attribute(:backtrace, val[0,self.class.maximum_text_length])
end
end
def comments=(val)
if !val || val.length < self.class.maximum_text_length
write_attribute(:comments, val)
else
write_attribute(:comments, val[0,self.class.maximum_text_length])
end
end
def url=(val)
write_attribute(:url, Canvas::LoggingFilter.filter_uri(val))
end
def guess_email
self.email = nil if self.email && self.email.empty?
self.email ||= self.user.email rescue nil
unless self.email
domain = HostUrl.outgoing_email_domain.gsub(/[^a-zA-Z0-9]/, '-')
# example.com definitely won't exist
self.email = "unknown-#{domain}@instructure.example.com"
end
self.email
end
# delete old error reports before a given date
# returns the number of destroyed error reports
def self.destroy_error_reports(before_date)
self.delete_all(['created_at < ?', before_date])
end
USEFUL_ENV = [
"HTTP_ACCEPT",
"HTTP_ACCEPT_ENCODING",
"HTTP_COOKIE",
"HTTP_HOST",
"HTTP_REFERER",
"HTTP_USER_AGENT",
"PATH_INFO",
"QUERY_STRING",
"REMOTE_HOST",
"REQUEST_METHOD",
"REQUEST_PATH",
"REQUEST_URI",
"SERVER_NAME",
"SERVER_PORT",
"SERVER_PROTOCOL",
]
def self.useful_http_env_stuff_from_request(request)
stuff = request.env.slice(*USEFUL_ENV)
stuff['REMOTE_ADDR'] = request.remote_ip # ActionController::Request#remote_ip has proxy smarts
stuff['QUERY_STRING'] = Canvas::LoggingFilter.filter_query_string("?" + stuff['QUERY_STRING'])
stuff['REQUEST_URI'] = Canvas::LoggingFilter.filter_uri(stuff['REQUEST_URI'])
stuff['path_parameters'] = Canvas::LoggingFilter.filter_params(request.path_parameters.dup).inspect # params rails picks up from the url
stuff['query_parameters'] = Canvas::LoggingFilter.filter_params(request.query_parameters.dup).inspect # params rails picks up from the query string
stuff['request_parameters'] = Canvas::LoggingFilter.filter_params(request.request_parameters.dup).inspect # params from forms
stuff
end
def self.categories
distinct('category')
end
# Send the error report based on configuration either via a POST or email to an external location.
def send_via_email_or_post
error_report = self
config = Canvas::Plugin.find('error_reporting').try(:settings) || {}
message_type = (error_report.backtrace || "").split("\n").first.match(/\APosted as[^_]*_([A-Z]*)_/)[1] rescue nil
message_type ||= "ERROR"
body = %{From #{error_report.email}, #{(error_report.user.name rescue "")}
#{message_type} #{error_report.comments + "\n" if error_report.comments}
#{"url: " + error_report.url + "\n" if error_report.url }
#{"user_id: " + (error_report.user_id.to_s) + "\n" if error_report.user_id}
error_id: #{error_report.id}
#{error_report.message + "\n" if error_report.message}
}
if config[:action] == 'post' && config[:url] && config[:subject_param] && config[:body_param]
params = {}
params[config[:subject_param]] = error_report.subject
params[config[:body_param]] = body
Net::HHTP.post_form(URI.parse(config[:url]), params)
elsif config[:action] == 'email' && config[:email]
Message.create!(
:to => config[:email],
:from => "#{error_report.email}",
:subject => "#{error_report.subject} (#{message_type})",
:body => body,
:delay_for => 0,
:context => error_report
)
end
end
private :send_via_email_or_post
end