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.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.travis.yml +9 -0
- data/Dockerfile +13 -0
- data/Gemfile +6 -0
- data/Guardfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +51 -0
- data/README.rdoc +3 -0
- data/Rakefile +19 -0
- data/bin/logging_demo +17 -0
- data/build.sh +7 -0
- data/docker-compose.yml +4 -0
- data/lib/paul_bunyan.rb +70 -0
- data/lib/paul_bunyan/json_formatter.rb +122 -0
- data/lib/paul_bunyan/level.rb +28 -0
- data/lib/paul_bunyan/log_relayer.rb +148 -0
- data/lib/paul_bunyan/rails_ext.rb +7 -0
- data/lib/paul_bunyan/rails_ext/instrumentation.rb +41 -0
- data/lib/paul_bunyan/rails_ext/rack_logger.rb +24 -0
- data/lib/paul_bunyan/railtie.rb +75 -0
- data/lib/paul_bunyan/railtie/log_subscriber.rb +182 -0
- data/lib/paul_bunyan/text_formatter.rb +11 -0
- data/lib/paul_bunyan/version.rb +3 -0
- data/lib/tasks/paul_bunyan_tasks.rake +4 -0
- data/paul_bunyan.gemspec +30 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +28 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/secret_token.rb +1 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/gemfiles/40.gemfile +5 -0
- data/spec/gemfiles/40.gemfile.lock +137 -0
- data/spec/gemfiles/41.gemfile +5 -0
- data/spec/gemfiles/41.gemfile.lock +142 -0
- data/spec/gemfiles/42.gemfile +5 -0
- data/spec/gemfiles/42.gemfile.lock +167 -0
- data/spec/lib/paul_bunyan/json_formatter_spec.rb +237 -0
- data/spec/lib/paul_bunyan/level_spec.rb +78 -0
- data/spec/lib/paul_bunyan/log_relayer_spec.rb +333 -0
- data/spec/lib/paul_bunyan/rails_ext/instrumentation_spec.rb +81 -0
- data/spec/lib/paul_bunyan/railtie/log_subscriber_spec.rb +304 -0
- data/spec/lib/paul_bunyan/railtie_spec.rb +37 -0
- data/spec/lib/paul_bunyan_spec.rb +137 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/notification_helpers.rb +22 -0
- 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,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
|