log_sanity 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +141 -0
- data/LICENSE +20 -0
- data/README.md +180 -0
- data/Rakefile +34 -0
- data/lib/log_sanity.rb +36 -0
- data/lib/log_sanity/extensions/action_controller_helper.rb +12 -0
- data/lib/log_sanity/extensions/active_support_subscriber.rb +25 -0
- data/lib/log_sanity/formatter.rb +45 -0
- data/lib/log_sanity/log_subscribers/action_controller.rb +56 -0
- data/lib/log_sanity/log_subscribers/action_dispatch.rb +63 -0
- data/lib/log_sanity/log_subscribers/action_mailer.rb +27 -0
- data/lib/log_sanity/log_subscribers/active_job.rb +69 -0
- data/lib/log_sanity/log_subscribers/base.rb +10 -0
- data/lib/log_sanity/middleware/request_logger.rb +69 -0
- data/lib/log_sanity/middleware/routing_error_catcher.rb +30 -0
- data/lib/log_sanity/railtie.rb +50 -0
- data/lib/log_sanity/version.rb +3 -0
- data/lib/tasks/log_sanity_tasks.rake +4 -0
- data/log_sanity.gemspec +21 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/config/manifest.js +1 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/mailers/.keep +0 -0
- data/test/dummy/app/models/.keep +0 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +29 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +38 -0
- data/test/dummy/config/environments/production.rb +76 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +9 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/log_sanity_test.rb +7 -0
- data/test/test_helper.rb +19 -0
- metadata +171 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module LogSanity
|
2
|
+
module Extensions
|
3
|
+
module ActiveSupportSubscriber
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def detach_from(namespace, notifier=ActiveSupport::Notifications)
|
8
|
+
subscribers.select{|s| s.is_a? self}.each do |subscriber|
|
9
|
+
subscriber.public_methods(false).each do |event|
|
10
|
+
pattern = "#{event}.#{namespace}"
|
11
|
+
notifier.notifier.listeners_for(pattern).each do |listener|
|
12
|
+
if listener.instance_variable_get(:@delegate) == subscriber
|
13
|
+
notifier.unsubscribe listener
|
14
|
+
subscriber.patterns.delete pattern
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
subscribers.delete subscriber if subscriber.patterns.empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Receives a variety of objects for logging.
|
2
|
+
# LogSanity itself sends Hashes which are formatted with #to_json.
|
3
|
+
# Other than Strings, will embed any other object into a jsonified hash.
|
4
|
+
# Strings are a bit of a special case and by default continue to be formatted
|
5
|
+
# with whatever Rails' formatter originally was. As such, it can be configured
|
6
|
+
# using `config.log_formatter`. This keeps exception backtraces and other
|
7
|
+
# various logs still as Strings. If you prefer Strings to also be turned into
|
8
|
+
# jsonified messages, set `config.logsanity.json_strings = true`.
|
9
|
+
|
10
|
+
module LogSanity
|
11
|
+
class Formatter < Logger::Formatter
|
12
|
+
|
13
|
+
def call(severity, timestamp, progname, msg)
|
14
|
+
if msg.is_a? Hash
|
15
|
+
msg['at'] = timestamp unless msg.key?('at')
|
16
|
+
elsif msg.is_a? String
|
17
|
+
if string_formatter
|
18
|
+
return string_formatter.call(severity, timestamp, progname, msg)
|
19
|
+
else
|
20
|
+
msg = {'at' => timestamp, 'message' => msg}
|
21
|
+
end
|
22
|
+
else
|
23
|
+
msg = {'at' => timestamp, 'object' => msg.inspect}
|
24
|
+
end
|
25
|
+
if msg['at'].is_a? Float
|
26
|
+
monot = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
27
|
+
msg['at'] = Time.now - (monot - msg['at'])
|
28
|
+
end
|
29
|
+
msg['at'] = msg['at'].utc
|
30
|
+
"#{msg.to_json}\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
# noop; for TaggedLogging compatibility
|
34
|
+
def clear_tags! ; end
|
35
|
+
def tagged(*_) ; yield self ; end
|
36
|
+
def current_tags ; [] ; end
|
37
|
+
|
38
|
+
attr_writer :string_formatter
|
39
|
+
|
40
|
+
def string_formatter
|
41
|
+
@string_formatter ||= ActiveSupport::Logger::SimpleFormatter.new
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module LogSanity
|
2
|
+
module LogSubscriber
|
3
|
+
class ActionController < Base
|
4
|
+
INTERNAL_PARAMS = %w(controller action format _method only_path)
|
5
|
+
|
6
|
+
def process_action(event)
|
7
|
+
payload = event.payload
|
8
|
+
params = payload[:params].except(*INTERNAL_PARAMS)
|
9
|
+
format = payload[:format]
|
10
|
+
|
11
|
+
# log 'method', payload[:method]
|
12
|
+
# log 'path', payload[:path]
|
13
|
+
# log 'controller', payload[:controller]
|
14
|
+
# log 'action', payload[:action]
|
15
|
+
log 'route', "#{payload[:controller]}##{payload[:action]}"
|
16
|
+
log 'format', format
|
17
|
+
log 'params', params if params.present?
|
18
|
+
|
19
|
+
status = payload[:status]
|
20
|
+
if status.nil? && payload[:exception].present?
|
21
|
+
exception_class_name = payload[:exception].first
|
22
|
+
status = ::ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
durations = {'total' => event.duration.round}
|
26
|
+
additions = ::ActionController::Base.log_process_action(payload)
|
27
|
+
additions.each do |add|
|
28
|
+
if add =~ /^([^:]+):?\s*([0-9.]+)(ms)?/
|
29
|
+
ms = $2.to_f.round
|
30
|
+
durations[$1.downcase] = ms if ms > 0
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
log 'duration', durations
|
35
|
+
log 'status', status
|
36
|
+
end
|
37
|
+
|
38
|
+
def halted_callback(event)
|
39
|
+
log 'filter_chain_halt', event.payload[:filter].inspect
|
40
|
+
end
|
41
|
+
|
42
|
+
def send_file(event)
|
43
|
+
log 'send_file', event.payload[:path]
|
44
|
+
end
|
45
|
+
|
46
|
+
def redirect_to(event)
|
47
|
+
log 'redirect', event.payload[:location]
|
48
|
+
end
|
49
|
+
|
50
|
+
def send_data(event)
|
51
|
+
log 'send_data', event.payload[:filename] || 'binary'
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module LogSanity
|
2
|
+
module LogSubscriber
|
3
|
+
class ActionDispatch < Base
|
4
|
+
|
5
|
+
def request(event)
|
6
|
+
payload = event.payload
|
7
|
+
return if payload[:silence]
|
8
|
+
|
9
|
+
info do
|
10
|
+
request = payload[:request]
|
11
|
+
response = payload[:response]
|
12
|
+
method = payload[:method] || (request.request_method rescue nil) || 'UNKNOWN'
|
13
|
+
f2 = {
|
14
|
+
'at' => event.time,
|
15
|
+
'event' => "#{request.scheme}_#{method.downcase}",
|
16
|
+
'ip' => request.remote_ip,
|
17
|
+
'rq' => request.uuid,
|
18
|
+
# 'params' => request.filtered_params,
|
19
|
+
# 'path' => request.filtered_path,
|
20
|
+
}
|
21
|
+
|
22
|
+
# unless fields['route']
|
23
|
+
# # most errors repopulate path, so look for the original one first.
|
24
|
+
# # original_path is, however, unfiltered.
|
25
|
+
# fields['path'] = payload[:env]['action_dispatch.original_path']
|
26
|
+
# fields['path'] ||= request.filtered_path
|
27
|
+
# end
|
28
|
+
|
29
|
+
fields['duration'] ||= {}
|
30
|
+
fields['duration']['total'] = event.duration.round
|
31
|
+
# rewrites 'total', which includes more of time spent in middleware
|
32
|
+
fields['status'] ||= response[0].to_i if response
|
33
|
+
compute_tags(request)
|
34
|
+
f2.merge fields
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def compute_tags(request)
|
42
|
+
Rails.application.config.log_tags.each_with_index do |tag, idx|
|
43
|
+
res = case tag
|
44
|
+
when Proc
|
45
|
+
tag.call(request)
|
46
|
+
when Symbol
|
47
|
+
request.send(tag)
|
48
|
+
else
|
49
|
+
tag
|
50
|
+
end
|
51
|
+
if res.is_a?(Hash)
|
52
|
+
fields.deep_merge!(res)
|
53
|
+
elsif tag.is_a? Symbol
|
54
|
+
log tag.to_s, res
|
55
|
+
else
|
56
|
+
log "tag#{idx}", res
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module LogSanity
|
2
|
+
module LogSubscriber
|
3
|
+
class ActionMailer < Base
|
4
|
+
|
5
|
+
def deliver(event)
|
6
|
+
info do
|
7
|
+
{ 'at' => Time.now,
|
8
|
+
'event' => 'mail_send',
|
9
|
+
'from' => Array(event.payload[:from]),
|
10
|
+
'to' => Array(event.payload[:to])
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def receive(event)
|
16
|
+
info do
|
17
|
+
{ 'at' => Time.now,
|
18
|
+
'event' => 'mail_receive',
|
19
|
+
'from' => Array(event.payload[:from]),
|
20
|
+
'to' => Array(event.payload[:to])
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module LogSanity
|
2
|
+
module LogSubscriber
|
3
|
+
class ActiveJob < Base
|
4
|
+
|
5
|
+
def enqueue(event)
|
6
|
+
info do
|
7
|
+
job = event.payload[:job]
|
8
|
+
e = {
|
9
|
+
'at' => Time.now,
|
10
|
+
'event' => 'job_enqueue',
|
11
|
+
'job' => job.class.name,
|
12
|
+
'id' => job.job_id,
|
13
|
+
'queue' => job.queue_name
|
14
|
+
}
|
15
|
+
e['params'] = job.arguments if job.arguments.any?
|
16
|
+
e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def enqueue_at(event)
|
21
|
+
info do
|
22
|
+
job = event.payload[:job]
|
23
|
+
e = {
|
24
|
+
'at' => Time.now,
|
25
|
+
'event' => 'job_enqueue',
|
26
|
+
'job' => job.class.name,
|
27
|
+
'id' => job.job_id,
|
28
|
+
'queue' => job.queue_name,
|
29
|
+
'start_at' => job.scheduled_at
|
30
|
+
}
|
31
|
+
e['params'] = job.arguments if job.arguments.any?
|
32
|
+
e
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# def perform_start(event)
|
37
|
+
# info do
|
38
|
+
# job = event.payload[:job]
|
39
|
+
# e = {
|
40
|
+
# 'at' => Time.now,
|
41
|
+
# 'event' => 'job_start',
|
42
|
+
# 'job' => job.class.name,
|
43
|
+
# 'id' => job.job_id,
|
44
|
+
# 'queue' => job.queue_name,
|
45
|
+
# }
|
46
|
+
# e['params'] = job.arguments if job.arguments.any?
|
47
|
+
# e
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
|
51
|
+
def perform(event)
|
52
|
+
info do
|
53
|
+
job = event.payload[:job]
|
54
|
+
e = {
|
55
|
+
'at' => Time.now,
|
56
|
+
'event' => 'job_perform',
|
57
|
+
'job' => job.class.name,
|
58
|
+
'id' => job.job_id,
|
59
|
+
'queue' => job.queue_name,
|
60
|
+
'duration' => {'total' => event.duration.round}
|
61
|
+
}
|
62
|
+
e['params'] = job.arguments if job.arguments.any?
|
63
|
+
e
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module LogSanity
|
2
|
+
class RequestLogger
|
3
|
+
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
request = ActionDispatch::Request.new(env)
|
10
|
+
|
11
|
+
conditionally_silence(request) do |silence|
|
12
|
+
begin
|
13
|
+
start(request: request)
|
14
|
+
resp = @app.call(env)
|
15
|
+
resp[2] = Rack::BodyProxy.new(resp[2]) do
|
16
|
+
finish(env: env, request: request, response: resp, silence: silence)
|
17
|
+
end
|
18
|
+
resp
|
19
|
+
rescue Exception => e
|
20
|
+
finish(env: env, request: request, exception: e, silence: silence)
|
21
|
+
raise e
|
22
|
+
end
|
23
|
+
end
|
24
|
+
ensure
|
25
|
+
ActiveSupport::LogSubscriber.flush_all!
|
26
|
+
end
|
27
|
+
|
28
|
+
def conditionally_silence(request)
|
29
|
+
if silence = silence_path?(request)
|
30
|
+
logger.silence do
|
31
|
+
yield silence
|
32
|
+
end
|
33
|
+
else
|
34
|
+
yield silence
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def start(params)
|
43
|
+
LogSanity.reset_fields
|
44
|
+
instrumenter = ActiveSupport::Notifications.instrumenter
|
45
|
+
instrumenter.start 'request.action_dispatch', params
|
46
|
+
end
|
47
|
+
|
48
|
+
def finish(params)
|
49
|
+
instrumenter = ActiveSupport::Notifications.instrumenter
|
50
|
+
instrumenter.finish 'request.action_dispatch', params
|
51
|
+
end
|
52
|
+
|
53
|
+
def silence_path?(request)
|
54
|
+
Rails.application.config.logsanity.silence_paths.any? do |s|
|
55
|
+
case s
|
56
|
+
when Regexp
|
57
|
+
s =~ request.path
|
58
|
+
when String
|
59
|
+
s == request.path
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def logger
|
65
|
+
Rails.logger
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# middleware to catch and sanely handle routing errors without treating them
|
2
|
+
# like all other exceptions (that is, without verbose backtraces and other
|
3
|
+
# such).
|
4
|
+
# intended to be added to the end of the middleware stack (nearest the app).
|
5
|
+
# while built on top of ShowExceptions to reuse its error rendering logic,
|
6
|
+
# does not replace it.
|
7
|
+
|
8
|
+
module LogSanity
|
9
|
+
class RoutingErrorCatcher < ActionDispatch::ShowExceptions
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
request = ActionDispatch::Request.new env
|
13
|
+
_, headers, body = response = @app.call(env)
|
14
|
+
|
15
|
+
if headers['X-Cascade'] == 'pass'
|
16
|
+
body.close if body.respond_to?(:close)
|
17
|
+
raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
|
18
|
+
end
|
19
|
+
|
20
|
+
response
|
21
|
+
rescue ActionController::RoutingError => exception
|
22
|
+
if request.show_exceptions?
|
23
|
+
render_exception(request, exception)
|
24
|
+
else
|
25
|
+
raise exception
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module LogSanity
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
config.logsanity = ActiveSupport::OrderedOptions.new
|
4
|
+
config.logsanity.enabled = false
|
5
|
+
config.logsanity.json_strings = false
|
6
|
+
config.logsanity.silence_paths = []
|
7
|
+
|
8
|
+
initializer "log_sanity.configure" do |app|
|
9
|
+
app.config.log_tags ||= []
|
10
|
+
if app.config.logsanity.enabled
|
11
|
+
orig_formatter = Rails.logger.formatter
|
12
|
+
Rails.logger.formatter = LogSanity::Formatter.new
|
13
|
+
Rails.logger.formatter.string_formatter = orig_formatter unless app.config.logsanity.json_strings
|
14
|
+
|
15
|
+
if defined?(ActionController)
|
16
|
+
require 'action_controller/log_subscriber'
|
17
|
+
ActionController::LogSubscriber.detach_from :action_controller
|
18
|
+
end
|
19
|
+
if defined?(ActionMailer)
|
20
|
+
require 'action_mailer/log_subscriber'
|
21
|
+
ActionMailer::LogSubscriber.detach_from :action_mailer
|
22
|
+
end
|
23
|
+
if defined?(ActionView)
|
24
|
+
require 'action_view/log_subscriber'
|
25
|
+
ActionView::LogSubscriber.detach_from :action_view
|
26
|
+
end
|
27
|
+
if defined?(ActiveJob)
|
28
|
+
require 'active_job/logging'
|
29
|
+
ActiveJob::Logging::LogSubscriber.detach_from :active_job
|
30
|
+
end
|
31
|
+
if defined?(ActiveRecord)
|
32
|
+
if ActiveRecord::Base.logger.debug?
|
33
|
+
Rails.logger.info '[LogSanity] ActiveRecord::Base.logger in debug mode and will still log queries'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
LogSanity::LogSubscriber::ActionController.attach_to :action_controller
|
38
|
+
LogSanity::LogSubscriber::ActionDispatch.attach_to :action_dispatch
|
39
|
+
LogSanity::LogSubscriber::ActionMailer.attach_to :action_mailer
|
40
|
+
LogSanity::LogSubscriber::ActiveJob.attach_to :active_job
|
41
|
+
|
42
|
+
app.middleware.swap Rails::Rack::Logger, LogSanity::RequestLogger
|
43
|
+
|
44
|
+
show_exceptions_app = app.config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
|
45
|
+
app.middleware.use LogSanity::RoutingErrorCatcher, show_exceptions_app
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|