coverband 5.2.5 → 5.2.6.rc.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -22,7 +22,8 @@ module Coverband
22
22
  Coverband.report_coverage
23
23
  # to ensure we track mailer views we now need to report views tracking
24
24
  # at exit as well for rake tasks and background tasks that can trigger email
25
- Coverband.configuration.view_tracker&.report_views_tracked
25
+ Coverband.configuration.view_tracker&.save_report
26
+ Coverband.configuration.translations_tracker&.save_report
26
27
  end
27
28
  end
28
29
  end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "singleton"
5
+
6
+ module Coverband
7
+ module Collectors
8
+ ###
9
+ # This abstract class makes it easy to track any used/unused with timestamp set of usage
10
+ ###
11
+ class AbstractTracker
12
+ REPORT_ROUTE = "/"
13
+ TITLE = "abstract"
14
+
15
+ attr_accessor :target
16
+ attr_reader :logger, :store, :ignore_patterns
17
+
18
+ def initialize(options = {})
19
+ raise NotImplementedError, "#{self.class.name} requires a newer version of Rails" unless self.class.supported_version?
20
+ raise "Coverband: #{self.class.name} initialized before configuration!" if !Coverband.configured? && ENV["COVERBAND_TEST"] == "test"
21
+
22
+ @ignore_patterns = Coverband.configuration.ignore
23
+ @store = options.fetch(:store) { Coverband.configuration.store }
24
+ @logger = options.fetch(:logger) { Coverband.configuration.logger }
25
+ @target = options.fetch(:target) do
26
+ concrete_target
27
+ end
28
+
29
+ @one_time_timestamp = false
30
+
31
+ @logged_keys = Set.new
32
+ @keys_to_record = Set.new
33
+ end
34
+
35
+ def logged_keys
36
+ @logged_keys.to_a
37
+ end
38
+
39
+ def keys_to_record
40
+ @keys_to_record.to_a
41
+ end
42
+
43
+ ###
44
+ # This method is called on every translation usage
45
+ ###
46
+ def track_key(key)
47
+ if key
48
+ if newly_seen_key?(key)
49
+ @logged_keys << key
50
+ @keys_to_record << key if track_key?(key)
51
+ end
52
+ end
53
+ end
54
+
55
+ def used_keys
56
+ redis_store.hgetall(tracker_key)
57
+ end
58
+
59
+ def all_keys
60
+ target.uniq
61
+ end
62
+
63
+ def unused_keys(used_keys = nil)
64
+ recently_used_keys = (used_keys || self.used_keys).keys
65
+ all_keys.reject { |k| recently_used_keys.include?(k.to_s) }
66
+ end
67
+
68
+ def as_json
69
+ used_keys = self.used_keys
70
+ {
71
+ unused_keys: unused_keys(used_keys),
72
+ used_keys: used_keys
73
+ }.to_json
74
+ end
75
+
76
+ def tracking_since
77
+ if (tracking_time = redis_store.get(tracker_time_key))
78
+ Time.at(tracking_time.to_i).iso8601
79
+ else
80
+ "N/A"
81
+ end
82
+ end
83
+
84
+ def reset_recordings
85
+ redis_store.del(tracker_key)
86
+ redis_store.del(tracker_time_key)
87
+ end
88
+
89
+ def clear_key!(key)
90
+ return unless key
91
+ puts "#{tracker_key} key #{key}"
92
+ redis_store.hdel(tracker_key, key)
93
+ @logged_keys.delete(key)
94
+ end
95
+
96
+ def save_report
97
+ redis_store.set(tracker_time_key, Time.now.to_i) unless @one_time_timestamp || tracker_time_key_exists?
98
+ @one_time_timestamp = true
99
+ reported_time = Time.now.to_i
100
+ @keys_to_record.to_a.each do |key|
101
+ redis_store.hset(tracker_key, key.to_s, reported_time)
102
+ end
103
+ @keys_to_record.clear
104
+ rescue => e
105
+ # we don't want to raise errors if Coverband can't reach redis.
106
+ # This is a nice to have not a bring the system down
107
+ logger&.error "Coverband: #{self.class.name} failed to store, error #{e.class.name} info #{e.message}"
108
+ end
109
+
110
+ # This is the basic rails version supported, if there is something more unique over ride in subclass
111
+ def self.supported_version?
112
+ defined?(Rails) && defined?(Rails::VERSION) && Rails::VERSION::STRING.split(".").first.to_i >= 5
113
+ end
114
+
115
+ def route
116
+ self.class::REPORT_ROUTE
117
+ end
118
+
119
+ def title
120
+ self.class::TITLE
121
+ end
122
+
123
+ protected
124
+
125
+ def newly_seen_key?(key)
126
+ !@logged_keys.include?(key)
127
+ end
128
+
129
+ def track_key?(key, options = {})
130
+ @ignore_patterns.none? { |pattern| key.to_s.include?(pattern) }
131
+ end
132
+
133
+ private
134
+
135
+ def concrete_target
136
+ raise "subclass must implement"
137
+ end
138
+
139
+ def redis_store
140
+ store.raw_store
141
+ end
142
+
143
+ def tracker_time_key_exists?
144
+ if defined?(redis_store.exists?)
145
+ redis_store.exists?(tracker_time_key)
146
+ else
147
+ redis_store.exists(tracker_time_key)
148
+ end
149
+ end
150
+
151
+ def tracker_key
152
+ "#{class_key}_tracker"
153
+ end
154
+
155
+ def tracker_time_key
156
+ "#{class_key}_tracker_time"
157
+ end
158
+
159
+ def class_key
160
+ @class_key ||= self.class.name.split("::").last
161
+ end
162
+ end
163
+ end
164
+ end
@@ -8,51 +8,23 @@ module Coverband
8
8
  ###
