lumberjack 1.0.13 → 1.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +129 -0
  3. data/{MIT_LICENSE → MIT_LICENSE.txt} +0 -0
  4. data/README.md +142 -23
  5. data/VERSION +1 -1
  6. data/lib/lumberjack.rb +70 -22
  7. data/lib/lumberjack/context.rb +35 -0
  8. data/lib/lumberjack/device.rb +22 -8
  9. data/lib/lumberjack/device/date_rolling_log_file.rb +9 -9
  10. data/lib/lumberjack/device/log_file.rb +14 -3
  11. data/lib/lumberjack/device/multi.rb +46 -0
  12. data/lib/lumberjack/device/null.rb +1 -3
  13. data/lib/lumberjack/device/rolling_log_file.rb +45 -21
  14. data/lib/lumberjack/device/size_rolling_log_file.rb +10 -10
  15. data/lib/lumberjack/device/writer.rb +92 -61
  16. data/lib/lumberjack/formatter.rb +97 -28
  17. data/lib/lumberjack/formatter/date_time_formatter.rb +25 -0
  18. data/lib/lumberjack/formatter/exception_formatter.rb +25 -2
  19. data/lib/lumberjack/formatter/id_formatter.rb +23 -0
  20. data/lib/lumberjack/formatter/object_formatter.rb +12 -0
  21. data/lib/lumberjack/formatter/pretty_print_formatter.rb +4 -4
  22. data/lib/lumberjack/formatter/string_formatter.rb +1 -1
  23. data/lib/lumberjack/formatter/strip_formatter.rb +12 -0
  24. data/lib/lumberjack/formatter/structured_formatter.rb +63 -0
  25. data/lib/lumberjack/log_entry.rb +44 -16
  26. data/lib/lumberjack/logger.rb +275 -69
  27. data/lib/lumberjack/rack.rb +3 -2
  28. data/lib/lumberjack/rack/context.rb +18 -0
  29. data/lib/lumberjack/rack/request_id.rb +4 -4
  30. data/lib/lumberjack/rack/unit_of_work.rb +1 -1
  31. data/lib/lumberjack/severity.rb +11 -10
  32. data/lib/lumberjack/tag_formatter.rb +96 -0
  33. data/lib/lumberjack/tagged_logger_support.rb +66 -0
  34. data/lib/lumberjack/tagged_logging.rb +29 -0
  35. data/lib/lumberjack/tags.rb +42 -0
  36. data/lib/lumberjack/template.rb +81 -33
  37. data/lumberjack.gemspec +31 -0
  38. metadata +26 -53
  39. data/Rakefile +0 -40
  40. data/spec/device/date_rolling_log_file_spec.rb +0 -73
  41. data/spec/device/log_file_spec.rb +0 -48
  42. data/spec/device/null_spec.rb +0 -12
  43. data/spec/device/rolling_log_file_spec.rb +0 -151
  44. data/spec/device/size_rolling_log_file_spec.rb +0 -58
  45. data/spec/device/writer_spec.rb +0 -118
  46. data/spec/formatter/exception_formatter_spec.rb +0 -20
  47. data/spec/formatter/inspect_formatter_spec.rb +0 -13
  48. data/spec/formatter/pretty_print_formatter_spec.rb +0 -14
  49. data/spec/formatter/string_formatter_spec.rb +0 -12
  50. data/spec/formatter_spec.rb +0 -45
  51. data/spec/log_entry_spec.rb +0 -69
  52. data/spec/logger_spec.rb +0 -411
  53. data/spec/lumberjack_spec.rb +0 -29
  54. data/spec/rack/request_id_spec.rb +0 -48
  55. data/spec/rack/unit_of_work_spec.rb +0 -26
  56. data/spec/severity_spec.rb +0 -23
  57. data/spec/spec_helper.rb +0 -32
  58. data/spec/template_spec.rb +0 -34
@@ -30,14 +30,17 @@ module Lumberjack
30
30
  # The time that the device was last flushed.
31
31
  attr_reader :last_flushed_at
32
32
 
33
- # The name of the program associated with log messages.
33
+ # Set +silencer+ to false to disable silencing the log.
34
+ attr_accessor :silencer
35
+
36
+ # Set the name of the program to attach to log entries.
34
37
  attr_writer :progname
