rails_semantic_logger 4.19.0 → 5.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +55 -98
  3. data/Rakefile +5 -2
  4. data/lib/rails_semantic_logger/action_controller/log_subscriber.rb +86 -16
  5. data/lib/rails_semantic_logger/action_mailer/log_subscriber.rb +36 -22
  6. data/lib/rails_semantic_logger/action_view/log_subscriber.rb +74 -40
  7. data/lib/rails_semantic_logger/active_job/log_subscriber.rb +218 -7
  8. data/lib/rails_semantic_logger/active_record/log_subscriber.rb +62 -164
  9. data/lib/rails_semantic_logger/appenders.rb +91 -0
  10. data/lib/rails_semantic_logger/engine.rb +51 -38
  11. data/lib/rails_semantic_logger/extensions/action_cable/tagged_logger_proxy.rb +44 -3
  12. data/lib/rails_semantic_logger/extensions/action_dispatch/debug_exceptions.rb +5 -14
  13. data/lib/rails_semantic_logger/extensions/active_job/logging.rb +2 -2
  14. data/lib/rails_semantic_logger/extensions/active_model_serializers/logging.rb +2 -2
  15. data/lib/rails_semantic_logger/extensions/active_support/logger.rb +24 -15
  16. data/lib/rails_semantic_logger/extensions/rails/server.rb +1 -1
  17. data/lib/rails_semantic_logger/extensions/sidekiq/sidekiq.rb +33 -16
  18. data/lib/rails_semantic_logger/options.rb +175 -18
  19. data/lib/rails_semantic_logger/rack/logger.rb +6 -13
  20. data/lib/rails_semantic_logger/sidekiq/defaults.rb +4 -2
  21. data/lib/rails_semantic_logger/sidekiq/job_logger.rb +41 -15
  22. data/lib/rails_semantic_logger/solid_queue/log_subscriber.rb +179 -0
  23. data/lib/rails_semantic_logger/version.rb +1 -1
  24. data/lib/rails_semantic_logger.rb +81 -26
  25. metadata +15 -16
  26. data/lib/rails_semantic_logger/delayed_job/plugin.rb +0 -11
  27. data/lib/rails_semantic_logger/extensions/active_support/log_subscriber.rb +0 -13
  28. data/lib/rails_semantic_logger/extensions/rack/server.rb +0 -12
  29. data/lib/rails_semantic_logger/extensions/rackup/server.rb +0 -12
@@ -1,27 +1,36 @@
1
1
  require "active_support/logger"
2
2
 
3
3
  module ActiveSupport
4
- # More hacks to try and stop Rails from being it's own worst enemy.
4
+ # More hacks to try and stop Rails from being its own worst enemy.
5
5
  class Logger
6
6
  class << self
7
+ # Keep a handle on the genuine constructor so that callers which supply a
8
+ # real log destination still get a real logger (see #new below).
9
+ alias _semantic_logger_original_new new
10
+
7
11
  undef :logger_outputs_to?
8
12
 
9
- # Prevent broadcasting since SemanticLogger already supports multiple loggers
10
- if method_defined?(:broadcast)
11
- undef :broadcast
12
- def broadcast(_logger)
13
- Module.new
14
- end
13
+ # Prevent Rails from trying to merge/broadcast loggers (e.g. ActiveRecord's
14
+ # `console` hook and `rails server`'s log_to_stdout). SemanticLogger already
15
+ # multiplexes through its own appenders, and SemanticLogger::Logger does not
16
+ # implement #broadcast_to, so the merge path would otherwise raise.
17
+ def logger_outputs_to?(*_args)
18
+ true
15
19
  end
16
- end
17
20
 
18
- # Prevent Console from trying to merge loggers
19
- def self.logger_outputs_to?(*_args)
20
- true
21
- end
22
-
23
- def self.new(*_args, **_kwargs)
24
- SemanticLogger[self]
21
+ # Historically every `ActiveSupport::Logger.new(...)` call was redirected to
22
+ # SemanticLogger, silently discarding the requested destination. That broke
23
+ # third-party callers such as Webpacker's `ActiveSupport::Logger.new(STDOUT)`,
24
+ # whose output never reached STDOUT (issue #141). Only redirect to
25
+ # SemanticLogger when no destination is supplied; otherwise honor the caller
26
+ # and build a genuine logger pointed at the requested destination.
27
+ def new(*args, **kwargs)
28
+ if args.empty? && kwargs.empty?
29
+ SemanticLogger[self]
30
+ else
31
+ _semantic_logger_original_new(*args, **kwargs)
32
+ end
33
+ end
25
34
  end
