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