35
38
 
36
- # The device being written to.
37
- attr_reader :device
39
+ # The device being written to
40
+ attr_accessor :device
38
41
 
39
- # Set +silencer+ to false to disable silencing the log.
40
- attr_accessor :silencer
42
+ # The TagFormatter used for formatting tags for output
43
+ attr_accessor :tag_formatter
41
44
 
42
45
  # Create a new logger to log to a Device.
43
46
  #
@@ -45,37 +48,50 @@ module Lumberjack
45
48
  #
46
49
  # If it is a Device object, that object will be used.
47
50
  # If it has a +write+ method, it will be wrapped in a Device::Writer class.
48
- # If it is <tt>:null</tt>, it will be a Null device that won't record any output.
51
+ # If it is :null, it will be a Null device that won't record any output.
49
52
  # Otherwise, it will be assumed to be file path and wrapped in a Device::LogFile class.
50
53
  #
51
54
  # This method can take the following options:
52
55
  #
53
- # * <tt>:level</tt> - The logging level below which messages will be ignored.
54
- # * <tt>:progname</tt> - The name of the program that will be recorded with each log entry.
55
- # * <tt>:flush_seconds</tt> - The maximum number of seconds between flush calls.
56
- # * <tt>:roll</tt> - If the log device is a file path, it will be a Device::DateRollingLogFile if this is set.
57
- # * <tt>:max_size</tt> - If the log device is a file path, it will be a Device::SizeRollingLogFile if this is set.
56
+ # * :level - The logging level below which messages will be ignored.
57
+ # * :formatter - The formatter to use for outputting messages to the log.
58
+ # * :datetime_format - The format to use for log timestamps.
59
+ # * :tag_formatter - The TagFormatter to use for formatting tags.
60
+ # * :progname - The name of the program that will be recorded with each log entry.
61
+ # * :flush_seconds - The maximum number of seconds between flush calls.
62
+ # * :roll - If the log device is a file path, it will be a Device::DateRollingLogFile if this is set.
63
+ # * :max_size - If the log device is a file path, it will be a Device::SizeRollingLogFile if this is set.
58
64
  #
59
65
  # All other options are passed to the device constuctor.
60
- def initialize(device = STDOUT, options = {})
61
- @thread_settings = {}
62
-
66
+ def initialize(device = $stdout, options = {})
63
67
  options = options.dup
64
68
  self.level = options.delete(:level) || INFO
65
69
  self.progname = options.delete(:progname)
66
70
  max_flush_seconds = options.delete(:flush_seconds).to_f
67
71
 
68
- @device = open_device(device, options)
69
- @_formatter = Formatter.new
72
+ @device = open_device(device, options) if device
73
+ self.formatter = (options[:formatter] || Formatter.new)
74
+ @tag_formatter = (options[:tag_formatter] || TagFormatter.new)
75
+ time_format = (options[:datetime_format] || options[:time_format])
76
+ self.datetime_format = time_format if time_format
70
77
  @last_flushed_at = Time.now
71
78
  @silencer = true
79
+ @tags = {}
80
+ @closed = false
72
81
 
73
82
  create_flusher_thread(max_flush_seconds) if max_flush_seconds > 0
74
83
  end
75
84
 
76
- # Get the Formatter object used to convert messages into strings.
77
- def formatter
78
- @_formatter
85
+ # Get the timestamp format on the device if it has one.
86
+ def datetime_format
87
+ device.datetime_format if device.respond_to?(:datetime_format)
88
+ end
89
+
90
+ # Set the timestamp format on the device if it is supported.
91
+ def datetime_format=(format)
92
+ if device.respond_to?(:datetime_format=)
93
+ device.datetime_format = format
94
+ end
79
95
  end
80
96
 
81
97
  # Get the level of severity of entries that are logged. Entries with a lower
@@ -84,6 +100,47 @@ module Lumberjack
84
100
  thread_local_value(:lumberjack_logger_level) || @level
85
101
  end
86
102
 
