log_sanity 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/Gemfile +15 -0
  4. data/Gemfile.lock +141 -0
  5. data/LICENSE +20 -0
  6. data/README.md +180 -0
  7. data/Rakefile +34 -0
  8. data/lib/log_sanity.rb +36 -0
  9. data/lib/log_sanity/extensions/action_controller_helper.rb +12 -0
  10. data/lib/log_sanity/extensions/active_support_subscriber.rb +25 -0
  11. data/lib/log_sanity/formatter.rb +45 -0
  12. data/lib/log_sanity/log_subscribers/action_controller.rb +56 -0
  13. data/lib/log_sanity/log_subscribers/action_dispatch.rb +63 -0
  14. data/lib/log_sanity/log_subscribers/action_mailer.rb +27 -0
  15. data/lib/log_sanity/log_subscribers/active_job.rb +69 -0
  16. data/lib/log_sanity/log_subscribers/base.rb +10 -0
  17. data/lib/log_sanity/middleware/request_logger.rb +69 -0
  18. data/lib/log_sanity/middleware/routing_error_catcher.rb +30 -0
  19. data/lib/log_sanity/railtie.rb +50 -0
  20. data/lib/log_sanity/version.rb +3 -0
  21. data/lib/tasks/log_sanity_tasks.rake +4 -0
  22. data/log_sanity.gemspec +21 -0
  23. data/test/dummy/README.rdoc +28 -0
  24. data/test/dummy/Rakefile +6 -0
  25. data/test/dummy/app/assets/config/manifest.js +1 -0
  26. data/test/dummy/app/assets/images/.keep +0 -0
  27. data/test/dummy/app/assets/javascripts/application.js +13 -0
  28. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  29. data/test/dummy/app/controllers/application_controller.rb +5 -0
  30. data/test/dummy/app/controllers/concerns/.keep +0 -0
  31. data/test/dummy/app/helpers/application_helper.rb +2 -0
  32. data/test/dummy/app/mailers/.keep +0 -0
  33. data/test/dummy/app/models/.keep +0 -0
  34. data/test/dummy/app/models/concerns/.keep +0 -0
  35. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  36. data/test/dummy/bin/bundle +3 -0
  37. data/test/dummy/bin/rails +4 -0
  38. data/test/dummy/bin/rake +4 -0
  39. data/test/dummy/bin/setup +29 -0
  40. data/test/dummy/config.ru +4 -0
  41. data/test/dummy/config/application.rb +29 -0
  42. data/test/dummy/config/boot.rb +5 -0
  43. data/test/dummy/config/environment.rb +5 -0
  44. data/test/dummy/config/environments/development.rb +38 -0
  45. data/test/dummy/config/environments/production.rb +76 -0
  46. data/test/dummy/config/environments/test.rb +42 -0
  47. data/test/dummy/config/initializers/assets.rb +11 -0
  48. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  50. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  51. data/test/dummy/config/initializers/inflections.rb +16 -0
  52. data/test/dummy/config/initializers/mime_types.rb +4 -0
  53. data/test/dummy/config/initializers/session_store.rb +3 -0
  54. data/test/dummy/config/initializers/wrap_parameters.rb +9 -0
  55. data/test/dummy/config/locales/en.yml +23 -0
  56. data/test/dummy/config/routes.rb +56 -0
  57. data/test/dummy/config/secrets.yml +22 -0
  58. data/test/dummy/lib/assets/.keep +0 -0
  59. data/test/dummy/log/.keep +0 -0
  60. data/test/dummy/public/404.html +67 -0
  61. data/test/dummy/public/422.html +67 -0
  62. data/test/dummy/public/500.html +66 -0
  63. data/test/dummy/public/favicon.ico +0 -0
  64. data/test/log_sanity_test.rb +7 -0
  65. data/test/test_helper.rb +19 -0
  66. metadata +171 -0
@@ -0,0 +1,12 @@
1
+ module LogSanity
2
+ module Extensions
3
+ module ActionControllerHelper
4
+ extend ActiveSupport::Concern
5
+
6
+ def log_field(key, val)
7
+ LogSanity.log key, val
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -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,10 @@
1
+ module LogSanity
2
+ module LogSubscriber
3
+ class Base < ::ActiveSupport::LogSubscriber
4
+
5
+ private
6
+ delegate :fields, :log, to: LogSanity
7
+
8
+ end
9
+ end
10
+ 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