9
9
  # This class tracks route usage via ActiveSupport::Notifications
10
10
  ###
11
- class RouteTracker
12
- attr_accessor :target
13
- attr_reader :logger, :store, :ignore_patterns
11
+ class RouteTracker < AbstractTracker
12
+ REPORT_ROUTE = "routes_tracker"
13
+ TITLE = "Routes"
14
14
 
15
15
  def initialize(options = {})
16
- raise NotImplementedError, "Route Tracker requires Rails 4 or greater" unless self.class.supported_version?
17
- raise "Coverband: route tracker initialized before configuration!" if !Coverband.configured? && ENV["COVERBAND_TEST"] == "test"
18
-
19
- @ignore_patterns = Coverband.configuration.ignore
20
- @store = options.fetch(:store) { Coverband.configuration.store }
21
- @logger = options.fetch(:logger) { Coverband.configuration.logger }
22
- @target = options.fetch(:target) do
23
- if defined?(Rails.application)
24
- Rails.application.routes.routes.map do |route|
25
- {
26
- controller: route.defaults[:controller],
27
- action: route.defaults[:action],
28
- url_path: route.path.spec.to_s.gsub("(.:format)", ""),
29
- verb: route.verb
30
- }
31
- end
32
- else
33
- []
34
- end
16
+ if Rails&.respond_to?(:version) && Gem::Version.new(Rails.version) >= Gem::Version.new("6.0.0") && Gem::Version.new(Rails.version) < Gem::Version.new("7.1.0")
17
+ require_relative "../utils/rails6_ext"
35
18
  end
36
19
 
37
- @one_time_timestamp = false
38
-
39
- @logged_routes = Set.new
40
- @routes_to_record = Set.new
41
- end
42
-
43
- def logged_routes
44
- @logged_routes.to_a
45
- end
46
-
47
- def routes_to_record
48
- @routes_to_record.to_a
20
+ super
49
21
  end
50
22
 
51
23
  ###
52
24
  # This method is called on every routing call, so we try to reduce method calls
53
25
  # and ensure high performance
54
26
  ###
55
- def track_routes(_name, _start, _finish, _id, payload)
27
+ def track_key(payload)
56
28
  route = if payload[:request]
57
29
  {
58
30
  controller: nil,
@@ -69,104 +41,53 @@ module Coverband
69
41
  }
70
42
  end
71
43
  if route
72
- if newly_seen_route?(route)
73
- @logged_routes << route
74
- @routes_to_record << route if track_route?(route)
44
+ if newly_seen_key?(route)
45
+ @logged_keys << route
46
+ @keys_to_record << route if track_key?(route)
75
47
  end
76
48
  end
77
49
  end
