error_stalker 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}
|