paul_bunyan 1.0.0

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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.travis.yml +9 -0
  4. data/Dockerfile +13 -0
  5. data/Gemfile +6 -0
  6. data/Guardfile +16 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +51 -0
  9. data/README.rdoc +3 -0
  10. data/Rakefile +19 -0
  11. data/bin/logging_demo +17 -0
  12. data/build.sh +7 -0
  13. data/docker-compose.yml +4 -0
  14. data/lib/paul_bunyan.rb +70 -0
  15. data/lib/paul_bunyan/json_formatter.rb +122 -0
  16. data/lib/paul_bunyan/level.rb +28 -0
  17. data/lib/paul_bunyan/log_relayer.rb +148 -0
  18. data/lib/paul_bunyan/rails_ext.rb +7 -0
  19. data/lib/paul_bunyan/rails_ext/instrumentation.rb +41 -0
  20. data/lib/paul_bunyan/rails_ext/rack_logger.rb +24 -0
  21. data/lib/paul_bunyan/railtie.rb +75 -0
  22. data/lib/paul_bunyan/railtie/log_subscriber.rb +182 -0
  23. data/lib/paul_bunyan/text_formatter.rb +11 -0
  24. data/lib/paul_bunyan/version.rb +3 -0
  25. data/lib/tasks/paul_bunyan_tasks.rake +4 -0
  26. data/paul_bunyan.gemspec +30 -0
  27. data/spec/dummy/Rakefile +6 -0
  28. data/spec/dummy/app/assets/images/.keep +0 -0
  29. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  30. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  31. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  32. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  33. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  34. data/spec/dummy/app/mailers/.keep +0 -0
  35. data/spec/dummy/app/models/.keep +0 -0
  36. data/spec/dummy/app/models/concerns/.keep +0 -0
  37. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  38. data/spec/dummy/bin/bundle +3 -0
  39. data/spec/dummy/bin/rails +4 -0
  40. data/spec/dummy/bin/rake +4 -0
  41. data/spec/dummy/bin/setup +29 -0
  42. data/spec/dummy/config.ru +4 -0
  43. data/spec/dummy/config/application.rb +28 -0
  44. data/spec/dummy/config/boot.rb +5 -0
  45. data/spec/dummy/config/database.yml +25 -0
  46. data/spec/dummy/config/environment.rb +5 -0
  47. data/spec/dummy/config/environments/development.rb +41 -0
  48. data/spec/dummy/config/environments/production.rb +79 -0
  49. data/spec/dummy/config/environments/test.rb +42 -0
  50. data/spec/dummy/config/initializers/assets.rb +11 -0
  51. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  52. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  53. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  54. data/spec/dummy/config/initializers/inflections.rb +16 -0
  55. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  56. data/spec/dummy/config/initializers/secret_token.rb +1 -0
  57. data/spec/dummy/config/initializers/session_store.rb +3 -0
  58. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  59. data/spec/dummy/config/locales/en.yml +23 -0
  60. data/spec/dummy/config/routes.rb +56 -0
  61. data/spec/dummy/config/secrets.yml +22 -0
  62. data/spec/dummy/lib/assets/.keep +0 -0
  63. data/spec/dummy/log/.keep +0 -0
  64. data/spec/dummy/public/404.html +67 -0
  65. data/spec/dummy/public/422.html +67 -0
  66. data/spec/dummy/public/500.html +66 -0
  67. data/spec/dummy/public/favicon.ico +0 -0
  68. data/spec/gemfiles/40.gemfile +5 -0
  69. data/spec/gemfiles/40.gemfile.lock +137 -0
  70. data/spec/gemfiles/41.gemfile +5 -0
  71. data/spec/gemfiles/41.gemfile.lock +142 -0
  72. data/spec/gemfiles/42.gemfile +5 -0
  73. data/spec/gemfiles/42.gemfile.lock +167 -0
  74. data/spec/lib/paul_bunyan/json_formatter_spec.rb +237 -0
  75. data/spec/lib/paul_bunyan/level_spec.rb +78 -0
  76. data/spec/lib/paul_bunyan/log_relayer_spec.rb +333 -0
  77. data/spec/lib/paul_bunyan/rails_ext/instrumentation_spec.rb +81 -0
  78. data/spec/lib/paul_bunyan/railtie/log_subscriber_spec.rb +304 -0
  79. data/spec/lib/paul_bunyan/railtie_spec.rb +37 -0
  80. data/spec/lib/paul_bunyan_spec.rb +137 -0
  81. data/spec/spec_helper.rb +24 -0
  82. data/spec/support/notification_helpers.rb +22 -0
  83. metadata +303 -0