26
35
  end
27
36
  end
@@ -9,7 +9,7 @@ module Rails
9
9
  def log_to_stdout
10
10
  wrapped_app # touch the app so the logger is set up
11
11
 
12
- SemanticLogger.add_appender(io: $stdout, formatter: :color) unless SemanticLogger.appenders.console_output?
12
+ RailsSemanticLogger.add_server_appenders
13
13
  end
14
14
  end
15
15
  end
@@ -12,12 +12,12 @@ module Sidekiq
12
12
  if defined?(::Sidekiq::Logging)
13
13
  # Replace Sidekiq logging context
14
14
  module Logging
15
- def self.with_context(msg, &block)
16
- SemanticLogger.tagged(msg, &block)
15
+ def self.with_context(msg, &)
16
+ SemanticLogger.tagged(msg, &)
17
17
  end
18
18
 
19
19
  def self.job_hash_context(job_hash)
20
- h = {jid: job_hash["jid"]}
20
+ h = {jid: job_hash["jid"], class: job_hash["wrapped"] || job_hash["class"]}
21
21
  h[:bid] = job_hash["bid"] if job_hash["bid"]
22
22
  h[:queue] = job_hash["queue"] if job_hash["queue"]
23
23
  h
@@ -30,7 +30,7 @@ module Sidekiq
30
30
  # Convert string to machine readable format
31
31
  class Processor
32
32
  def log_context(job_hash)
33
- h = {jid: job_hash["jid"]}
33
+ h = {jid: job_hash["jid"], class: job_hash["wrapped"] || job_hash["class"]}
34
34
  h[:bid] = job_hash["bid"] if job_hash["bid"]
35
35
  h[:queue] = job_hash["queue"] if job_hash["queue"]
36
36
  h
@@ -44,24 +44,41 @@ module Sidekiq
44
44
  # rubocop:disable Style/ExplicitBlockArgument
45
45
  def call(worker, item, queue)
46
46
  SemanticLogger.tagged(queue: queue) do
47
- worker.logger.info(
48
- "Start #perform",
49
- metric: "sidekiq.queue.latency",
50
- metric_amount: job_latency_ms(item)
51
- )
52
- worker.logger.measure_info(
53
- "Completed #perform",
54
- on_exception_level: :error,
55
- log_exception: :full,
56
- metric: "sidekiq.job.perform"
57
- ) { yield }
47
+ if perform_messages_enabled?
48
+ worker.logger.info(
49
+ "Start #perform",
50
+ metric: "sidekiq.queue.latency",
51
+ metric_amount: job_latency_ms(item)
52
+ )
53
+
54
+ worker.logger.measure_info(
55
+ "Completed #perform",
56
+ on_exception_level: :error,
57
+ log_exception: :full,
58
+ metric: "sidekiq.job.perform"
59
+ ) { yield }
60
+ else
61
+ yield
62
+ end
58
63
  end
59
64
  end
60
65
 
66
+ def perform_messages_enabled?
67
+ RailsSemanticLogger::Sidekiq::JobLogger.perform_messages != false
68
+ end
69
+
61
70
  def job_latency_ms(job)
62
71
  return unless job && job["enqueued_at"]
63
72
 
64
- (Time.now.to_f - job["enqueued_at"].to_f) * 1000
73
+ enqueued_at = job["enqueued_at"]
74
+ if enqueued_at.is_a?(Float)
75
+ # Sidekiq <= 7: seconds since epoch
76
+ (Time.now.to_f - enqueued_at) * 1000
77
+ else
78
+ # Sidekiq 8+: milliseconds since epoch
79
+ now = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
80
+ now - enqueued_at
81
+ end
65
82
  end
66
83
  end
67
84
  end
@@ -4,7 +4,9 @@ module RailsSemanticLogger
4
4
  # * Convert Action Controller and Active Record text messages to semantic data
