log_sanity 0.2.5
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.
- 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
|