103
+ alias sev_threshold level
104
+
105
+ # Set the log level using either an integer level like Logger::INFO or a label like
106
+ # :info or "info"
107
+ def level=(value)
108
+ @level = if value.is_a?(Integer)
109
+ value
110
+ else
111
+ Severity.label_to_level(value)
112
+ end
113
+ end
114
+
115
+ alias sev_threshold= level=
116
+
117
+ # Set the Lumberjack::Formatter used to format objects for logging as messages.
118
+ def formatter=(value)
119
+ @_formatter = (value.is_a?(TaggedLoggerSupport::Formatter) ? value.__formatter : value)
120
+ end
121
+
122
+ # Get the Lumberjack::Formatter used to format objects for logging as messages.
123
+ def formatter
124
+ if respond_to?(:tagged)
125
+ # Wrap in an object that supports ActiveSupport::TaggedLogger API
126
+ TaggedLoggerSupport::Formatter.new(logger: self, formatter: @_formatter)
127
+ else
128
+ @_formatter
129
+ end
130
+ end
131
+
132
+ # Enable this logger to function like an ActiveSupport::TaggedLogger. This will make the logger
133
+ # API compatible with ActiveSupport::TaggedLogger and is provided as a means of compatibility
134
+ # with other libraries that assume they can call the `tagged` method on a logger to add tags.
135
+ #
136
+ # The tags added with this method are just strings so they are stored in the logger tags
137
+ # in an array under the "tagged" tag. So calling `logger.tagged("foo", "bar")` will result
138
+ # in tags `{"tagged" => ["foo", "bar"]}`.
139
+ def tagged_logger!
140
+ extend(TaggedLoggerSupport)
141
+ self
142
+ end
143
+
87
144
  # Add a message to the log with a given severity. The message can be either
88
145
  # passed in the +message+ argument or supplied with a block. This method
89
146
  # is not normally called. Instead call one of the helper functions
@@ -94,39 +151,59 @@ module Lumberjack
94
151
  #
95
152
  # === Example
96
153
  #
97
- # logger.add(Lumberjack::Severity::ERROR, exception)
98
- # logger.add(Lumberjack::Severity::INFO, "Request completed")
99
- # logger.add(:warn, "Request took a long time")
100
- # logger.add(Lumberjack::Severity::DEBUG){"Start processing with options #{options.inspect}"}
101
- def add(severity, message = nil, progname = nil)
102
- severity = Severity.label_to_level(severity) if severity.is_a?(String) || severity.is_a?(Symbol)
154
+ # logger.add_entry(Logger::ERROR, exception)
155
+ # logger.add_entry(Logger::INFO, "Request completed")
156
+ # logger.add_entry(:warn, "Request took a long time")
157
+ # logger.add_entry(Logger::DEBUG){"Start processing with options #{options.inspect}"}
158
+ def add_entry(severity, message, progname = nil, tags = nil)
159
+ begin
160
+ severity = Severity.label_to_level(severity) unless severity.is_a?(Integer)
161
+ return true unless device && severity && severity >= level
162
+
163
+ return true if Thread.current[:lumberjack_logging]
164
+ Thread.current[:lumberjack_logging] = true
103
165
 
104
- return unless severity && severity >= level
166
+ time = Time.now
167
+ message = message.call if message.is_a?(Proc)
168
+ message = formatter.format(message)
169
+ progname ||= self.progname
105
170
 
106
- time = Time.now
171
+ current_tags = self.tags
172
+ tags = nil unless tags.is_a?(Hash)
173
+ if current_tags.empty?
174
+ tags = Tags.stringify_keys(tags) unless tags.nil?
175
+ else
176
+ tags = if tags.nil?
177
+ current_tags.dup
178
+ else
179
+ current_tags.merge(Tags.stringify_keys(tags))
180
+ end
181
+ end
182
+ tags = Tags.expand_runtime_values(tags)
183
+ tags = tag_formatter.format(tags) if tag_formatter
184
+
185
+ entry = LogEntry.new(time, severity, message, progname, $$, tags)
186
+ write_to_device(entry)
187
+ ensure
188
+ Thread.current[:lumberjack_logging] = nil
189
+ end
190
+ true
191
+ end
192
+
193
+ # ::Logger compatible method to add a log entry.
194
+ def add(severity, message = nil, progname = nil, &block)
107
195
  if message.nil?