78
50
 
79
- def used_routes
80
- redis_store.hgetall(tracker_key)
81
- end
82
-
83
- def all_routes
84
- target.uniq
51
+ def self.supported_version?
52
+ defined?(Rails) && defined?(Rails::VERSION) && Rails::VERSION::STRING.split(".").first.to_i >= 6
85
53
  end
86
54
 
87
- def unused_routes(used_routes = nil)
88
- recently_used_routes = (used_routes || self.used_routes).keys
55
+ def unused_keys(used_keys = nil)
56
+ recently_used_routes = (used_keys || self.used_keys).keys
89
57
  # NOTE: we match with or without path to handle paths with named params like `/user/:user_id` to used routes filling with all the variable named paths
90
- all_routes.reject { |r| recently_used_routes.include?(r.to_s) || recently_used_routes.include?(r.merge(url_path: nil).to_s) }
91
- end
92
-
93
- def as_json
94
- used_routes = self.used_routes
95
- {
96
- unused_routes: unused_routes(used_routes),
97
- used_routes: used_routes
98
- }.to_json
58
+ all_keys.reject { |r| recently_used_routes.include?(r.to_s) || recently_used_routes.include?(r.merge(url_path: nil).to_s) }
99
59
  end
100
60
 
101
- def tracking_since
102
- if (tracking_time = redis_store.get(tracker_time_key))
103
- Time.at(tracking_time.to_i).iso8601
104
- else
105
- "N/A"
61
+ def railtie!
62
+ ActiveSupport::Notifications.subscribe("start_processing.action_controller") do |name, start, finish, id, payload|
63
+ Coverband.configuration.route_tracker.track_key(payload)
106
64
  end
107
- end
108
-
109
- def reset_recordings
110
- redis_store.del(tracker_key)
111
- redis_store.del(tracker_time_key)
112
- end
113
-
114
- def clear_route!(route)
115
- return unless route
116
65
 
117
- redis_store.hdel(tracker_key, route)
118
- @logged_routes.delete(route)
119
- end
120
-
121
- def report_routes_tracked
122
- redis_store.set(tracker_time_key, Time.now.to_i) unless @one_time_timestamp || tracker_time_key_exists?
123
- @one_time_timestamp = true
124
- reported_time = Time.now.to_i
125
- @routes_to_record.to_a.each do |route|
126
- redis_store.hset(tracker_key, route.to_s, reported_time)
66
+ # NOTE: This event was instrumented in Aug 10th 2022, but didn't make the 7.0.4 release and should be in the next release
67
+ # https://github.com/rails/rails/pull/43755
68
+ # Automatic tracking of redirects isn't avaible before Rails 7.1.0 (currently tested against the 7.1.0.alpha)
69
+ # We could consider back porting or patching a solution that works on previous Rails versions
70
+ ActiveSupport::Notifications.subscribe("redirect.action_dispatch") do |name, start, finish, id, payload|
71
+ Coverband.configuration.route_tracker.track_key(payload)
127
72
  end
128
- @routes_to_record.clear
129
- rescue => e
130
- # we don't want to raise errors if Coverband can't reach redis.
131
- # This is a nice to have not a bring the system down
132
- logger&.error "Coverband: route_tracker failed to store, error #{e.class.name} info #{e.message}"
133
- end
134
-
135
- def self.supported_version?
136
- defined?(Rails) && defined?(Rails::VERSION) && Rails::VERSION::STRING.split(".").first.to_i >= 4
137
- end
138
-
139
- protected
140
-
141
- def newly_seen_route?(route)
142
- !@logged_routes.include?(route)
143
- end
144
-
145
- def track_route?(route, options = {})
146
- @ignore_patterns.none? { |pattern| route.to_s.include?(pattern) }
147
73
  end
148
74
 
149
75
  private
150
76
 
151
- def redis_store
152
- store.raw_store
153
- end
154
-
155
- def tracker_time_key_exists?
156
- if defined?(redis_store.exists?)
157
- redis_store.exists?(tracker_time_key)
77
+ def concrete_target
78
+ if defined?(Rails.application)
79
+ Rails.application.routes.routes.map do |route|
80
+ {
81
+ controller: route.defaults[:controller],
82
+ action: route.defaults[:action],
83
+ url_path: route.path.spec.to_s.gsub("(.:format)", ""),
84
+ verb: route.verb
85
+ }
86
+ end
158
87
  else