5
5
  #
6
6
  # Rails -- Started -- { :ip => "127.0.0.1", :method => "GET", :path => "/dashboards/inquiry_recent_activity" }
7
+ # rubocop:disable Layout/LineLength
7
8
  # UserController -- Completed #index -- { :action => "index", :db_runtime => 54.64, :format => "HTML", :method => "GET", :mongo_runtime => 0.0, :path => "/users", :status => 200, :status_message => "OK", :view_runtime => 709.88 }
9
+ # rubocop:enable Layout/LineLength
8
10
  #
9
11
  # config.rails_semantic_logger.semantic = true
10
12
  #
@@ -44,6 +46,10 @@ module RailsSemanticLogger
44
46
  #
45
47
  # config.rails_semantic_logger.add_file_appender = true
46
48
  #
49
+ # DEPRECATED: declare appenders via #appenders instead. Declaring any appender there already
50
+ # replaces the default file appender, so this flag is no longer needed:
51
+ # config.rails_semantic_logger.appenders { |appenders| appenders.add(file_name: ...) }
52
+ #
47
53
  # * Silence asset logging
48
54
  #
49
55
  # config.rails_semantic_logger.quiet_assets = false
@@ -52,6 +58,10 @@ module RailsSemanticLogger
52
58
  #
53
59
  # config.rails_semantic_logger.console_logger = false
54
60
  #
61
+ # DEPRECATED: declare a console appender explicitly via #appenders instead, or
62
+ # declare none to disable it:
63
+ # config.rails_semantic_logger.appenders { |appenders| appenders.add_console(...) }
64
+ #
55
65
  # * Override the output format for the primary Rails log file.
56
66
  #
57
67
  # Valid options:
@@ -97,10 +107,6 @@ module RailsSemanticLogger
97
107
  #
98
108
  # config.rails_semantic_logger.filter = nil
99
109
  #
100
- # * named_tags: *DEPRECATED*
101
- # Instead, supply a Hash to config.log_tags
102
- # config.rails_semantic_logger.named_tags = nil
103
- #
104
110
  # * Change the message format of Action Controller action.
105
111
  # A block that will be called to format the message.
106
112
  # It is supplied with the `message` and `payload` and should return the formatted data.
@@ -108,24 +114,175 @@ module RailsSemanticLogger
108
114
  # config.rails_semantic_logger.action_message_format = -> (message, payload) do
109
115
  # "#{message} - #{payload[:controller]}##{payload[:action]}"
110
116
  # end
117
+ #
118
+ # * Do not replace the Sidekiq logger with a Semantic Logger logger.
119
+ #
120
+ # config.rails_semantic_logger.replace_sidekiq_logger = false
121
+ #
122
+ # * Do not replace the SolidQueue logger / log subscriber.
123
+ #
124
+ # config.rails_semantic_logger.replace_solid_queue_logger = false
111
125
  class Options
112
- attr_accessor :semantic, :started, :processing, :rendered, :ap_options, :add_file_appender,
113
- :quiet_assets, :format, :named_tags, :filter, :console_logger, :action_message_format
126
+ # Settings consumed while the logger itself is built, during Rails'
127
+ # `:initialize_logger` initializer. Changing them after that (e.g. from
128
+ # `config/initializers/*`, which Rails loads later) has no effect.
129
+ LOGGER_INIT_SETTINGS = %i[semantic replace_sidekiq_logger replace_solid_queue_logger].freeze
130
+
131
+ # Settings consumed as Rails finishes initializing (`config.after_initialize`),
132
+ # which runs *after* `config/initializers/*`. They may still be set there, but
133
+ # changing them after the application has booted has no effect.
134
+ POST_INIT_SETTINGS = %i[started processing rendered quiet_assets action_message_format].freeze
135
+
136
+ attr_reader(*LOGGER_INIT_SETTINGS, *POST_INIT_SETTINGS)
137
+
138
+ # DEPRECATED: configure these on the appender instead, via #appenders.
139
+ attr_reader :ap_options, :format, :filter, :console_logger, :add_file_appender
140
+
141
+ LOGGER_INIT_SETTINGS.each do |setting|
142
+ define_method("#{setting}=") do |value|
143
+ warn_if_logger_initialized(setting)
144
+ instance_variable_set(:"@#{setting}", value)
145
+ end
146
+ end
147
+
148
+ POST_INIT_SETTINGS.each do |setting|
149
+ define_method("#{setting}=") do |value|
150
+ warn_if_fully_initialized(setting)
151
+ instance_variable_set(:"@#{setting}", value)
152
+ end
153
+ end
114
154
 