108
- if block_given?
109
- message = yield
196
+ if block
197
+ message = block
110
198
  else
111
199
  message = progname
112
200
  progname = nil
113
201
  end
114
202
  end
115
-
116
- message = @_formatter.format(message)
117
- progname ||= self.progname
118
- entry = LogEntry.new(time, severity, message, progname, $$, Lumberjack.unit_of_work_id)
119
- begin
120
- device.write(entry)
121
- rescue => e
122
- $stderr.puts("#{e.class.name}: #{e.message}#{' at ' + e.backtrace.first if e.backtrace}")
123
- $stderr.puts(entry.to_s)
124
- end
125
-
126
- nil
203
+ add_entry(severity, message, progname)
127
204
  end
128
205
 
129
- alias_method :log, :add
206
+ alias log add
130
207
 
131
208
  # Flush the logging device. Messages are not guaranteed to be written until this method is called.
132
209
  def flush
@@ -138,12 +215,22 @@ module Lumberjack
138
215
  # Close the logging device.
139
216
  def close
140
217
  flush
141
- @device.close if @device.respond_to?(:close)
218
+ device.close if device.respond_to?(:close)
219
+ @closed = true
220
+ end
221
+
222
+ def closed?
223
+ @closed
224
+ end
225
+
226
+ def reopen(logdev = nil)
227
+ @closed = false
228
+ device.reopen(logdev) if device.respond_to?(:reopen)
142
229
  end
143
230
 
144
231
  # Log a +FATAL+ message. The message can be passed in either the +message+ argument or in a block.