159
- redis_store.exists(tracker_time_key)
88
+ []
160
89
  end
161
90
  end
162
-
163
- def tracker_key
164
- "route_tracker_2"
165
- end
166
-
167
- def tracker_time_key
168
- "route_tracker_time"
169
- end
170
91
  end
171
92
  end
172
93
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "singleton"
5
+
6
+ module Coverband
7
+ module Collectors
8
+ module I18n
9
+ module KeyRegistry
10
+ def lookup(locale, key, scope = [], options = {})
11
+ separator = options[:separator] || ::I18n.default_separator
12
+ flat_key = ::I18n.normalize_keys(locale, key, scope, separator).join(separator)
13
+ Coverband.configuration.translations_tracker.track_key(flat_key)
14
+
15
+ super
16
+ end
17
+ end
18
+ end
19
+
20
+ ###
21
+ # This class tracks translation usage via I18n::Backend
22
+ ###
23
+ class TranslationTracker < AbstractTracker
24
+ REPORT_ROUTE = "translations_tracker"
25
+ TITLE = "Translations"
26
+
27
+ def railtie!
28
+ # plugin to i18n
29
+ ::I18n::Backend::Simple.send :include, ::Coverband::Collectors::I18n::KeyRegistry
30
+ end
31
+
32
+ private
33
+
34
+ def concrete_target
35
+ if defined?(Rails.application)
36
+ app_translation_keys = []
37
+ app_translation_files = ::I18n.load_path.select { |f| f.match(/config\/locales/) }
38
+ app_translation_files.each do |file|
39
+ app_translation_keys += flatten_hash(YAML.load_file(file)).keys
40
+ end
41
+ app_translation_keys.uniq
42
+ else
43
+ []
44
+ end
45
+ end
46
+
47
+ def flatten_hash(hash)
48
+ hash.each_with_object({}) do |(k, v), h|
49
+ if v.is_a? Hash
50
+ flatten_hash(v).map do |h_k, h_v|
51
+ h["#{k}.#{h_k}".to_sym] = h_v
52
+ end
53
+ else
54
+ h[k] = v
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -12,45 +12,35 @@ module Coverband
12
12
  # but am now rolling into Coverband
13
13
  # https://github.com/livingsocial/flatfoot
14
14
  ###
15
- class ViewTracker
16
- attr_accessor :target
17
- attr_reader :logger, :roots, :store, :ignore_patterns
15
+ class ViewTracker < AbstractTracker
16
+ attr_reader :roots
18
17
 
19
- def initialize(options = {})
20
- raise NotImplementedError, "View Tracker requires Rails 4 or greater" unless self.class.supported_version?
21
- raise "Coverband: view tracker initialized before configuration!" if !Coverband.configured? && ENV["COVERBAND_TEST"] == "test"
18
+ REPORT_ROUTE = "views_tracker"
19
+ TITLE = "Views"
22
20
 
21
+ def initialize(options = {})
23
22
  @project_directory = File.expand_path(Coverband.configuration.root)
24
- @ignore_patterns = Coverband.configuration.ignore
25
- @store = options.fetch(:store) { Coverband.configuration.store }
26
- @logger = options.fetch(:logger) { Coverband.configuration.logger }
27
- @target = options.fetch(:target) { Dir.glob("#{@project_directory}/app/views/**/*.html.{erb,haml,slim}") }
28
-
29
23
  @roots = options.fetch(:roots) { Coverband.configuration.all_root_patterns }
30
24
  @roots = @roots.split(",") if @roots.is_a?(String)
31
- @one_time_timestamp = false
32
-
33
- @logged_views = Set.new
34
- @views_to_record = Set.new
35
- end
36
25
 
37
- def logged_views
38
- @logged_views.to_a
26
+ super
39
27
  end
40
28
 
41
- def views_to_record
42
- @views_to_record.to_a
29
+ def railtie!
30
+ ActiveSupport::Notifications.subscribe(/render_(template|partial|collection).action_view/) do |name, start, finish, id, payload|
31
+ Coverband.configuration.view_tracker.track_key(payload) unless name.include?("!")
32
+ end
43
33
  end
