paul_bunyan 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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