├── uninstall.rb ├── install.rb ├── views └── logged_exceptions │ ├── destroy.rjs │ ├── destroy_all.rjs │ ├── show.rjs │ ├── query.rjs │ ├── _feed.rhtml │ ├── _show.rhtml │ ├── query.rxml │ ├── _exceptions.rhtml │ └── index.rhtml ├── tasks └── exception_logger_tasks.rake ├── test └── exception_logger_test.rb ├── init.rb ├── generators └── exception_migration │ ├── exception_migration_generator.rb │ ├── USAGE │ └── templates │ └── migration.rb ├── Rakefile ├── lib ├── logged_exceptions_helper.rb ├── logged_exception.rb ├── exception_loggable.rb └── logged_exceptions_controller.rb ├── assets ├── exception_logger.js └── style.css └── README /uninstall.rb: -------------------------------------------------------------------------------- 1 | # Uninstall hook code here 2 | -------------------------------------------------------------------------------- /install.rb: -------------------------------------------------------------------------------- 1 | puts IO.read(File.join(File.dirname(__FILE__), 'README')) 2 | -------------------------------------------------------------------------------- /views/logged_exceptions/destroy.rjs: -------------------------------------------------------------------------------- 1 | page["exception-#{params[:id]}"].addClassName('deleted') 2 | page[:showpage].hide -------------------------------------------------------------------------------- /views/logged_exceptions/destroy_all.rjs: -------------------------------------------------------------------------------- 1 | page[:exceptions].replace :partial => "exceptions" 2 | page[:showpage].hide -------------------------------------------------------------------------------- /tasks/exception_logger_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :exception_logger do 3 | # # Task goes here 4 | # end -------------------------------------------------------------------------------- /views/logged_exceptions/show.rjs: -------------------------------------------------------------------------------- 1 | page[:showpage].replace_html :partial => "show" 2 | page[:showpage].visual_effect :blind_down, :duration => 0.25 -------------------------------------------------------------------------------- /views/logged_exceptions/query.rjs: -------------------------------------------------------------------------------- 1 | page[:exceptions].replace :partial => "exceptions" 2 | page[:showpage].hide 3 | page[:feed].replace :partial => 'feed' -------------------------------------------------------------------------------- /test/exception_logger_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | class ExceptionLoggerTest < Test::Unit::TestCase 4 | # Replace this with your real tests. 5 | def test_this_plugin 6 | flunk 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | $PAGINATION_TYPE = 'none' 2 | #require 'will_paginate' 3 | #$PAGINATION_TYPE = 'will_paginate' 4 | #WillPaginate.enable 5 | #require 'paginating_find' 6 | #$PAGINATION_TYPE = 'paginating_find' 7 | LoggedExceptionsController.view_paths = [File.join(directory, 'views')] 8 | -------------------------------------------------------------------------------- /views/logged_exceptions/_feed.rhtml: -------------------------------------------------------------------------------- 1 | <% 2 | feed_params = [:id, :query, :date_ranges_filter, :exception_names_filter, :controller_actions_filter].inject({:format => 'rss'}) do |p, key| 3 | params[key].blank? ? p : p.update(key => params[key]) 4 | end 5 | -%> 6 | -------------------------------------------------------------------------------- /generators/exception_migration/exception_migration_generator.rb: -------------------------------------------------------------------------------- 1 | class ExceptionMigrationGenerator < Rails::Generator::NamedBase 2 | attr_reader :exception_table_name 3 | def initialize(runtime_args, runtime_options = {}) 4 | @exception_table_name = (runtime_args.length < 2 ? 'logged_exceptions' : runtime_args[1]).tableize 5 | runtime_args << 'add_exception_table' if runtime_args.empty? 6 | super 7 | end 8 | 9 | def manifest 10 | record do |m| 11 | m.migration_template 'migration.rb', 'db/migrate' 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /generators/exception_migration/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | The exception migration generator creates a migration for the logged exceptions model. 3 | 4 | The generator takes a migration name as its argument. The migration name may be 5 | given in CamelCase or under_score. 'add_exception_table' is the default. 6 | 7 | The generator creates a migration class in db/migrate prefixed by its number 8 | in the queue. 9 | 10 | Example: 11 | ./script/generate exception_migration add_exception_table 12 | 13 | With 4 existing migrations, this will create an AddExceptionTable migration in the 14 | file db/migrate/5_add_exception_table.rb -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the exception_logger plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.pattern = 'test/**/*_test.rb' 12 | t.verbose = true 13 | end 14 | 15 | desc 'Generate documentation for the exception_logger plugin.' 16 | Rake::RDocTask.new(:rdoc) do |rdoc| 17 | rdoc.rdoc_dir = 'rdoc' 18 | rdoc.title = 'ExceptionLogger' 19 | rdoc.options << '--line-numbers' << '--inline-source' 20 | rdoc.rdoc_files.include('README') 21 | rdoc.rdoc_files.include('lib/**/*.rb') 22 | end 23 | -------------------------------------------------------------------------------- /generators/exception_migration/templates/migration.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %> < ActiveRecord::Migration 2 | def self.up 3 | create_table "<%= exception_table_name %>", :force => true do |t| 4 | t.column :exception_class, :string 5 | t.column :controller_name, :string 6 | t.column :action_name, :string 7 | t.column :message, :text 8 | t.column :backtrace, :text 9 | t.column :environment, :text 10 | t.column :request, :text 11 | t.column :created_at, :datetime 12 | end 13 | end 14 | 15 | def self.down 16 | drop_table "<%= exception_table_name %>" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /views/logged_exceptions/_show.rhtml: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to_remote 'Delete', {:url => { :action => 'destroy', :id => @exc }} , :class => "util" %> 3 | | 4 | <%= link_to_function "Close", "$('showpage').style.display='none';", :class => "util" %> 5 | 6 |
7 | 8 |
9 | <%= @exc.created_at.strftime("%A, %b %d, %Y at %l:%M %p") %> 10 |
11 |