44
34
 
45
35
  ###
46
36
  # This method is called on every render call, so we try to reduce method calls
47
37
  # and ensure high performance
48
38
  ###
49
- def track_views(_name, _start, _finish, _id, payload)
39
+ def track_key(payload)
50
40
  if (file = payload[:identifier])
51
- if newly_seen_file?(file)
52
- @logged_views << file
53
- @views_to_record << file if track_file?(file)
41
+ if newly_seen_key?(file)
42
+ @logged_keys << file
43
+ @keys_to_record << file if track_file?(file)
54
44
  end
55
45
  end
56
46
 
@@ -61,13 +51,13 @@ module Coverband
61
51
  # http://edgeguides.rubyonrails.org/active_support_instrumentation.html#render_partial-action_view
62
52
  ###
63
53
  return unless (layout_file = payload[:layout])
64
- return unless newly_seen_file?(layout_file)
54
+ return unless newly_seen_key?(layout_file)
65
55
 
66
- @logged_views << layout_file
67
- @views_to_record << layout_file if track_file?(layout_file, layout: true)
56
+ @logged_keys << layout_file
57
+ @keys_to_record << layout_file if track_file?(layout_file, layout: true)
68
58
  end
69
59
 
70
- def used_views
60
+ def used_keys
71
61
  views = redis_store.hgetall(tracker_key)
72
62
  normalized_views = {}
73
63
  views.each_pair do |view, time|
@@ -79,7 +69,7 @@ module Coverband
79
69
  normalized_views
80
70
  end
81
71
 
82
- def all_views
72
+ def all_keys
83
73
  all_views = []
84
74
  target.each do |view|
85
75
  roots.each do |root|
@@ -90,92 +80,35 @@ module Coverband
90
80
  all_views.uniq
91
81
  end
92
82
 
93
- def unused_views(used_views = nil)
94
- recently_used_views = (used_views || self.used_views).keys
95
- unused_views = all_views - recently_used_views
83
+ def unused_keys(used_views = nil)
84
+ recently_used_views = (used_keys || used_keys).keys
85
+ unused_views = all_keys - recently_used_views
96
86
  # since layouts don't include format we count them used if they match with ANY formats