145
- def fatal(message = nil, progname = nil, &block)
146
- add(FATAL, message, progname, &block)
232
+ def fatal(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
233
+ call_add_entry(FATAL, message_or_progname_or_tags, progname_or_tags, &block)
147
234
  end
148
235
 
149
236
  # Return +true+ if +FATAL+ messages are being logged.
@@ -151,9 +238,14 @@ module Lumberjack
151
238
  level <= FATAL
152
239
  end
153
240
 
241
+ # Set the log level to fatal.
242
+ def fatal!
243
+ self.level = FATAL
244
+ end
245
+
154
246
  # Log an +ERROR+ message. The message can be passed in either the +message+ argument or in a block.
155
- def error(message = nil, progname = nil, &block)
156
- add(ERROR, message, progname, &block)
247
+ def error(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
248
+ call_add_entry(ERROR, message_or_progname_or_tags, progname_or_tags, &block)
157
249
  end
158
250
 
159
251
  # Return +true+ if +ERROR+ messages are being logged.
@@ -161,9 +253,14 @@ module Lumberjack
161
253
  level <= ERROR
162
254
  end
163
255
 
256
+ # Set the log level to error.
257
+ def error!
258
+ self.level = ERROR
259
+ end
260
+
164
261
  # Log a +WARN+ message. The message can be passed in either the +message+ argument or in a block.
165
- def warn(message = nil, progname = nil, &block)
166
- add(WARN, message, progname, &block)
262
+ def warn(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
263
+ call_add_entry(WARN, message_or_progname_or_tags, progname_or_tags, &block)
167
264
  end
168
265
 
169
266
  # Return +true+ if +WARN+ messages are being logged.
@@ -171,9 +268,14 @@ module Lumberjack
171
268
  level <= WARN
172
269
  end
173
270
 
271
+ # Set the log level to warn.
272
+ def warn!
273
+ self.level = WARN
274
+ end
275
+
174
276
  # Log an +INFO+ message. The message can be passed in either the +message+ argument or in a block.
175
- def info(message = nil, progname = nil, &block)
176
- add(INFO, message, progname, &block)
277
+ def info(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
278
+ call_add_entry(INFO, message_or_progname_or_tags, progname_or_tags, &block)
177
279
  end
178
280
 
179
281
  # Return +true+ if +INFO+ messages are being logged.
@@ -181,9 +283,14 @@ module Lumberjack
181
283
  level <= INFO
182
284
  end
183
285
 
286
+ # Set the log level to info.
287
+ def info!
288
+ self.level = INFO
289
+ end
290
+
184
291
  # Log a +DEBUG+ message. The message can be passed in either the +message+ argument or in a block.
185
- def debug(message = nil, progname = nil, &block)
186
- add(DEBUG, message, progname, &block)
292
+ def debug(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
293
+ call_add_entry(DEBUG, message_or_progname_or_tags, progname_or_tags, &block)
187
294
  end
188
295
 
189
296
  # Return +true+ if +DEBUG+ messages are being logged.
@@ -191,21 +298,19 @@ module Lumberjack
191
298
  level <= DEBUG
192
299
  end
193
300
 
301
+ # Set the log level to debug.
302
+ def debug!
303
+ self.level = DEBUG
304
+ end
305
+
194
306
  # Log a message when the severity is not known. Unknown messages will always appear in the log.
195
307
  # The message can be passed in either the +message+ argument or in a block.
196
- def unknown(message = nil, progname = nil, &block)
197
- add(UNKNOWN, message, progname, &block)
308
+ def unknown(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
309
+ call_add_entry(UNKNOWN, message_or_progname_or_tags, progname_or_tags, &block)
198
310
  end
199
311
 
200
- alias_method :<<, :unknown
201
-
202
- # Set the minimum level of severity of messages to log.
203
- def level=(severity)
204
- if severity.is_a?(Integer)
205
- @level = severity
206
- else
207
- @level = Severity.label_to_level(severity)
208
- end
312
+ def <<(msg)
313
+ add_entry(UNKNOWN, msg)
209
314
  end
210
315
 
211
316
  # Silence the logger by setting a new log level inside a block. By default, only +ERROR+ or +FATAL+
@@ -213,12 +318,15 @@ module Lumberjack
213
318
  #
214
319
  # === Example
215
320
  #
216
- # logger.level = Lumberjack::Severity::INFO
321
+ # logger.level = Logger::INFO
217
322
  # logger.silence do
218
323
  # do_something # Log level inside the block is +ERROR+
219
324
  # end
220
325
  def silence(temporary_level = ERROR, &block)
221
326
  if silencer
327
+ unless temporary_level.is_a?(Integer)
328
+ temporary_level = Severity.label_to_level(temporary_level)
329
+ end
222
330
  push_thread_local_value(:lumberjack_logger_level, temporary_level, &block)
223
331
  else
224
332
  yield
@@ -240,8 +348,95 @@ module Lumberjack
240
348
  thread_local_value(:lumberjack_logger_progname) || @progname
241
349
  end
242
350
 
351
+ # Set a hash of tags on logger. If a block is given, the tags will only be set
352
+ # for the duration of the block. If this method is called inside such a block,
353
+ # the tags will only be defined on the tags in that block. When the parent block
354
+ # exits, all the tags will be reverted. If there is no block, then the tags will
355
+ # be defined as global and apply to all log statements.
356
+ def tag(tags, &block)
357
+ tags = Tags.stringify_keys(tags)
358
+ thread_tags = thread_local_value(:lumberjack_logger_tags)
359
+ if block
360
+ merged_tags = (thread_tags ? thread_tags.merge(tags) : tags.dup)
361
+ push_thread_local_value(:lumberjack_logger_tags, merged_tags, &block)
362
+ elsif thread_tags
363
+ thread_tags.merge!(tags)
364
+ nil
365
+ else
366
+ @tags.merge!(tags)
367
+ nil
368
+ end
369
+ end
370
+
371
+ # Remove a tag from the current tag context. If this is called inside a block to a
372
+ # call to `tag`, the tags will only be removed for the duration of that block. Otherwise
373
+ # they will be removed from the global tags.
374
+ def remove_tag(*tag_names)
375
+ thread_tags = thread_local_value(:lumberjack_logger_tags)
376
+ if thread_tags
377
+ tag_names.each { |name| thread_tags.delete(name.to_s) }
378
+ else
379
+ tag_names.each { |name| @tags.delete(name.to_s) }
380
+ end
381
+ end
382
+
383
+ # Return all tags in scope on the logger including global tags set on the Lumberjack
384
+ # context, tags set on the logger, and tags set on the current block for the logger.
385
+ def tags
386
+ tags = {}
387
+ context_tags = Lumberjack.context_tags
388
+ tags.merge!(context_tags) if context_tags && !context_tags.empty?
389
+ tags.merge!(@tags) if !@tags.empty? && !thread_local_value(:lumberjack_logger_untagged)
390
+ scope_tags = thread_local_value(:lumberjack_logger_tags)
391
+ tags.merge!(scope_tags) if scope_tags && !scope_tags.empty?
392
+ tags
393
+ end
394
+
395
+ # Remove all tags on the current logger and logging context within a block.
396
+ # You can still set new block scoped tags within theuntagged block and provide
397
+ # tags on individual log methods.
398
+ def untagged(&block)
399
+ Lumberjack.use_context(nil) do
400
+ scope_tags = thread_local_value(:lumberjack_logger_tags)
401
+ untagged = thread_local_value(:lumberjack_logger_untagged)
402
+ begin
403
+ set_thread_local_value(:lumberjack_logger_untagged, true)
404
+ set_thread_local_value(:lumberjack_logger_tags, nil)
405
+ tag({}, &block)
406
+ ensure
407
+ set_thread_local_value(:lumberjack_logger_untagged, untagged)
408
+ set_thread_local_value(:lumberjack_logger_tags, scope_tags)
409
+ end
410
+ end
411
+ end
412
+
243
413
  private
244
414
 
415
+ # Dereference arguments to log calls so we can have methods with compatibility with ::Logger
416
+ def call_add_entry(severity, message_or_progname_or_tags, progname_or_tags, &block) #:nodoc:
417
+ message = nil
418
+ progname = nil
419
+ tags = nil
420
+ if block
421
+ message = block
422
+ if message_or_progname_or_tags.is_a?(Hash)
423
+ tags = message_or_progname_or_tags
424
+ progname = progname_or_tags
425
+ else
426
+ progname = message_or_progname_or_tags
427
+ tags = progname_or_tags if progname_or_tags.is_a?(Hash)
428
+ end
429
+ else
430
+ message = message_or_progname_or_tags
431
+ if progname_or_tags.is_a?(Hash)
432
+ tags = progname_or_tags
433
+ else
434
+ progname = progname_or_tags
435
+ end
436
+ end
437
+ add_entry(severity, message, progname, tags)
438
+ end
439
+
245
440
  # Set a local value for a thread tied to this object.
246
441
  def set_thread_local_value(name, value) #:nodoc:
247
442
  values = Thread.current[name]
@@ -276,7 +471,9 @@ module Lumberjack
276
471
 
277
472
  # Open a logging device.
278
473
  def open_device(device, options) #:nodoc:
279
- if device.is_a?(Device)
474
+ if device.nil?
475
+ nil
476
+ elsif device.is_a?(Device)
280
477
  device
281
478
  elsif device.respond_to?(:write) && device.respond_to?(:flush)
282
479
  Device::Writer.new(device, options)
@@ -287,25 +484,34 @@ module Lumberjack
287
484
  if options[:roll]
288
485
  Device::DateRollingLogFile.new(device, options)
289
486
  elsif options[:max_size]
290
- Device::SizeRollingLogFile.new(device, options)
487
+ Device::SizeRollingLogFile.new(device, options)
291
488
  else
292
489
  Device::LogFile.new(device, options)
293
490
  end
294
491
  end
295
492
  end
296
493
 
494
+ def write_to_device(entry) #:nodoc:
495
+ device.write(entry)
496
+ rescue => e
497
+ # rubocop:disable Style/StderrPuts
498
+ $stderr.puts("#{e.class.name}: #{e.message}#{" at " + e.backtrace.first if e.backtrace}")
499
+ $stderr.puts(entry.to_s)
500
+ # rubocop:enable Style/StderrPuts
501
+ end
502
+
297
503
  # Create a thread that will periodically call flush.
298
504
  def create_flusher_thread(flush_seconds) #:nodoc:
299
505
  if flush_seconds > 0
300
506
  begin
301
507
  logger = self
302
508
  Thread.new do
303
- loop do
509
+ until closed?
304
510
  begin
305
511
  sleep(flush_seconds)
306
512
  logger.flush if Time.now - logger.last_flushed_at >= flush_seconds
307
513
  rescue => e
308
- STDERR.puts("Error flushing log: #{e.inspect}")
514
+ warn("Error flushing log: #{e.inspect}")
309
515
  end
310
516
  end
311
517
  end