115
155
  # Setup default values
116
156
  def initialize
117
- @semantic = true
118
- @started = false
119
- @processing = false
120
- @rendered = false
121
- @ap_options = {multiline: false}
122
- @add_file_appender = true
123
- @quiet_assets = false
124
- @format = :default
125
- @named_tags = nil
126
- @filter = nil
127
- @console_logger = true
128
- @action_message_format = nil
157
+ @semantic = true
158
+ @started = false
159
+ @processing = false
160
+ @rendered = false
161
+ @ap_options = {multiline: false}
162
+ @add_file_appender = true
163
+ @quiet_assets = false
164
+ @format = :default
165
+ @filter = nil
166
+ @console_logger = true
167
+ @action_message_format = nil
168
+ @replace_sidekiq_logger = true
169
+ @replace_solid_queue_logger = true
170
+ end
171
+
172
+ # Declare the appenders for Rails Semantic Logger to create, replacing the
173
+ # default file appender:
174
+ #
175
+ # config.rails_semantic_logger.appenders do |appenders|
176
+ # appenders.add(file_name: "log/#{Rails.env}.log", formatter: :json)
177
+ # appenders.add_server(io: $stdout, formatter: :color)
178
+ # appenders.add_console(io: $stderr, formatter: :color)
179
+ # end
180
+ #
181
+ # The method names the context in which the appender is created; the destination
182
+ # is an ordinary `SemanticLogger.add_appender` argument. Use `add` for an
183
+ # appender that is always created, `add_server` for one created only when serving
184
+ # requests (`rails server`, a rack server, Sidekiq in server mode; defaults to
185
+ # `$stdout`), and `add_console` for one created only inside a `rails console`
186
+ # session (defaults to `$stderr`). Any appender works in any context, so a
187
+ # context may declare several (e.g. a server-only stdout and file appender).
188
+ #
189
+ # `add_server` appenders are created automatically under `rails server` and
190
+ # Sidekiq in server mode. App servers without a first-party hook (bare puma,
191
+ # rackup, Passenger, Unicorn) are not detected; create them from the server's
192
+ # own boot hook instead, e.g. in `config/puma.rb`:
193
+ #
194
+ # on_booted { RailsSemanticLogger.add_server_appenders }
195
+ #
196
+ # Returns the underlying RailsSemanticLogger::Appenders collection. When at
197
+ # least one appender has been declared, the default file appender (and the
198
+ # `format`, `ap_options`, `filter`, and `add_file_appender` options) is no
199
+ # longer used.
200
+ def appenders
201
+ @appenders ||= RailsSemanticLogger::Appenders.new
202
+ if block_given?
203
+ warn_if_logger_initialized(:appenders)
204
+ yield @appenders
205
+ end
206
+ @appenders
207
+ end
208
+
209
+ # Whether the application declared its own appenders via #appenders.
210
+ def appenders?
211
+ defined?(@appenders) && @appenders.any?
212
+ end
213
+
214
+ # Marks that Rails Semantic Logger has already built its logger and the
215
+ # init-time appenders. Called by the engine at the end of the
216
+ # `:initialize_logger` initializer. After this point, settings that are only
217
+ # read while building those appenders no longer have any effect, so changing
218
+ # them emits a warning (see #warn_if_logger_initialized).
219
+ def logger_initialized!
220
+ @logger_initialized = true
221
+ end
222
+
223
+ # Marks that Rails has finished initializing the application (the engine's
224
+ # `config.after_initialize` has run). Called by the engine. After this point,
225
+ # the POST_INIT_SETTINGS have already been consumed, so changing them warns.
226
+ def fully_initialized!
227
+ @fully_initialized = true
228
+ end
229
+
230
+ def ap_options=(value)
231
+ deprecate_appender_option(:ap_options)
232
+ warn_if_logger_initialized(:ap_options)
233
+ @ap_options = value
234
+ end
235
+
236
+ def format=(value)
237
+ deprecate_appender_option(:format)
238
+ warn_if_logger_initialized(:format)
239
+ @format = value
240
+ end
241
+
242
+ def filter=(value)
243
+ deprecate_appender_option(:filter)
244
+ warn_if_logger_initialized(:filter)
245
+ @filter = value
246
+ end
247
+
248
+ def console_logger=(value)
249
+ deprecate_appender_option(:console_logger, via: "appenders.add_console(...)")
250
+ @console_logger = value
251
+ end
252
+
253
+ def add_file_appender=(value)
254
+ deprecate_appender_option(:add_file_appender)
255
+ warn_if_logger_initialized(:add_file_appender)
256
+ @add_file_appender = value
257
+ end
258
+
259
+ private
260
+
261
+ def deprecate_appender_option(option, via: "appenders.add(...)")
262
+ RailsSemanticLogger.deprecator.warn(
263
+ "`config.rails_semantic_logger.#{option}=` is deprecated and will be removed in a future release. " \
264
+ "Declare the destination and formatting directly instead, via " \
265
+ "`config.rails_semantic_logger.appenders { |appenders| #{via} }`."
266
+ )
267
+ end
268
+
269
+ # Warn when an init-time setting is changed after the logger and its
270
+ # appenders have already been built. This is the classic footgun of putting
271
+ # logger configuration in `config/initializers/*`, which Rails loads *after*
272
+ # the logger is initialized, so the change silently has no effect.
273
+ def warn_if_logger_initialized(setting)
274
+ return unless defined?(@logger_initialized) && @logger_initialized
275
+
276
+ RailsSemanticLogger.warn_initialized_too_late(setting)
277
+ end
278
+
279
+ # Warn when a setting consumed at the end of Rails initialization is changed
280
+ # after the application has already booted. Unlike #warn_if_logger_initialized,
281
+ # `config/initializers/*` is *not* too late for these, so it is not suggested.
282
+ def warn_if_fully_initialized(setting)
283
+ return unless defined?(@fully_initialized) && @fully_initialized
284
+
285
+ RailsSemanticLogger.warn_initialized_too_late(setting, config_initializers_too_late: false)
129
286
  end