@@ -0,0 +1,148 @@
1
+ require 'forwardable'
2
+ require 'set'
3
+
4
+ module PaulBunyan
5
+ class LogRelayer
6
+ extend Forwardable
7
+
8
+ # delegate non-relayed methods to the primary logger
9
+ DELEGATED_METHODS = %i(
10
+ progname progname=
11
+ level level=
12
+ sev_threshold sev_threshold=
13
+
14
+ formatter formatter=
15
+ datetime_format datetime_format=
16
+
17
+ close
18
+
19
+ debug? info? warn? error? fatal?
20
+ )
21
+ delegate DELEGATED_METHODS => :primary_logger
22
+
23
+ include Logger::Severity
24
+
25
+ attr_reader :loggers
26
+
27
+ def primary_logger
28
+ loggers[0]
29
+ end
30
+
31
+ def secondary_loggers
32
+ loggers[1..-1] || []
33
+ end
34
+
35
+ def add_logger(logger)
36
+ loggers.push(logger)
37
+ logger
38
+ end
39
+
40
+ def remove_logger(logger)
41
+ loggers.delete(logger)
42
+ end
43
+
44
+ def initialize
45
+ @loggers = []
46
+ end
47
+
48
+ def add(severity, message = nil, progname = nil, &block)
49
+ block = memoized_block(&block) if block
50
+ loggers.reduce(true) do |memo, logger|
51
+ logger.add(severity, message, progname, &block) && memo
52
+ end
53
+ end
54
+ alias_method :log, :add
55
+
56
+ def <<(msg)
57
+ loggers.reduce(nil) do |memo, logger|
58
+ n = logger << msg
59
+
60
+ # this would be simpler with an array, but would generate unnecessary garbage
61
+ if memo.nil? || n.nil?
62
+ memo || n
63
+ else
64
+ memo < n ? memo : n
65
+ end
66
+ end
67
+ end
68
+
69
+ def debug(progname = nil, &block)
70
+ add(DEBUG, nil, progname, &block)
71
+ end
72
+
73
+ def info(progname = nil, &block)
74
+ add(INFO, nil, progname, &block)
75
+ end
76
+
77
+ def warn(progname = nil, &block)
78
+ add(WARN, nil, progname, &block)
79
+ end
80
+
81
+ def error(progname = nil, &block)
82
+ add(ERROR, nil, progname, &block)
83
+ end
84
+
85
+ def fatal(progname = nil, &block)
86
+ add(FATAL, nil, progname, &block)
87
+ end
88
+
89
+ def unknown(progname = nil, &block)
90
+ add(UNKNOWN, nil, progname, &block)
91
+ end
92
+
93
+ def level
94
+ logger = loggers.min { |a, b| a.level <=> b.level }
95
+ logger.nil? ? DEBUG : logger.level
96
+ end
97
+
98
+ module TaggedRelayer
99
+ def current_tags
100
+ tags = loggers.each_with_object(Set.new) do |logger, set|
101
+ set.merge(logger.current_tags) if logger.respond_to?(:current_tags)
102
+ end
103
+ tags.to_a
104
+ end
105
+
106
+ def push_tags(*tags)
107
+ tags.flatten.reject(&:blank?).tap do |new_tags|
108
+ loggers.each { |logger| logger.push_tags(*new_tags) if logger.respond_to?(:push_tags) }
109
+ end
110
+ end
111
+
112
+ def pop_tags(size = 1)
113
+ loggers.each { |logger| logger.pop_tags(size) if logger.respond_to?(:pop_tags) }
114
+ nil
115
+ end
116
+
117
+ def clear_tags!
118
+ loggers.each { |logger| logger.clear_tags! if logger.respond_to?(:clear_tags!) }
119
+ nil
120
+ end
121
+
122
+ def flush
123
+ loggers.each { |logger| logger.flush if logger.respond_to?(:flush) }
124
+ nil
125
+ end
126
+
127
+ def tagged(*tags)
128
+ new_tags = push_tags(*tags)
129
+ yield self
130
+ ensure
131
+ pop_tags(new_tags.size)
132
+ end
133
+ end
134
+ include TaggedRelayer
135
+
136
+ private
137
+
138
+ def memoized_block(&block)
139
+ called = false
140
+ result = nil
141
+ proc do
142
+ next result if called
143
+ called = true
144
+ result = block.call
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,7 @@
1
+ module PaulBunyan
2
+ module RailsExt
3
+ end
4
+ end
5
+
6
+ require 'paul_bunyan/rails_ext/rack_logger'
7
+ require 'paul_bunyan/rails_ext/instrumentation'
@@ -0,0 +1,41 @@
1
+ require 'action_controller/metal/instrumentation'
2
+
3
+ module ActionController
4
+ module Instrumentation
5
+ def process_action(*args)
6
+ raw_payload = base_payload.merge(custom_payload)
7
+
8
+ ActiveSupport::Notifications.instrument('start_processing.action_controller', raw_payload.dup)
9
+
10
+ ActiveSupport::Notifications.instrument('process_action.action_controller', raw_payload) do |payload|
11
+ begin
12
+ result = super
13
+ payload[:status] = response.status
14
+ result
15
+ ensure
16
+ append_info_to_payload(payload)
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def base_payload
24
+ {
25
+ controller: self.class.name,
26
+ action: self.action_name,
27
+ params: request.filtered_parameters,
28
+ format: request.format.try(:ref),
29
+ method: request.request_method,
30
+ path: (request.fullpath rescue 'unknown'),
31
+ }
32
+ end
33
+
34
+ def custom_payload
35
+ {
36
+ request_id: request.env['action_dispatch.request_id'],
37
+ ip: request.ip,
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ require 'rails/rack/logger'
2
+
3
+ module Rails
4
+ module Rack
5
+ class Logger
6
+
7
+ # This was copied directly from the rails source and had
8
+ # the logging lines removed. N.B. this may break in future
9
+ # versions of rails.
10
+ def call_app(request, env)
11
+ instrumenter = ActiveSupport::Notifications.instrumenter
12
+ instrumenter.start 'request.action_dispatch', request: request
13
+ resp = @app.call(env)
14
+ resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
15
+ resp
16
+ rescue
17
+ finish(request)
18
+ raise
19
+ ensure
20
+ ActiveSupport::LogSubscriber.flush_all!
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,75 @@
1
+ require 'paul_bunyan/rails_ext'
2
+ require 'paul_bunyan/railtie/log_subscriber'
3
+ require 'action_controller/log_subscriber'
4
+ require 'action_view/log_subscriber'
5
+
6
+ module PaulBunyan
7
+ class Railtie < ::Rails::Railtie
8
+ DEFAULT_LOGGERS = [ActionController::LogSubscriber, ActionView::LogSubscriber].freeze
9
+
10
+ def self.activesupport_formatter
11
+ ActiveSupport::Logger::SimpleFormatter.new.tap do |formatter|
12
+ formatter.extend ActiveSupport::TaggedLogging::Formatter
13
+ end
14
+ end
15
+
16
+ def self.development_or_test?
17
+ Rails.env.development? || Rails.env.test?
18
+ end
19
+
20
+ # Set up the config and some defaults
21
+ config.logging = ActiveSupport::OrderedOptions.new
22
+ config.logging.override_location = !Rails.env.test?
23
+ config.logging.formatter = (development_or_test? ? activesupport_formatter : JSONFormatter.new)
24
+ config.logging.handle_request_logging = !development_or_test?
25
+
26
+ # hook our initializer in before the rails logging initializer
27
+ initializer 'initalize_logger.logging', group: :all, before: :initialize_logger do |app|
28
+ logging_config = config.logging
29
+
30
+ new_logger = PaulBunyan.add_logger(ActiveSupport::Logger.new(log_target(app.config)))
31
+ new_logger.level = PaulBunyan::Level.coerce_level(ENV['LOG_LEVEL'] || ::Rails.application.config.log_level || 'INFO')
32
+ new_logger.formatter = logging_config.formatter
33
+
34
+ if logging_config.handle_request_logging
35
+ unsubscribe_default_log_subscribers
36
+ LogSubscriber.subscribe_to_events
37
+ end
38
+
39
+ Rails.logger = PaulBunyan.logger
40
+ end
41
+
42
+ def conditionally_unsubscribe(listener)
43
+ delegate = listener.instance_variable_get(:@delegate)
44
+ ActiveSupport::Notifications.unsubscribe(listener) if DEFAULT_LOGGERS.include?(delegate.class)
45
+ end
46
+
47
+ def file_target(app_config)
48
+ path = app_config.paths['log'].first
49
+ path_dir = File.dirname(path)
50
+ FileUtils.mkdir_p(path_dir) unless File.exist?(path_dir)
51
+
52
+ output = File.open(path, 'a')
53
+ output.binmode
54
+ output.sync = app_config.autoflush_log
55
+ output
56
+ end
57
+
58
+ def log_target(app_config)
59
+ config.logging.override_location ? stream_target : file_target(app_config)
60
+ end
61
+
62
+ def stream_target
63
+ STDOUT.sync = true
64
+ STDOUT
65
+ end
66
+
67
+ def unsubscribe_default_log_subscribers
68
+ LogSubscriber.event_patterns.each do |pattern|
69
+ ActiveSupport::Notifications.notifier.listeners_for(pattern).each do |listener|
70
+ conditionally_unsubscribe(listener)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,182 @@
1
+ require 'set'
2
+ require 'active_support/log_subscriber'
3
+ require 'action_controller/log_subscriber'
4
+ require 'action_view/log_subscriber'
5
+ require 'request_store'
6
+
7
+ module PaulBunyan
8
+ INTERNAL_PARAMS = ActionController::LogSubscriber::INTERNAL_PARAMS
9
+ VIEWS_PATTERN = ActionView::LogSubscriber::VIEWS_PATTERN
10
+
11
+ FileTransfer = Struct.new(:path, :transfer_time)
12
+ RenderedTemplate = Struct.new(:path, :runtime, :layout)
13
+ RequestAggregator = Struct.new(
14
+ :method,
15
+ :controller,
16
+ :action,
17
+ :format,
18
+ :path,
19
+ :request_id,
20
+ :ip,
21
+ :status,
22
+ :view_runtime,
23
+ :db_runtime,
24
+ :params,
25
+ :halting_filter,
26
+ :sent_file,
27
+ :redirect_location,
28
+ :sent_data,
29
+ :view,
30
+ :partials
31
+ )
32
+
33
+ class LogSubscriber < ActiveSupport::LogSubscriber
34
+
35
+ @action_controller_events = Set.new
36
+ @action_view_events = Set.new
37
+
38
+ class << self
39
+ attr_reader :action_controller_events, :action_view_events
40
+
41
+ # Register a new event for the the specified namespace this
42
+ # subscriber should subscribe to.
43
+ #
44
+ # @param event_name [Symbol] the name of the event we'll subscribe to
45
+ %w{controller view}.each do |namespace_part|
46
+ namespace = "action_#{namespace_part}"
47
+ define_method "#{namespace}_event" do |event_name|
48
+ send("#{namespace}_events") << event_name
49
+ end
50
+ end
51
+ end
52
+
53
+ # Build an array of event patterns from the action_controller_events
54
+ # set for use in finding subscriptions we should add and ones we should
55
+ # remove from the default subscribers
56
+ def self.event_patterns
57
+ action_controller_events.map{ |event| "#{event}.action_controller" } +
58
+ action_view_events.map { |event| "#{event}.action_view" }
59
+ end
60
+
61
+ # Subscribe to the events we've registered using action_controller_event
62
+ def self.subscribe_to_events(notifier = ActiveSupport::Notifications)
63
+ subscriber = new
64
+ notifier = notifier
65
+
66
+ subscribers << subscriber
67
+
68
+ event_patterns.each do |pattern|
69
+ subscriber.patterns << pattern
70
+ notifier.subscribe(pattern, subscriber)
71
+ end
72
+ end
73
+
74
+ if Rails.version.start_with?('4.0')
75
+ attr_reader :patterns
76
+
77
+ def initialize
78
+ super
79
+ @patterns ||= []
80
+ end
81
+ end
82
+
83
+ # Handle the start_processing event in the action_controller namespace
84
+ #
85
+ # We're only registering for this event so the default
86
+ # log subscribe gets booted off of it :-)
87
+ action_controller_event def start_processing(event)
88
+ return
89
+ end
90
+
91
+ # Handle the process_action event in the action_controller namespace
92
+ #
93
+ # We're using this to capture the vast majority of our info that goes into
94
+ # the log line
95
+ #
96
+ # @param event [ActiveSupport::Notifications::Event]
97
+ ACTION_PAYLOAD_KEYS = [:method, :controller, :action, :format, :path, :request_id, :ip, :status, :view_runtime, :db_runtime]
98
+ action_controller_event def process_action(event)
99
+ payload = event.payload
100
+ ACTION_PAYLOAD_KEYS.each do |attr|
101
+ aggregator[attr] = payload[attr]
102
+ end
103
+ aggregator.params = payload[:params].except(*INTERNAL_PARAMS)
104
+
105
+ logger.info { aggregator_without_nils }
106
+ end
107
+
108
+ action_controller_event def halted_callback(event)
109
+ aggregator.halting_filter = event.payload[:filter].inspect
110
+ end
111
+
112
+ action_controller_event def send_file(event)
113
+ payload = event.payload
114
+ aggregator.sent_file = FileTransfer.new(payload[:path], event.duration)
115
+ end
116
+
117
+ action_controller_event def redirect_to(event)
118
+ aggregator.redirect_location = event.payload[:location]
119
+ end
120
+
121
+ action_controller_event def send_data(event)
122
+ payload = event.payload
123
+ aggregator.sent_data = FileTransfer.new(payload[:filename], event.duration)
124
+ end
125
+
126
+ action_view_event def render_template(event)
127
+ aggregator.view = extract_render_data_from(event)
128
+ end
129
+
130
+ action_view_event def render_partial(event)
131
+ aggregator.partials ||= []
132
+ aggregator.partials << extract_render_data_from(event)
133
+ end
134
+ alias :render_collection :render_partial
135
+ action_view_event :render_collection
136
+
137
+ def logger
138
+ PaulBunyan.logger
139
+ end
140
+
141
+ private
142
+
143
+ def aggregator
144
+ RequestStore[:logging_request_aggregator] ||= RequestAggregator.new
145
+ end
146
+
147
+ def aggregator_without_nils
148
+ struct_without_nils(aggregator).inject({}) { |data, (key, value)|
149
+ if Struct === value
150
+ new_data = {key => struct_without_nils(value)}
151
+ elsif Array === value
152
+ new_data = {key => value.map{ |v| struct_without_nils(v) }}
153
+ else
154
+ new_data = {key => value}
155
+ end
156
+ data.merge(new_data)
157
+ }
158
+ end
159
+
160
+ def clean_view_path(path)
161
+ return nil if path.nil?
162
+ path.sub(rails_root, '').sub(VIEWS_PATTERN, '')
163
+ end
164
+
165
+ def extract_render_data_from(event)
166
+ payload = event.payload
167
+ RenderedTemplate.new(
168
+ clean_view_path(payload[:identifier]),
169
+ event.duration,
170
+ clean_view_path(payload[:layout])
171
+ )
172
+ end
173
+
174
+ def rails_root
175
+ @rails_root ||= "#{Rails.root}/"
176
+ end
177
+
178
+ def struct_without_nils(struct)
179
+ struct.to_h.reject{ |_, value| value.nil? }
180
+ end
181
+ end
182
+ end