12 | <%= @exc.exception_class %> in <%= @exc.controller_name.camelcase %>/<%= @exc.action_name %> 13 |

14 | 15 |

Request

16 | 17 | <%= textilize(@exc.request) %> 18 | 19 |

Backtrace

20 | 21 | <%= simple_format @exc.message %> 22 | 23 |
24 | <%=h(@exc.backtrace).gsub(/\n/,"
") %> 25 |
26 | 27 |

Environment

28 | 29 | <%= textilize(@exc.environment) %> -------------------------------------------------------------------------------- /views/logged_exceptions/query.rxml: -------------------------------------------------------------------------------- 1 | xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8" 2 | 3 | xml.rss "version" => "2.0" do 4 | xml.channel do 5 | xml.title "Recent Exceptions#{%( (filtered)) if filtered?} | #{LoggedExceptionsController.application_name}" 6 | xml.link url_for(:only_path => false, :skip_relative_url_root => false) 7 | xml.language "en-us" 8 | xml.ttl "60" 9 | 10 | @exceptions.each do |exc| 11 | xml.item do 12 | xml.title "#{exc.exception_class} in #{exc.controller_action} @ #{exc.created_at.rfc822}" 13 | xml.description exc.message 14 | xml.pubDate exc.created_at.rfc822 15 | xml.guid [request.host_with_port, 'exceptions', exc.id.to_s].join(":"), "isPermaLink" => "false" 16 | xml.link url_for(:action => 'index', :id => exc, :only_path => false, :skip_relative_url_root => false) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /views/logged_exceptions/_exceptions.rhtml: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= link_to_remote 'Delete Visible', :url => { :action => 'destroy_all' }, :with => "ExceptionLogger.deleteAll()" %> 4 | <%= pagination_remote_links @exceptions %> 5 |
6 | 7 |

Exceptions <%= "(filtered)" if filtered? %>

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% exceptions.each do |exc| -%> 20 | exception"> 21 | 28 | 41 | 42 | 43 | 44 | <% end -%> 45 | 46 | 47 |
ExceptionDate
22 |
23 | 24 | <%= link_to_remote "#{exc.exception_class} in #{exc.controller_action}", :url => { :action => 'show', :id => exc } %>
25 | <%=h exc.message %> 26 | 27 |
29 | <% 30 | if Date.today == exc.created_at.to_date 31 | if exc.created_at > Time.now - 4.hours 32 | %> 33 | <%= time_ago_in_words(exc.created_at).gsub(/about /,"~ ") %> ago 34 | <% else %> 35 | Today, <%= exc.created_at.strftime("%l:%M %p") %> 36 | <% end %> 37 | <% else %> 38 | <%= exc.created_at.strftime("%b %d, %Y") %> 39 | <% end %> 40 | <%= link_to_remote 'Delete', {:url => { :action => 'destroy', :id => exc }} , :class => "util" %>
48 | 49 |
50 | <%= pagination_remote_links @exceptions %> 51 |
52 | 53 | 54 | 55 |
56 | -------------------------------------------------------------------------------- /lib/logged_exceptions_helper.rb: -------------------------------------------------------------------------------- 1 | module LoggedExceptionsHelper 2 | def filtered? 3 | [:query, :date_ranges_filter, :exception_names_filter, :controller_actions_filter].any? { |p| params[p] } 4 | end 5 | 6 | def pagination_remote_links(collection) 7 | ret = '' 8 | if $PAGINATION_TYPE == 'will_paginate' 9 | pagination = will_paginate (collection, 10 | :renderer => 'LoggedExceptionsHelper::PaginationRenderer', 11 | :prev_label => '', 12 | :next_label => '', 13 | :container => false) 14 | if collection.total_pages > 1 then 15 | ret = ":: Pages : #{pagination}" 16 | end 17 | elsif $PAGINATION_TYPE == 'paginating_find' then 18 | pagination = paginating_links collection 19 | ret = ":: Pages : #{pagination}" 20 | else 21 | next_page = params[:page].to_i + 1 22 | prev_page = 0 23 | prev_link = '' 24 | if params[:page].to_i > 0 then 25 | prev_page = params[:page].to_i - 1 26 | prev_link = " Previous page" 27 | end 28 | next_link = "Next page" 29 | ret = "Pagination not available#{prev_link} - #{next_link}" 30 | end 31 | end 32 | 33 | if $PAGINATION_TYPE == 'will_paginate' 34 | class PaginationRenderer < WillPaginate::LinkRenderer 35 | def page_link_or_span(page, span_class = 'current', text = nil) 36 | text ||= page.to_s 37 | if page and page != current_page 38 | @template.link_to_function text, "ExceptionLogger.setPage(#{page})" 39 | else 40 | @template.content_tag :span, text, :class => span_class 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /assets/exception_logger.js: -------------------------------------------------------------------------------- 1 | ExceptionLogger = { 2 | filters: ['exception_names', 'controller_actions', 'date_ranges'], 3 | setPage: function(num) { 4 | $('page').value = num; 5 | $('query-form').onsubmit(); 6 | }, 7 | 8 | setFilter: function(context, name) { 9 | var filterName = context + '_filter' 10 | $(filterName).value = ($F(filterName) == name) ? '' : name; 11 | this.deselect(context, filterName); 12 | $('page').value = '1'; 13 | $('query-form').onsubmit(); 14 | }, 15 | 16 | deselect: function(context, filterName) { 17 | $$('#' + context + ' a').each(function(a) { 18 | var value = $(filterName) ? $F(filterName) : null; 19 | a.className = (value && (a.getAttribute('title') == value || a.innerHTML == value)) ? 'selected' : ''; 20 | }); 21 | }, 22 | 23 | deleteAll: function() { 24 | return Form.serialize('query-form') + '&' + $$('tr.exception').collect(function(tr) { return tr.getAttribute('id').gsub(/^\w+-/, ''); }).toQueryString('ids'); 25 | } 26 | } 27 | 28 | Event.observe(window, 'load', function() { 29 | ExceptionLogger.filters.each(function(context) { 30 | $(context + '_filter').value = ''; 31 | }); 32 | }); 33 | 34 | Object.extend(Array.prototype, { 35 | toQueryString: function(name) { 36 | return this.collect(function(item) { return name + "[]=" + encodeURIComponent(item) }).join('&'); 37 | } 38 | }); 39 | 40 | Ajax.Responders.register({ 41 | onCreate: function() { 42 | if($('activity') && Ajax.activeRequestCount > 0) $('activity').visualEffect('appear', {duration:0.25}); 43 | }, 44 | 45 | onComplete: function() { 46 | if($('activity') && Ajax.activeRequestCount == 0) $('activity').visualEffect('fade', {duration:0.25}); 47 | } 48 | }); -------------------------------------------------------------------------------- /lib/logged_exception.rb: -------------------------------------------------------------------------------- 1 | class LoggedException < ActiveRecord::Base 2 | class << self 3 | def create_from_exception(controller, exception, data) 4 | message = exception.message.inspect 5 | message << "\n* Extra Data\n\n#{data}" unless data.blank? 6 | create! \ 7 | :exception_class => exception.class.name, 8 | :controller_name => controller.controller_name, 9 | :action_name => controller.action_name, 10 | :message => message, 11 | :backtrace => exception.backtrace, 12 | :request => controller.request 13 | end 14 | 15 | def find_exception_class_names 16 | connection.select_values "SELECT DISTINCT exception_class FROM #{table_name} ORDER BY exception_class" 17 | end 18 | 19 | def find_exception_controllers_and_actions 20 | find(:all, :select => "DISTINCT controller_name, action_name", :order => "controller_name, action_name").collect(&:controller_action) 21 | end 22 | 23 | def host_name 24 | `hostname -s`.chomp 25 | end 26 | end 27 | 28 | def backtrace=(backtrace) 29 | backtrace = sanitize_backtrace(backtrace) * "\n" unless backtrace.is_a?(String) 30 | write_attribute :backtrace, backtrace 31 | end 32 | 33 | def request=(request) 34 | if request.is_a?(String) 35 | write_attribute :request, request 36 | else 37 | max = request.env.keys.max { |a,b| a.length <=> b.length } 38 | env = request.env.keys.sort.inject [] do |env, key| 39 | env << '* ' + ("%-*s: %s" % [max.length, key, request.env[key].to_s.strip]) 40 | end 41 | write_attribute(:environment, (env << "* Process: #{$$}" << "* Server : #{self.class.host_name}") * "\n") 42 | 43 | write_attribute(:request, [ 44 | "* URL:#{" #{request.method.to_s.upcase}" unless request.get?} #{request.protocol}#{request.env["HTTP_HOST"]}#{request.request_uri}", 45 | "* Format: #{request.format.to_s}", 46 | "* Parameters: #{request.parameters.inspect}", 47 | "* Rails Root: #{rails_root}" 48 | ] * "\n") 49 | end 50 | end 51 | 52 | def controller_action 53 | @controller_action ||= "#{controller_name.camelcase}/#{action_name}" 54 | end 55 | 56 | private 57 | @@rails_root = Pathname.new(RAILS_ROOT).cleanpath.to_s 58 | @@backtrace_regex = /^#{Regexp.escape(@@rails_root)}/ 59 | 60 | def sanitize_backtrace(trace) 61 | trace.collect { |line| Pathname.new(line.gsub(@@backtrace_regex, "[RAILS_ROOT]")).cleanpath.to_s } 62 | end 63 | 64 | def rails_root 65 | @@rails_root 66 | end 67 | end -------------------------------------------------------------------------------- /lib/exception_loggable.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | 3 | # Copyright (c) 2005 Jamis Buck 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | module ExceptionLoggable 24 | def self.included(target) 25 | target.extend(ClassMethods) 26 | end 27 | 28 | module ClassMethods 29 | def consider_local(*args) 30 | local_addresses.concat(args.flatten.map { |a| IPAddr.new(a) }) 31 | end 32 | 33 | def local_addresses 34 | addresses = read_inheritable_attribute(:local_addresses) 35 | unless addresses 36 | addresses = [IPAddr.new("127.0.0.1")] 37 | write_inheritable_attribute(:local_addresses, addresses) 38 | end 39 | addresses 40 | end 41 | 42 | def exception_data(deliverer = self, &block) 43 | deliverer = block if block 44 | if deliverer == self 45 | read_inheritable_attribute(:exception_data) 46 | else 47 | write_inheritable_attribute(:exception_data, deliverer) 48 | end 49 | end 50 | end 51 | 52 | def local_request? 53 | remote = IPAddr.new(request.remote_ip) 54 | !self.class.local_addresses.detect { |addr| addr.include?(remote) }.nil? 55 | end 56 | 57 | def rescue_action_in_public(exception) 58 | status = response_code_for_rescue(exception) 59 | render_optional_error_file status 60 | log_exception(exception) if status != :not_found 61 | end 62 | 63 | def log_exception(exception) 64 | deliverer = self.class.exception_data 65 | data = case deliverer 66 | when nil then {} 67 | when Symbol then send(deliverer) 68 | when Proc then deliverer.call(self) 69 | end 70 | 71 | LoggedException.create_from_exception(self, exception, data) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /views/logged_exceptions/index.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logged Exceptions 5 | 6 | <%= javascript_include_tag 'prototype','effects' %> 7 | 10 | 13 | 14 | 15 |
16 | 17 | 18 | 70 | 71 |
72 | 75 |
76 | <%= render :partial => "exceptions" %> 77 |
78 |
79 | 80 | 81 | 82 |
83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ExceptionLogger 2 | =============== 3 | 4 | The Exception Logger (forgive the horrible name) logs your Rails exceptions in the database and provides a funky web interface to manage them. 5 | 6 | First you need to generate the migration: 7 | 8 | ./script/generate exception_migration 9 | 10 | Next, you'll need to include the ExceptionLoggable module into ApplicationController. Once that's done you might want to modify key methods to customize the logging: 11 | 12 | render_404(exception) - Shows the 404 template. 13 | 14 | render_500(exception) - Shows the 500 template. 15 | 16 | log_exception(exception) - Logs the actual exception in the database. 17 | 18 | rescue_action_in_public(exception) - Does not log these exceptions: ActiveRecord::RecordNotFound, ActionController::UnknownController, ActionController::UnknownAction 19 | 20 | Now add a new route to your routes.rb: 21 | 22 | map.connect "logged_exceptions/:action/:id", :controller => "logged_exceptions" 23 | 24 | After that, visit /logged_exceptions in your application to manage the exceptions. 25 | 26 | Once you have done that, open up the vendor/plugins/init.rb file and choose your pagination, 27 | supported options are will_paginate, paginating_find, and simple mysql based pagination (Uses LIMIT) 28 | The current default is none. To use the other options you need to uncomment the $PAGINATION_TYPE line 29 | and the require for that pagination, you should comment out what you won't use etc... 30 | 31 | It's understandable that you may want to require authentication. Add this to your config/environments/production.rb: 32 | 33 | # config/environments/production.rb 34 | config.after_initialize do 35 | require 'application' unless Object.const_defined?(:ApplicationController) 36 | LoggedExceptionsController.class_eval do 37 | # set the same session key as the app 38 | session :session_key => '_beast_session_id' 39 | 40 | # include any custom auth modules you need 41 | include AuthenticationSystem 42 | 43 | before_filter :login_required 44 | 45 | # optional, sets the application name for the rss feeds 46 | self.application_name = "Beast" 47 | 48 | protected 49 | # only allow admins 50 | # this obviously depends on how your auth system works 51 | def authorized? 52 | current_user.is_a?(Admin) 53 | end 54 | 55 | # assume app's login required doesn't use http basic 56 | def login_required_with_basic 57 | respond_to do |accepts| 58 | # alias_method_chain will alias the app's login_required to login_required_without_basic 59 | accepts.html { login_required_without_basic } 60 | 61 | # access_denied_with_basic_auth is defined in LoggedExceptionsController 62 | # get_auth_data returns back the user/password pair 63 | accepts.rss do 64 | access_denied_with_basic_auth unless self.current_user = User.authenticate(*get_auth_data) 65 | end 66 | end 67 | end 68 | 69 | alias_method_chain :login_required, :basic 70 | end 71 | end 72 | 73 | The exact code of course depends on the specific needs of your application. 74 | 75 | CREDITS 76 | 77 | Jamis Buck - original exception_notification plugin 78 | Rick Olson - model/controller code 79 | Josh Goebel - design 80 | Jason Knight - Pagination support, built on/inspired by Ryanb's willpaginate support. 81 | -------------------------------------------------------------------------------- /lib/logged_exceptions_controller.rb: -------------------------------------------------------------------------------- 1 | class LoggedExceptionsController < ActionController::Base 2 | cattr_accessor :application_name 3 | layout nil 4 | 5 | def index 6 | @exception_names = LoggedException.find_exception_class_names 7 | @controller_actions = LoggedException.find_exception_controllers_and_actions 8 | query 9 | end 10 | 11 | def query 12 | conditions = [] 13 | parameters = [] 14 | unless params[:id].blank? 15 | conditions << 'id = ?' 16 | parameters << params[:id] 17 | end 18 | unless params[:query].blank? 19 | conditions << 'message LIKE ?' 20 | parameters << "%#{params[:query]}%" 21 | end 22 | unless params[:date_ranges_filter].blank? 23 | conditions << 'created_at >= ?' 24 | parameters << params[:date_ranges_filter].to_f.days.ago.utc 25 | end 26 | unless params[:exception_names_filter].blank? 27 | conditions << 'exception_class = ?' 28 | parameters << params[:exception_names_filter] 29 | end 30 | unless params[:controller_actions_filter].blank? 31 | conditions << 'controller_name = ? AND action_name = ?' 32 | parameters += params[:controller_actions_filter].split('/').collect(&:downcase) 33 | end 34 | if $PAGINATION_TYPE == 'will_paginate' then 35 | @exceptions = LoggedException.paginate :order => 'created_at desc', :per_page => 30, 36 | :conditions => conditions.empty? ? nil : parameters.unshift(conditions * ' and '), :page => params[:page] 37 | elsif $PAGINATION_TYPE == 'paginating_find' then 38 | params[:limit] ||= 25 39 | params[:page] ||= 1 40 | @exceptions = LoggedException.find (:all,:order => 'created_at desc',:page => {:size => params[:limit], :current => params[:page]}, 41 | :conditions => conditions.empty? ? nil : parameters.unshift(conditions * ' and ')) 42 | else 43 | #we have no pagination so do basic sql pagination 44 | params[:limit] ||= 25 45 | params[:page] ||= 0 46 | page = params[:page] 47 | if params[:page].to_i >= 1 then 48 | page = params[:page].to_i * params[:limit].to_i 49 | end 50 | @exceptions = LoggedException.find(:all, :limit => "#{page},#{params[:limit]}", :conditions => conditions.empty? ? nil : parameters.unshift(conditions * ' and ')) 51 | end 52 | 53 | respond_to do |format| 54 | format.html { redirect_to :action => 'index' unless action_name == 'index' } 55 | format.js { render :action => 'query.rjs' } 56 | format.rss { render :action => 'query.rxml' } 57 | end 58 | end 59 | 60 | def show 61 | @exc = LoggedException.find params[:id] 62 | end 63 | 64 | def destroy 65 | LoggedException.destroy params[:id] 66 | end 67 | 68 | def destroy_all 69 | LoggedException.delete_all ['id in (?)', params[:ids]] unless params[:ids].blank? 70 | query 71 | end 72 | 73 | private 74 | def access_denied_with_basic_auth 75 | headers["Status"] = "Unauthorized" 76 | headers["WWW-Authenticate"] = %(Basic realm="Web Password") 77 | render :text => "Could't authenticate you", :status => '401 Unauthorized' 78 | end 79 | 80 | @@http_auth_headers = %w(X-HTTP_AUTHORIZATION HTTP_AUTHORIZATION Authorization) 81 | # gets BASIC auth info 82 | def get_auth_data 83 | auth_key = @@http_auth_headers.detect { |h| request.env.has_key?(h) } 84 | auth_data = request.env[auth_key].to_s.split unless auth_key.blank? 85 | return auth_data && auth_data[0] == 'Basic' ? Base64.decode64(auth_data[1]).split(':')[0..1] : [nil, nil] 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | margin:0; 4 | padding:0; 5 | background:#000; 6 | font-family:'Lucida Grande', Arial, Helvetica, sans-serif; 7 | font-size:1.0em; 8 | color:white; 9 | } 10 | 11 | #container 12 | { 13 | xwidth:95%; 14 | margin:0 auto; 15 | min-width:800px; 16 | } 17 | 18 | #left 19 | { 20 | xfloat:left; 21 | xwidth:76%; 22 | margin-right:235px; 23 | 24 | } 25 | 26 | #left .page 27 | { 28 | font-size:0.9em; 29 | background:white; 30 | border:2px solid #999; 31 | border-width:0 2px 2px 0; 32 | padding:25px; 33 | xmargin-bottom:1em; 34 | color:black; 35 | } 36 | 37 | #right 38 | { 39 | margin-top:1em; 40 | float:right; 41 | xwidth:22%; 42 | font-size:0.9em; 43 | width:200px; 44 | margin-right:1.25em; 45 | } 46 | 47 | #right hr 48 | { 49 | border:0; 50 | border-top:1px solid #222; 51 | } 52 | 53 | #search 54 | { 55 | margin-top:2em; 56 | background:#111; 57 | padding:10px 5px; 58 | border:1px solid #222; 59 | border-width:1px 0; 60 | } 61 | 62 | 63 | ul.filters 64 | { 65 | list-style-type:none; 66 | padding:0; 67 | margin:0; 68 | font-size:0.9em; 69 | } 70 | ul.filters li { margin-bottom:0.2em;} 71 | 72 | 73 | ul.filters a, 74 | ul.filters a:visited { 75 | display:block; 76 | padding:1px 7px; 77 | color:#fff; 78 | xbackground-color:#fff; 79 | text-decoration:none; 80 | } 81 | 82 | ul.filters a:hover 83 | { 84 | background:#666; 85 | color:white; 86 | } 87 | 88 | ul.filters a.selected, 89 | ul.filters a.selected:visited, 90 | ul.filters a:active { 91 | color:gold; 92 | background-color:#333; 93 | text-decoration:none; 94 | font-weight:bold; 95 | } 96 | 97 | 98 | onclick a:hover 99 | { 100 | color:#fff; 101 | text-decoration:none; 102 | } 103 | 104 | #exceptions table 105 | { 106 | width:99%; 107 | border-collapse:collapse; 108 | } 109 | 110 | td 111 | { 112 | padding:5px 10px; 113 | xborder:1px solid #ddd; 114 | } 115 | 116 | #backtrace 117 | { 118 | overflow:auto; 119 | font-family:"Bitstream Vera Sans Mono", "Monaco", "Courier", monospace;; 120 | font-size:0.85em; 121 | margin-top:0.25em; 122 | } 123 | 124 | h2 125 | { 126 | background-color:#ddd; 127 | font-size:0.8em; 128 | padding:3px 10px; 129 | } 130 | 131 | form {margin:0;} 132 | 133 | h3 { 134 | font-size:0.8em; 135 | color:#ddd; 136 | background:#222; 137 | padding:3px 7px; 138 | } 139 | 140 | div.date 141 | { 142 | color:#666; 143 | font-size:0.8em; 144 | } 145 | 146 | h1 147 | { 148 | margin-top:0.25em; 149 | font-size:1.25em; 150 | padding-bottom:5px; 151 | border-bottom:2px solid #ddd; 152 | } 153 | 154 | h1 span 155 | { 156 | color:#aaa; 157 | font-weight:normal; 158 | } 159 | 160 | a 161 | { 162 | color:#369; 163 | text-decoration:none; 164 | } 165 | a:hover 166 | { 167 | color:blue; 168 | text-decoration:underline; 169 | } 170 | 171 | th 172 | { 173 | text-align:left; 174 | xbackground:#333; 175 | xcolor:gold; 176 | font-size:0.75em; 177 | padding:2px 10px; 178 | } 179 | 180 | tr { xcursor:pointer; } 181 | 182 | tr.eor td 183 | { 184 | background:#e7e7e7; 185 | 186 | } 187 | 188 | /* 189 | tr:hover td, 190 | tr.eor:hover td 191 | { 192 | background:#333; 193 | color:white; 194 | } 195 | tr:hover td a, 196 | tr.eor:hover td a { color:gold; } 197 | */ 198 | 199 | .message 200 | { 201 | font-size:0.8em; 202 | } 203 | 204 | a.util 205 | { 206 | color:#c00; 207 | font-size:0.7em; 208 | } 209 | 210 | .pipe 211 | { 212 | font-size:0.75em; 213 | color:#999; 214 | } 215 | 216 | .tools { float:right; } 217 | 218 | .time 219 | { 220 | color:#666; 221 | font-size:0.75em; 222 | xvertical-align:top; 223 | } 224 | 225 | 226 | .expclass 227 | { 228 | xcolor:#999; 229 | } 230 | .expclass a 231 | { 232 | font-size:0.9em; 233 | } 234 | 235 | tr.deleted td { 236 | color:#aaa; 237 | text-decoration: line-through; 238 | } 239 | tr.deleted td a { color:#aaa; } 240 | 241 | .pages { float:right; margin-right:1em; } 242 | .pages a { text-decoration:underline; } 243 | .pages-bottom { border-top:2px solid #ddd; text-align:right; float:none; 244 | padding-top:0.4em; 245 | margin-top:0.4em; 246 | padding-right:1em; 247 | margin-right:0; 248 | } 249 | 250 | /* right */ 251 | 252 | #right h4 253 | { 254 | font-size:0.75em; 255 | xbackground:#171717; 256 | xbackground:#333; 257 | color:#999; 258 | padding:3px 5px; 259 | margin-bottom:0.5em; 260 | font-weight:normal; 261 | } 262 | 263 | /* tabs */ 264 | 265 | ul.tabs 266 | { 267 | list-style-type:none; 268 | padding:0; 269 | margin:1em 0; 270 | float:left; 271 | } 272 | ul.tabs li { float:left; display:inline; } 273 | ul.tabs li a 274 | { 275 | font-size:0.8em; 276 | padding:3px 7px; 277 | margin-right:1em; 278 | text-decoration:none; 279 | color:black; 280 | } 281 | 282 | ul.tabs li a:hover 283 | { 284 | background:#666; 285 | color:white; 286 | } 287 | ul.tabs li.selected a 288 | { 289 | background:black; 290 | color:white; 291 | } 292 | 293 | #activity { 294 | position:fixed; 295 | top:0; right:18px; 296 | width:200px; 297 | padding:5px 0; 298 | text-align:center; 299 | background-color:#cf1313; 300 | color:#fff; 301 | opacity:.8; 302 | font-size: 11px; 303 | } 304 | 305 | #feed { 306 | margin-top: 15px; 307 | } 308 | --------------------------------------------------------------------------------