130
287
  end
131
288
  end
@@ -31,23 +31,16 @@ module RailsSemanticLogger
31
31
 
32
32
  private
33
33
 
34
- @logger = SemanticLogger["Rack"]
34
+ @logger = SemanticLogger[::Rack]
35
35
  @started_request_log_level = :debug
36
36
 
37
37
  def call_app(request, env)
38
38
  instrumenter = ActiveSupport::Notifications.instrumenter
39
- if (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR >= 1) || Rails::VERSION::MAJOR > 7
40
- handle = instrumenter.build_handle "request.action_dispatch", request: request
41
- instrumenter_finish = lambda {
42
- handle.finish
43
- }
44
- handle.start
45
- else
46
- instrumenter_state = instrumenter.start "request.action_dispatch", request: request
47
- instrumenter_finish = lambda {
48
- instrumenter.finish_with_state(instrumenter_state, "request.action_dispatch", request: request)
49
- }
50
- end
39
+ handle = instrumenter.build_handle "request.action_dispatch", request: request
40
+ instrumenter_finish = lambda {
41
+ handle.finish
42
+ }
43
+ handle.start
51
44
 
52
45
  logger.send(self.class.started_request_log_level) { started_request_message(request) }
53
46
  status, headers, body = @app.call(env)
@@ -2,6 +2,8 @@ module RailsSemanticLogger
2
2
  module Sidekiq
3
3
  module Defaults
4
4
  # Prevent exception logging during standard error handling since the Job Logger below already logs the exception.
5
+ # Logs the remaining Sidekiq context at :info (matching upstream Sidekiq's default handler) rather than :warn,
6
+ # since the exception itself is already logged at :error by the Job Logger.
5
7
  ERROR_HANDLER =
6
8
  if ::Sidekiq::VERSION.to_f < 7.1 ||
7
9
  (::Sidekiq::VERSION.to_f == 7.1 && ::Sidekiq::VERSION.split(".").last.to_i < 6)