97
87
  unused_views.reject { |view| view.match(/\/layouts\//) && recently_used_views.any? { |used_view| view.include?(used_view) } }
98
88
  end
99
89
 
100
- def as_json
101
- used_views = self.used_views
102
- {
103
- unused_views: unused_views(used_views),
104
- used_views: used_views
105
- }.to_json
106
- end
107
-
108
- def tracking_since
109
- if (tracking_time = redis_store.get(tracker_time_key))
110
- Time.at(tracking_time.to_i).iso8601
111
- else
112
- "N/A"
113
- end
114
- end
115
-
116
- def reset_recordings
117
- redis_store.del(tracker_key)
118
- redis_store.del(tracker_time_key)
119
- end
120
-
121
- def clear_file!(filename)
90
+ def clear_key!(filename)
122
91
  return unless filename
123
92
 
124
93
  filename = "#{@project_directory}/#{filename}"
125
94
  redis_store.hdel(tracker_key, filename)
126
- @logged_views.delete(filename)
127
- end
128
-
129
- def report_views_tracked
130
- redis_store.set(tracker_time_key, Time.now.to_i) unless @one_time_timestamp || tracker_time_key_exists?
131
- @one_time_timestamp = true
132
- reported_time = Time.now.to_i
133
- @views_to_record.to_a.each do |file|
134
- redis_store.hset(tracker_key, file, reported_time)
135
- end
136
- @views_to_record.clear
137
- rescue => e
138
- # we don't want to raise errors if Coverband can't reach redis.
139
- # This is a nice to have not a bring the system down
140
- logger&.error "Coverband: view_tracker failed to store, error #{e.class.name}"
141
- end
142
-
143
- def self.supported_version?
144
- defined?(Rails) && defined?(Rails::VERSION) && Rails::VERSION::STRING.split(".").first.to_i >= 4
95
+ @logged_keys.delete(filename)
145
96
  end
146
97
 
147
- protected
148
-
149
- def newly_seen_file?(file)
150
- !@logged_views.include?(file)
151
- end
98
+ private
152
99
 
153
100
  def track_file?(file, options = {})
154
101
  (file.start_with?(@project_directory) || options[:layout]) &&
155
102
  @ignore_patterns.none? { |pattern| file.include?(pattern) }
156
103
  end
157
104
 
158
- private
159
-
160
- def redis_store
161
- store.raw_store
162
- end
163
-
164
- def tracker_time_key_exists?
165
- if defined?(redis_store.exists?)
166
- redis_store.exists?(tracker_time_key)
105
+ def concrete_target
106
+ if defined?(Rails.application)
107
+ Dir.glob("#{@project_directory}/app/views/**/*.html.{erb,haml,slim}")
167
108
  else
168
- redis_store.exists(tracker_time_key)
109
+ []
169
110
  end
170
111
  end
171
-
172
- def tracker_key
173
- "render_tracker_2"
174
- end
175
-
176
- def tracker_time_key
177
- "render_tracker_time"
178
- end
179
112
  end
180
113
  end
181
114
  end
@@ -6,7 +6,7 @@ module Coverband
6
6
  # This class extends view tracker to support web service reporting
7
7
  ###
8
8
  class ViewTrackerService < ViewTracker
9
- def report_views_tracked
9
+ def save_report
10
10
  reported_time = Time.now.to_i
11
11
  if @views_to_record.any?
12
12
  relative_views = @views_to_record.map! do |view|
@@ -9,7 +9,9 @@ module Coverband
9
9
  :test_env, :web_enable_clear, :gem_details, :web_debug, :report_on_exit,
10
10
  :simulate_oneshot_lines_coverage,
11
11
  :view_tracker, :defer_eager_loading_data,
12
- :track_routes, :route_tracker
12
+ :track_routes, :route_tracker,
13
+ :track_translations, :translations_tracker,
14
+ :trackers
13
15
  attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id,
14
16
  :s3_secret_access_key, :password, :api_key, :service_url, :coverband_timeout, :service_dev_mode,
15
17
  :service_test_mode, :process_type, :track_views, :redis_url,
@@ -70,6 +72,8 @@ module Coverband
70
72
  @view_tracker = nil
71
73
  @track_routes = false
72
74
  @route_tracker = nil
75
+ @track_translations = false
76
+ @translations_tracker = nil
73
77
  @web_debug = false
74
78
  @report_on_exit = true
75
79
  @use_oneshot_lines_coverage = ENV["ONESHOT"] || false
@@ -92,6 +96,8 @@ module Coverband
92
96
  @redis_ttl = 2_592_000 # in seconds. Default is 30 days.
93
97
  @reporting_wiggle = nil
94
98
 
99
+ @trackers = []
100
+
95
101
  # TODO: these are deprecated
96
102
  @s3_region = nil
97
103
  @s3_bucket = nil
@@ -101,6 +107,31 @@ module Coverband
101
107
  @gem_details = false
102
108
  end
103
109
 
110
+ def railtie!
111
+ if Coverband.configuration.track_routes
112
+ Coverband.configuration.route_tracker = Coverband::Collectors::RouteTracker.new
113
+ trackers << Coverband.configuration.route_tracker
114
+ end
115
+
116
+ if Coverband.configuration.track_translations
117
+ Coverband.configuration.translations_tracker = Coverband::Collectors::TranslationTracker.new
118
+ trackers << Coverband.configuration.translations_tracker
119
+ end
120
+
121
+ if Coverband.configuration.track_views
122
+ Coverband.configuration.view_tracker = if Coverband.coverband_service?
123
+ Coverband::Collectors::ViewTrackerService.new
124
+ else
125
+ Coverband::Collectors::ViewTracker.new
126
+ end
127
+ trackers << Coverband.configuration.view_tracker
128
+ end
129
+ trackers.each { |tracker| tracker.railtie! }
130
+ rescue Redis::CannotConnectError => error
131
+ Coverband.configuration.logger.info "Redis is not available (#{error}), Coverband not configured"
132
+ Coverband.configuration.logger.info "If this is a setup task like assets:precompile feel free to ignore"
133
+ end
134
+
104
135
  def logger
105
136
  @logger ||= if defined?(Rails.logger) && Rails.logger
106
137
  Rails.logger