error_stalker 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +76 -0
- data/README.rdoc +3 -0
- data/Rakefile +19 -0
- data/bin/create_indexes +14 -0
- data/bin/error_stalker_server +16 -0
- data/error_stalker.gemspec +21 -0
- data/lib/error_stalker.rb +4 -0
- data/lib/error_stalker/backend.rb +14 -0
- data/lib/error_stalker/backend/base.rb +14 -0
- data/lib/error_stalker/backend/in_memory.rb +25 -0
- data/lib/error_stalker/backend/log_file.rb +33 -0
- data/lib/error_stalker/backend/server.rb +41 -0
- data/lib/error_stalker/client.rb +62 -0
- data/lib/error_stalker/exception_group.rb +29 -0
- data/lib/error_stalker/exception_report.rb +116 -0
- data/lib/error_stalker/plugin.rb +42 -0
- data/lib/error_stalker/plugin/base.rb +24 -0
- data/lib/error_stalker/plugin/email_sender.rb +60 -0
- data/lib/error_stalker/plugin/lighthouse_reporter.rb +95 -0
- data/lib/error_stalker/plugin/views/exception_email.erb +18 -0
- data/lib/error_stalker/plugin/views/report.erb +18 -0
- data/lib/error_stalker/server.rb +152 -0
- data/lib/error_stalker/server/public/exception_logger.css +173 -0
- data/lib/error_stalker/server/public/grid.css +338 -0
- data/lib/error_stalker/server/public/images/background.png +0 -0
- data/lib/error_stalker/server/public/jquery-1.4.4.min.js +167 -0
- data/lib/error_stalker/server/views/_exception_message.erb +1 -0
- data/lib/error_stalker/server/views/_exception_table.erb +34 -0
- data/lib/error_stalker/server/views/index.erb +31 -0
- data/lib/error_stalker/server/views/layout.erb +18 -0
- data/lib/error_stalker/server/views/search.erb +41 -0
- data/lib/error_stalker/server/views/show.erb +32 -0
- data/lib/error_stalker/server/views/similar.erb +6 -0
- data/lib/error_stalker/sinatra_link_renderer.rb +25 -0
- data/lib/error_stalker/store.rb +11 -0
- data/lib/error_stalker/store/base.rb +75 -0
- data/lib/error_stalker/store/in_memory.rb +109 -0
- data/lib/error_stalker/store/mongoid.rb +318 -0
- data/lib/error_stalker/version.rb +4 -0
- data/test/test_helper.rb +8 -0
- data/test/unit/backend/base_test.rb +9 -0
- data/test/unit/backend/in_memory_test.rb +22 -0
- data/test/unit/backend/log_file_test.rb +25 -0
- data/test/unit/client_test.rb +67 -0
- data/test/unit/exception_report_test.rb +24 -0
- data/test/unit/plugins/email_sender_test.rb +12 -0
- data/test/unit/server_test.rb +141 -0
- data/test/unit/stores/in_memory_test.rb +58 -0
- metadata +109 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# The ErrorStalker server supports third-party plugins that add
|
2
|
+
# functionality to the server.
|
3
|
+
#
|
4
|
+
# Plugins should inherit from ErrorStalker::Plugin::Base. This base
|
5
|
+
# plugin provides functions that can be overridden in its
|
6
|
+
# children. Currently, a few hooks are provided for plugins to
|
7
|
+
# override:
|
8
|
+
#
|
9
|
+
# [ErrorStalker::Plugin::Base.new] Called when the server starts. This
|
10
|
+
# method can be overridden to do things
|
11
|
+
# like add extra routes to the server
|
12
|
+
# or initialize any data structures
|
13
|
+
# that the plugin needs to keep track
|
14
|
+
# of. Take a look at
|
15
|
+
# ErrorStalker::Plugin::LighthouseReporter
|
16
|
+
# for a good example of how this method
|
17
|
+
# can be hooked to provide additional
|
18
|
+
# functionality.
|
19
|
+
#
|
20
|
+
# [ErrorStalker::Plugin::Base#exception_links] Called when rendering
|
21
|
+
# exception details. This
|
22
|
+
# function should return an
|
23
|
+
# array of [link_text,
|
24
|
+
# link_href] pairs that
|
25
|
+
# will be used to link to
|
26
|
+
# additional routes that
|
27
|
+
# the plugin might add.
|
28
|
+
#
|
29
|
+
# [ErrorStalker::Plugin::Base#after_create] Called when a new exception
|
30
|
+
# is
|
31
|
+
# reported. ErrorStalker::Plugin::EmailSender
|
32
|
+
# has a good example of this
|
33
|
+
# being used.
|
34
|
+
#
|
35
|
+
# After creating a plugin, it can be added to the server in the server
|
36
|
+
# configuration file or manually added using the
|
37
|
+
# ErrorStalker::Server#plugins attribute.
|
38
|
+
module ErrorStalker::Plugin
|
39
|
+
autoload :Base, 'error_stalker/plugin/base'
|
40
|
+
autoload :LighthouseReporter, 'error_stalker/plugin/lighthouse_reporter'
|
41
|
+
autoload :EmailSender, 'error_stalker/plugin/email_sender'
|
42
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# The base ErrorStalker::Plugin, that all plugins should inherit
|
2
|
+
# from. Provides default implementations of all the supported plugin
|
3
|
+
# methods, so you can (and should) call +super+ in your plugin subclasses.
|
4
|
+
class ErrorStalker::Plugin::Base
|
5
|
+
|
6
|
+
# Create a new instance of this plugin. +app+ is the sinatra
|
7
|
+
# ErrorStalker::Server instance, and +params+ is an arbitrary hash of
|
8
|
+
# plugin-specific parameters or options.
|
9
|
+
def initialize(app, params = {})
|
10
|
+
end
|
11
|
+
|
12
|
+
# An array of [name, href] pairs of links that will show up on the
|
13
|
+
# exception detail page. These are most commonly used to link to
|
14
|
+
# additional routes added by the plugin.
|
15
|
+
def exception_links(exception_report)
|
16
|
+
[]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Called after a new exception is reported. At the point that this
|
20
|
+
# is called, +exception_report+ will have an ID and has been
|
21
|
+
# associated with an exception group.
|
22
|
+
def after_create(app, exception_report)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
# The email sender plugin will send an email to an address the first
|
4
|
+
# time an exception in a group is reported. Future exceptions that go
|
5
|
+
# in the same group will not trigger emails.
|
6
|
+
class ErrorStalker::Plugin::EmailSender < ErrorStalker::Plugin::Base
|
7
|
+
|
8
|
+
# Parameters that are used in order to figure out how and where to
|
9
|
+
# send the report email. These parameters are passed directly to the
|
10
|
+
# {mail gem}[https://github.com/mikel/mail]. See that page for a
|
11
|
+
# reference. The subject and body are created by the plugin, but all
|
12
|
+
# other parameters (to, from, etc.) will have to be passed in.
|
13
|
+
attr_reader :mail_params
|
14
|
+
|
15
|
+
# Create a new instance of this plugin. +mail_params+ is a hash of
|
16
|
+
# parameters that are used to build the report email.
|
17
|
+
def initialize(app, mail_params = {})
|
18
|
+
super(app, mail_params)
|
19
|
+
@mail_params = mail_params
|
20
|
+
end
|
21
|
+
|
22
|
+
# Builds the mail object from +exception_report+ that we can later
|
23
|
+
# deliver. This is mostly here to make testing easier.
|
24
|
+
def build_email(exception_report, exception_url)
|
25
|
+
@report = exception_report
|
26
|
+
@url = exception_url
|
27
|
+
|
28
|
+
mail = Mail.new({
|
29
|
+
'subject' => "Exception on #{exception_report.machine} - #{exception_report.exception.to_s[0, 64]}",
|
30
|
+
'body' => ERB.new(File.read(File.expand_path('views/exception_email.erb', File.dirname(__FILE__)))).result(binding)
|
31
|
+
}.merge(mail_params))
|
32
|
+
|
33
|
+
if mail_params['delivery_method']
|
34
|
+
mail.delivery_method(mail_params['delivery_method'].to_sym, (mail_params['delivery_settings'] || {}))
|
35
|
+
end
|
36
|
+
|
37
|
+
mail
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets up the parameters we need to build a new exception report
|
41
|
+
# email, and sends the mail.
|
42
|
+
def send_email(app, exception_report)
|
43
|
+
request = app.request
|
44
|
+
host_with_port = request.host
|
45
|
+
host_with_port << ":#{request.port}" if request.port != 80
|
46
|
+
url = "#{request.scheme}://#{host_with_port}/exceptions/#{exception_report.id}.html"
|
47
|
+
mail = build_email(exception_report, url)
|
48
|
+
mail.deliver
|
49
|
+
end
|
50
|
+
|
51
|
+
# Hook to trigger an email when a new exception report with
|
52
|
+
# +report+'s digest comes in.
|
53
|
+
def after_create(app, report)
|
54
|
+
# Only send an email if it's the first exception of this type
|
55
|
+
# we've seen
|
56
|
+
send_email(app, report) if app.store.group(report.digest).count == 1
|
57
|
+
super(app, report)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'lighthouse'
|
2
|
+
|
3
|
+
# A simple plugin for reporting exceptions as new bugs in
|
4
|
+
# {Lighthouse}[http://lighthouseapp.com]. When this plugin is enabled,
|
5
|
+
# a new link will show up on the exception detail page that
|
6
|
+
# pre-populates a form for sending the exception report to Lighthouse.
|
7
|
+
class ErrorStalker::Plugin::LighthouseReporter < ErrorStalker::Plugin::Base
|
8
|
+
|
9
|
+
# The lighthouse project id that this plugin will report bugs to.
|
10
|
+
attr_reader :project_id
|
11
|
+
|
12
|
+
# Creates a new instance of this plugin. There are a few parameters
|
13
|
+
# that must be passed in in order for this plugin to work correctly:
|
14
|
+
#
|
15
|
+
# [params['account']] The Lighthouse account name these exceptions
|
16
|
+
# will be posted as
|
17
|
+
#
|
18
|
+
# [params['token']] The read/write token assigned by Lighthouse for
|
19
|
+
# API access
|
20
|
+
#
|
21
|
+
# [params['project_id']] The Lighthouse project these exceptions
|
22
|
+
# will be reported to
|
23
|
+
def initialize(app, params = {})
|
24
|
+
super(app, params)
|
25
|
+
app.class.send(:include, Actions)
|
26
|
+
app.lighthouse = self
|
27
|
+
Lighthouse.account = params['account']
|
28
|
+
Lighthouse.token = params['token']
|
29
|
+
@project_id = params['project_id']
|
30
|
+
end
|
31
|
+
|
32
|
+
# A list containing the link that will show up on the exception
|
33
|
+
# report's detail page. This link hooks into one of the new actions
|
34
|
+
# defined by this plugin.
|
35
|
+
def exception_links(exception_report)
|
36
|
+
super(exception_report) + [["Report to Lighthouse", "/lighthouse/report/#{exception_report.id}.html"]]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Create a new Lighthouse ticket object pre-populated with information
|
40
|
+
# about +exception+.
|
41
|
+
def new_ticket(request, exception)
|
42
|
+
ticket = Lighthouse::Ticket.new(:project_id => project_id)
|
43
|
+
ticket.title = "Exception: #{exception.exception}"
|
44
|
+
host_with_port = request.host
|
45
|
+
host_with_port << ":#{request.port}" if request.port != 80
|
46
|
+
ticket_link = "#{request.scheme}://#{host_with_port}/exceptions/#{exception.id}.html"
|
47
|
+
ticket.body = ticket_link
|
48
|
+
ticket.tags = "exception"
|
49
|
+
ticket
|
50
|
+
end
|
51
|
+
|
52
|
+
# Post a new ticket to lighthouse with the params specified in
|
53
|
+
# +params+.
|
54
|
+
def post_ticket(params)
|
55
|
+
ticket = Lighthouse::Ticket.new(:project_id => project_id)
|
56
|
+
ticket.title = params[:title]
|
57
|
+
ticket.body = params[:body]
|
58
|
+
ticket.tags = params[:tags]
|
59
|
+
ticket.save
|
60
|
+
end
|
61
|
+
|
62
|
+
# Extra actions that will be added to the ErrorStalker::Server
|
63
|
+
# instance when this plugin is enabled. Provides actions for
|
64
|
+
# creating a new Lighthouse ticket with exception details and
|
65
|
+
# posting the ticket to Lighthouse. This module also adds a
|
66
|
+
# +lighthouse+ accessor to the server instance that can be used to
|
67
|
+
# build and send tickets to Lighthouse.
|
68
|
+
module Actions
|
69
|
+
|
70
|
+
# Adds the actions described above to the ErrorStalker::Server
|
71
|
+
# instance.
|
72
|
+
def self.included(base)
|
73
|
+
base.class_eval do
|
74
|
+
|
75
|
+
attr_accessor :lighthouse
|
76
|
+
|
77
|
+
get '/lighthouse/report/:id.html' do
|
78
|
+
@exception = store.find(params["id"])
|
79
|
+
@ticket = lighthouse.new_ticket(request, @exception)
|
80
|
+
erb File.read(File.expand_path('views/report.erb', File.dirname(__FILE__)))
|
81
|
+
end
|
82
|
+
|
83
|
+
post '/lighthouse/report/:id.html' do
|
84
|
+
if lighthouse.post_ticket(params)
|
85
|
+
redirect "/exceptions/#{params[:id]}.html"
|
86
|
+
else
|
87
|
+
@error = "There was an error submitting the ticket: <br />#{@ticket.errors.join("<br />")}"
|
88
|
+
erb File.read(File.expand_path('views/report.erb', File.dirname(__FILE__)))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
Exception: <%= @report.application %> (<%= @report.machine %>) at <%= @report.timestamp %>
|
2
|
+
|
3
|
+
<%= @url %>
|
4
|
+
|
5
|
+
Exception
|
6
|
+
=========
|
7
|
+
|
8
|
+
<%= @report.exception.to_s %>
|
9
|
+
<% if @report.backtrace %>
|
10
|
+
Stack Trace
|
11
|
+
===========
|
12
|
+
|
13
|
+
<%= @report.backtrace.join("\n") %>
|
14
|
+
<% end %>
|
15
|
+
Extra Data
|
16
|
+
==========
|
17
|
+
|
18
|
+
<%= @report.data.to_yaml %>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<%= h @error if @error %>
|
2
|
+
|
3
|
+
<h1>Import into Lighthouse</h1>
|
4
|
+
|
5
|
+
<div id="lighthouse_form" class="clearfix">
|
6
|
+
<form method="post" action="/lighthouse/report/<%= @exception.id %>.html">
|
7
|
+
<label for="title">Title</label>
|
8
|
+
<input id="title" type="text" name="title" value="<%= @ticket.title %>" />
|
9
|
+
|
10
|
+
<label for="tags">Tags</label>
|
11
|
+
<input id="tags" type="text" name="tags" value="<%= @ticket.tags %>" />
|
12
|
+
|
13
|
+
<label for="body">Body</label>
|
14
|
+
<textarea id="body" name="body"><%= @ticket.body %></textarea>
|
15
|
+
|
16
|
+
<input type="submit" name="Create" value="Create" />
|
17
|
+
</form>
|
18
|
+
</div>
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
$: << File.expand_path('..', File.dirname(__FILE__))
|
4
|
+
require 'error_stalker'
|
5
|
+
require 'error_stalker/store'
|
6
|
+
require 'error_stalker/plugin'
|
7
|
+
require 'erb'
|
8
|
+
require 'will_paginate'
|
9
|
+
require 'will_paginate/view_helpers/base'
|
10
|
+
require 'error_stalker/sinatra_link_renderer'
|
11
|
+
require 'error_stalker/version'
|
12
|
+
|
13
|
+
module ErrorStalker
|
14
|
+
# The ErrorStalker server. Provides a UI for browsing, grouping, and
|
15
|
+
# searching exception reports, as well as a centralized store for
|
16
|
+
# keeping exception reports. As a Sinatra app, this can be run using
|
17
|
+
# a config.ru file or something like Vegas. A sample Vegas runner
|
18
|
+
# for the server is located in <tt>bin/error_stalker_server</tt>.
|
19
|
+
class Server < Sinatra::Base
|
20
|
+
|
21
|
+
# The number of exceptions or exception groups to show on each
|
22
|
+
# page.
|
23
|
+
PER_PAGE = 25
|
24
|
+
|
25
|
+
# The data store (ErrorStalker::Store instance) to use to store
|
26
|
+
# exception data
|
27
|
+
attr_accessor :store
|
28
|
+
|
29
|
+
# A list of plugins the server will use.
|
30
|
+
attr_accessor :plugins
|
31
|
+
|
32
|
+
set :root, File.dirname(__FILE__)
|
33
|
+
set :public, Proc.new { File.join(root, "server/public") }
|
34
|
+
set :views, Proc.new { File.join(root, "server/views") }
|
35
|
+
|
36
|
+
helpers do
|
37
|
+
include Rack::Utils
|
38
|
+
alias_method :h, :escape_html
|
39
|
+
|
40
|
+
include WillPaginate::ViewHelpers::Base
|
41
|
+
|
42
|
+
# Generates a url from an array of strings representing the
|
43
|
+
# parts of the path.
|
44
|
+
def url(*path_parts)
|
45
|
+
u = '/' + path_parts.join("/")
|
46
|
+
u += '.html' unless u =~ /\.\w{2,4}$/
|
47
|
+
u
|
48
|
+
end
|
49
|
+
alias_method :u, :url
|
50
|
+
|
51
|
+
# Cuts +str+ at +limit+ characters. If +str+ is too long, will
|
52
|
+
# apped '...' to the end of the returned string.
|
53
|
+
def cutoff(str, limit = 100)
|
54
|
+
if str.length > limit
|
55
|
+
str[0, limit] + '...'
|
56
|
+
else
|
57
|
+
str
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class << self
|
63
|
+
# A hash of configuration options, usually read from a
|
64
|
+
# configuration file.
|
65
|
+
attr_accessor :configuration
|
66
|
+
end
|
67
|
+
self.configuration = {}
|
68
|
+
|
69
|
+
# The default ErrorStalker::Store subclass to use by default for
|
70
|
+
# this ErrorStalker::Server instance. This is defined as a class
|
71
|
+
# method as well as an instance method so that it can be set by a
|
72
|
+
# configuration file before rack creates the instance of this
|
73
|
+
# sinatra app.
|
74
|
+
def self.store
|
75
|
+
if configuration['store']
|
76
|
+
store_class = configuration['store']['class'].split('::').inject(Object) {|mod, string| mod.const_get(string)}
|
77
|
+
store_class.new(*Array(configuration['store']['parameters']))
|
78
|
+
else
|
79
|
+
ErrorStalker::Store::InMemory.new
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# A hash of configuration options, usually read from a
|
84
|
+
# configuration file.
|
85
|
+
def configuration
|
86
|
+
self.class.configuration
|
87
|
+
end
|
88
|
+
|
89
|
+
# Creates a new instance of the server, based on the configuration
|
90
|
+
# contained in the +configuration+ attribute.
|
91
|
+
def initialize
|
92
|
+
super
|
93
|
+
self.plugins = []
|
94
|
+
if configuration['plugin']
|
95
|
+
configuration['plugin'].each do |config|
|
96
|
+
plugin_class = config['class'].split('::').inject(Object) {|mod, string| mod.const_get(string)}
|
97
|
+
self.plugins << plugin_class.new(self, config['parameters'])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
self.store = self.class.store
|
101
|
+
end
|
102
|
+
|
103
|
+
get '/' do
|
104
|
+
@records = store.recent.paginate(:page => params[:page], :per_page => PER_PAGE)
|
105
|
+
erb :index
|
106
|
+
end
|
107
|
+
|
108
|
+
get '/search' do
|
109
|
+
@results = store.search(params).paginate(:page => params[:page], :per_page => PER_PAGE) if params["Search"]
|
110
|
+
erb :search
|
111
|
+
end
|
112
|
+
|
113
|
+
get '/similar/:digest.html' do
|
114
|
+
@group = store.reports_in_group(params[:digest]).paginate(:page => params[:page], :per_page => PER_PAGE)
|
115
|
+
if @group
|
116
|
+
erb :similar
|
117
|
+
else
|
118
|
+
404
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
get '/exceptions/:id.html' do
|
123
|
+
@report = store.find(params[:id])
|
124
|
+
if @report
|
125
|
+
@group = store.group(@report.digest)
|
126
|
+
erb :show
|
127
|
+
else
|
128
|
+
404
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
get '/stats.json' do
|
133
|
+
timestamp = Time.at(params[:timestamp].to_i) if params[:timestamp]
|
134
|
+
# default to 1 hour ago
|
135
|
+
timestamp ||= Time.now - (60*60)
|
136
|
+
stats = {}
|
137
|
+
stats[:timestamp] = timestamp.to_i
|
138
|
+
stats[:total_since] = store.total_since(timestamp)
|
139
|
+
stats[:total] = store.total
|
140
|
+
stats[:version] = ErrorStalker::VERSION
|
141
|
+
stats.to_json
|
142
|
+
end
|
143
|
+
|
144
|
+
post '/report.json' do
|
145
|
+
report = ErrorStalker::ExceptionReport.new(JSON.parse(request.body.read))
|
146
|
+
report.id = store.store(report)
|
147
|
+
plugins.each {|p| p.after_create(self, report)}
|
148
|
+
200
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
body {
|
2
|
+
font-family: Helvetica, Calibri, sans-serif;
|
3
|
+
}
|
4
|
+
|
5
|
+
h1, h2, h3, h4 {
|
6
|
+
font-color: #222;
|
7
|
+
width: 960px;
|
8
|
+
overflow: hidden;
|
9
|
+
}
|
10
|
+
|
11
|
+
table {
|
12
|
+
border-collapse: separate;
|
13
|
+
border-spacing: 0;
|
14
|
+
margin: 0;
|
15
|
+
padding: 0;
|
16
|
+
border: 0;
|
17
|
+
outline: 0;
|
18
|
+
vertical-align: baseline;
|
19
|
+
}
|
20
|
+
|
21
|
+
h1 {
|
22
|
+
margin-top: 12px;
|
23
|
+
margin-bottom: 12px;
|
24
|
+
}
|
25
|
+
|
26
|
+
h3 {
|
27
|
+
margin-bottom: 4px;
|
28
|
+
}
|
29
|
+
|
30
|
+
a {
|
31
|
+
text-decoration: none;
|
32
|
+
color: #007a94;
|
33
|
+
}
|
34
|
+
|
35
|
+
a:visited {
|
36
|
+
color: #83389b;
|
37
|
+
}
|
38
|
+
|
39
|
+
table#exceptions td, table#exceptions th {
|
40
|
+
padding: 10px;
|
41
|
+
height: 80px;
|
42
|
+
word-wrap: break-word;
|
43
|
+
}
|
44
|
+
|
45
|
+
table#exceptions {
|
46
|
+
-moz-box-shadow: 0px 2px 5px #666;
|
47
|
+
-webkit-box-shadow: 0px 2px 5px #666;
|
48
|
+
box-shadow: 0px 2px 5px #666;
|
49
|
+
width: 960px;
|
50
|
+
}
|
51
|
+
|
52
|
+
table#exceptions, table#exceptions tr:first-child th:first-child {
|
53
|
+
-webkit-border-top-left-radius: 6px;
|
54
|
+
-moz-border-radius-topleft: 6px;
|
55
|
+
border-top-left-radius: 6px;
|
56
|
+
}
|
57
|
+
|
58
|
+
table#exceptions, table#exceptions tr:first-child th:last-child {
|
59
|
+
-webkit-border-top-right-radius: 6px;
|
60
|
+
-moz-border-radius-topright: 6px;
|
61
|
+
border-top-right-radius: 6px;
|
62
|
+
}
|
63
|
+
|
64
|
+
table#exceptions th {
|
65
|
+
color: #eee;
|
66
|
+
background-color: #333;
|
67
|
+
background: url('images/background.png');
|
68
|
+
border-bottom: 1px solid #dadada;
|
69
|
+
text-shadow:0 -1px 1px rgba(0,0,0,0.5);
|
70
|
+
}
|
71
|
+
|
72
|
+
table#exceptions tr.even {
|
73
|
+
background-color: #ccc;
|
74
|
+
}
|
75
|
+
|
76
|
+
table#exceptions tr.odd {
|
77
|
+
background-color: #eee;
|
78
|
+
}
|
79
|
+
|
80
|
+
|
81
|
+
table#exceptions .count {
|
82
|
+
width: 80px;
|
83
|
+
text-align: center;
|
84
|
+
}
|
85
|
+
|
86
|
+
table#exceptions .last_occurred {
|
87
|
+
width: 240px;
|
88
|
+
}
|
89
|
+
|
90
|
+
table#exceptions td.last_occurred {
|
91
|
+
font-size: 0.8em;
|
92
|
+
}
|
93
|
+
|
94
|
+
table#exceptions td.last_occurred .timestamp {
|
95
|
+
font-size: 1.2em;
|
96
|
+
}
|
97
|
+
|
98
|
+
table#exceptions .exception, table#exceptions .exception div {
|
99
|
+
width: 480px;
|
100
|
+
}
|
101
|
+
|
102
|
+
table#exceptions .links {
|
103
|
+
text-align: center;
|
104
|
+
width: 160px;
|
105
|
+
color: #777;
|
106
|
+
}
|
107
|
+
|
108
|
+
table#exceptions .details {
|
109
|
+
width: 240px;
|
110
|
+
}
|
111
|
+
|
112
|
+
pre {
|
113
|
+
overflow: auto;
|
114
|
+
padding: 8px;
|
115
|
+
background-color: #d6d6d6;
|
116
|
+
margin: 0px;
|
117
|
+
border: 1px solid #aaa;
|
118
|
+
}
|
119
|
+
|
120
|
+
code {
|
121
|
+
font-size: 14px;
|
122
|
+
font-family: Inconsolata, Consolas, 'Lucida Console', monospace;
|
123
|
+
}
|
124
|
+
|
125
|
+
dl.group_details dt {
|
126
|
+
clear: left;
|
127
|
+
float: left;
|
128
|
+
font-weight: bold;
|
129
|
+
}
|
130
|
+
|
131
|
+
dl.group_details dd {
|
132
|
+
margin-left: 220px;
|
133
|
+
}
|
134
|
+
|
135
|
+
form {
|
136
|
+
margin-left: 10px;
|
137
|
+
width: 380px;
|
138
|
+
}
|
139
|
+
|
140
|
+
form label {
|
141
|
+
width: 140px;
|
142
|
+
margin-right: 10px;
|
143
|
+
display: block;
|
144
|
+
float: left;
|
145
|
+
}
|
146
|
+
|
147
|
+
form input, #search_form select {
|
148
|
+
width: 220px;
|
149
|
+
margin-left: 10px;
|
150
|
+
float: left;
|
151
|
+
}
|
152
|
+
|
153
|
+
form textarea {
|
154
|
+
width: 220px;
|
155
|
+
height: 80px;
|
156
|
+
margin-left: 10px;
|
157
|
+
float: left;
|
158
|
+
}
|
159
|
+
|
160
|
+
form input[type=submit] {
|
161
|
+
clear: left;
|
162
|
+
width: auto;
|
163
|
+
margin: 8px 0px 0px 0px;
|
164
|
+
}
|
165
|
+
|
166
|
+
.pagination {
|
167
|
+
padding: 16px 0px;
|
168
|
+
}
|
169
|
+
|
170
|
+
.pagination em {
|
171
|
+
font-weight: bold;
|
172
|
+
font-style: normal;
|
173
|
+
}
|