@@ -10,7 +12,7 @@ module RailsSemanticLogger
10
12
  job_hash = ctx[:job] || {}
11
13
  klass = job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
12
14
  logger = klass ? SemanticLogger[klass] : ::Sidekiq.logger
13
- ctx[:context] ? logger.warn(ctx[:context], ctx) : logger.warn(ctx)
15
+ ctx[:context] ? logger.info(ctx[:context], ctx) : logger.info(ctx)
14
16
  end
15
17
  end
16
18
  else
@@ -19,7 +21,7 @@ module RailsSemanticLogger
19
21
  job_hash = ctx[:job] || {}
20
22
  klass = job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
21
23
  logger = klass ? SemanticLogger[klass] : ::Sidekiq.logger
22
- ctx[:context] ? logger.warn(ctx[:context], ctx) : logger.warn(ctx)
24
+ ctx[:context] ? logger.info(ctx[:context], ctx) : logger.info(ctx)
23
25
  end
24
26
  end
25
27
  end
@@ -1,6 +1,14 @@
1
1
  module RailsSemanticLogger
2
2
  module Sidekiq
3
3
  class JobLogger
4
+ class << self
5
+ attr_writer :perform_messages
6
+
7
+ def perform_messages
8
+ instance_variable_defined?(:@perform_messages) ? @perform_messages : true
9
+ end
10
+ end
11
+
4
12
  # Sidekiq 6.5 does not take any arguments, whereas v7 is given a logger
5
13
  def initialize(*_args)
6
14
  end
@@ -10,21 +18,27 @@ module RailsSemanticLogger
10
18
  logger = klass ? SemanticLogger[klass] : Sidekiq.logger
11
19
 
12
20
  SemanticLogger.tagged(queue: queue) do
13
- # Latency is the time between when the job was enqueued and when it started executing.
14
- logger.info(
15
- "Start #perform",
16
- metric: "sidekiq.queue.latency",
17
- metric_amount: job_latency_ms(item)
18
- )
21
+ if perform_messages_enabled?
22
+ # Latency is the time between when the job was enqueued and when it started executing.
23
+ logger.info(
24
+ "Start #perform",
25
+ metric: "sidekiq.queue.latency",
26
+ metric_amount: job_latency_ms(item)
27
+ )
28
+ end
19
29
 
20
30
  # Measure the duration of running the job
21
- logger.measure_info(
22
- "Completed #perform",
23
- on_exception_level: :error,
24
- log_exception: :full,
25
- metric: "sidekiq.job.perform",
26
- &block
27
- )
31
+ if perform_messages_enabled?
32
+ logger.measure_info(
33
+ "Completed #perform",
34
+ on_exception_level: :error,
35
+ log_exception: :full,
36
+ metric: "sidekiq.job.perform",
37
+ &block
38
+ )
39
+ elsif block_given?
40
+ yield
41
+ end
28
42
  end
29
43
  end
30
44
 
@@ -41,8 +55,12 @@ module RailsSemanticLogger
41
55
 
42
56
  private
43
57
 
58
+ def perform_messages_enabled?
59
+ self.class.perform_messages != false
60
+ end
61
+
44
62
  def job_hash_context(job_hash)
45
- h = {jid: job_hash["jid"]}
63
+ h = {jid: job_hash["jid"], class: job_hash["wrapped"] || job_hash["class"]}
46
64
  h[:bid] = job_hash["bid"] if job_hash["bid"]
47
65
  h[:tags] = job_hash["tags"] if job_hash["tags"]
48
66
  h[:queue] = job_hash["queue"] if job_hash["queue"]
@@ -52,7 +70,15 @@ module RailsSemanticLogger
52
70
  def job_latency_ms(job)
53
71
  return unless job && job["enqueued_at"]
54
72
 
55
- (Time.now.to_f - job["enqueued_at"].to_f) * 1000
73
+ enqueued_at = job["enqueued_at"]
74
+ if enqueued_at.is_a?(Float)
75
+ # Sidekiq <= 7: seconds since epoch
76
+ (Time.now.to_f - enqueued_at) * 1000
77
+ else
78
+ # Sidekiq 8+: milliseconds since epoch
79
+ now = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
80
+ now - enqueued_at
81
+ end
56
82
  end
57
83
  end
58
84
  end