lumberjack 1.0.13 → 1.2.8

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 (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
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Lumberjack
4
4
  module Rack
5
- require File.expand_path("../rack/unit_of_work.rb", __FILE__)
6
- require File.expand_path("../rack/request_id.rb", __FILE__)
5
+ require_relative "rack/unit_of_work"
6
+ require_relative "rack/request_id"
7
+ require_relative "rack/context"
7
8
  end
8
9
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literals: true
2
+
3
+ module Lumberjack
4
+ module Rack
5
+ # Middleware to create a global context for Lumberjack for the scope of a rack request.
6
+ class Context
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ Lumberjack.context do
13
+ @app.call(env)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -6,17 +6,17 @@ module Lumberjack
6
6
  # The format is expected to be a random UUID and only the first chunk is used for terseness
7
7
  # if the abbreviated argument is true.
8
8
  class RequestId
9
- REQUEST_ID = "action_dispatch.request_id".freeze
10
-
9
+ REQUEST_ID = "action_dispatch.request_id"
10
+
11
11
  def initialize(app, abbreviated = false)
12
12
  @app = app
13
13
  @abbreviated = abbreviated
14
14
  end
15
-
15
+
16
16
  def call(env)
17
17
  request_id = env[REQUEST_ID]
18
18
  if request_id && @abbreviated
19
- request_id = request_id.split('-'.freeze, 2).first
19
+ request_id = request_id.split("-", 2).first
20
20
  end
21
21
  Lumberjack.unit_of_work(request_id) do
22
22
  @app.call(env)
@@ -6,7 +6,7 @@ module Lumberjack
6
6
  def initialize(app)
7
7
  @app = app
8
8
  end
9
-
9
+
10
10
  def call(env)
11
11
  Lumberjack.unit_of_work do
12
12
  @app.call(env)
@@ -3,20 +3,21 @@
3
3
  module Lumberjack
4
4
  # The standard severity levels for logging messages.
5
5
  module Severity
6
- UNKNOWN = 5
7
- FATAL = 4
8
- ERROR = 3
9
- WARN = 2
10
- INFO = 1
11
- DEBUG = 0
12
-
13
- SEVERITY_LABELS = %w(DEBUG INFO WARN ERROR FATAL UNKNOWN).freeze
14
-
6
+ # Backward compatibilty with 1.0 API
7
+ DEBUG = ::Logger::Severity::DEBUG
8
+ INFO = ::Logger::Severity::INFO
9
+ WARN = ::Logger::Severity::WARN
10
+ ERROR = ::Logger::Severity::ERROR
11
+ FATAL = ::Logger::Severity::FATAL
12
+ UNKNOWN = ::Logger::Severity::UNKNOWN
13
+
14
+ SEVERITY_LABELS = %w[DEBUG INFO WARN ERROR FATAL UNKNOWN].freeze
15
+
15
16
  class << self
16
17
  def level_to_label(severity)
17
18
  SEVERITY_LABELS[severity] || SEVERITY_LABELS.last
18
19
  end
19
-
20
+
20
21
  def label_to_level(label)
21
22
  SEVERITY_LABELS.index(label.to_s.upcase) || UNKNOWN
22
23
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # Class for formatting tags. You can register a default formatter and tag
5
+ # name specific formatters. Formatters can be either `Lumberjack::Formatter`
6
+ # objects or any object that responds to `call`.
7
+ #
8
+ # tag_formatter = Lumberjack::TagFormatter.new.default(Lumberjack::Formatter.new)
9
+ # tag_formatter.add(["password", "email"]) { |value| "***" }
10
+ # tag_formatter.add("finished_at", Lumberjack::Formatter::DateTimeFormatter.new("%Y-%m-%dT%H:%m:%S%z"))
11
+ class TagFormatter
12
+ def initialize
13
+ @formatters = {}
14
+ @default_formatter = nil
15
+ end
16
+
17
+ # Add a default formatter applied to all tag values. This can either be a Lumberjack::Formatter
18
+ # or an object that responds to `call` or a block.
19
+ def default(formatter = nil, &block)
20
+ formatter ||= block
21
+ formatter = dereference_formatter(formatter)
22
+ @default_formatter = formatter
23
+ self
24
+ end
25
+
26
+ # Remove the default formatter.
27
+ def remove_default
28
+ @default_formatter = nil
29
+ self
30
+ end
31
+
32
+ # Add a formatter for specific tag names. This can either be a Lumberjack::Formatter
33
+ # or an object that responds to `call` or a block. The default formatter will not be
34
+ # applied.
35
+ def add(names, formatter = nil, &block)
36
+ formatter ||= block
37
+ formatter = dereference_formatter(formatter)
38
+ if formatter.nil?
39
+ remove(key)
40
+ else
41
+ Array(names).each do |name|
42
+ @formatters[name.to_s] = formatter
43
+ end
44
+ end
45
+ self
46
+ end
47
+
48
+ # Remove formatters for specific tag names. The default formatter will still be applied.
49
+ def remove(names)
50
+ Array(names).each do |name|
51
+ @formatters.delete(name.to_s)
52
+ end
53
+ self
54
+ end
55
+
56
+ # Remove all formatters.
57
+ def clear
58
+ @default_formatter = nil
59
+ @formatters.clear
60
+ self
61
+ end
62
+
63
+ # Format a hash of tags using the formatters
64
+ def format(tags)
65
+ return nil if tags.nil?
66
+ if @default_formatter.nil? && (@formatters.empty? || (@formatters.keys & tags.keys).empty?)
67
+ tags
68
+ else
69
+ formatted = {}
70
+ tags.each do |name, value|
71
+ formatter = (@formatters[name.to_s] || @default_formatter)
72
+ if formatter.is_a?(Lumberjack::Formatter)
73
+ value = formatter.format(value)
74
+ elsif formatter.respond_to?(:call)
75
+ value = formatter.call(value)
76
+ end
77
+ formatted[name.to_s] = value
78
+ end
79
+ formatted
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def dereference_formatter(formatter)
86
+ if formatter.is_a?(TaggedLoggerSupport::Formatter)
87
+ formatter.__formatter
88
+ elsif formatter.is_a?(Symbol)
89
+ formatter_class_name = "#{formatter.to_s.gsub(/(^|_)([a-z])/) { |m| $~[2].upcase }}Formatter"
90
+ Formatter.const_get(formatter_class_name).new
91
+ else
92
+ formatter
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "forwardable"
5
+
6
+ module Lumberjack
7
+ # Methods to make Lumberjack::Logger API compatible with ActiveSupport::TaggedLogger.
8
+ module TaggedLoggerSupport
9
+ class Formatter < DelegateClass(Lumberjack::Formatter)
10
+ extend Forwardable
11
+ def_delegators :@logger, :tagged, :push_tags, :pop_tags, :clear_tags!
12
+
13
+ def initialize(formatter:, logger:)
14
+ @logger = logger
15
+ @formatter = formatter
16
+ super(formatter)
17
+ end
18
+
19
+ def current_tags
20
+ tags = @logger.instance_variable_get(:@tags)
21
+ if tags.is_a?(Hash)
22
+ Array(tags["tagged"])
23
+ else
24
+ []
25
+ end
26
+ end
27
+
28
+ def tags_text
29
+ tags = current_tags
30
+ if tags.any?
31
+ tags.collect { |tag| "[#{tag}] " }.join
32
+ end
33
+ end
34
+
35
+ def __formatter
36
+ @formatter
37
+ end
38
+ end
39
+
40
+ # Compatibility with ActiveSupport::TaggedLogging which only supports adding tags as strings.
41
+ # If a tag looks like "key:value" or "key=value", it will be added as a key value pair.
42
+ # Otherwise it will be appended to a list named "tagged".
43
+ def tagged(*tags, &block)
44
+ tag_hash = {}
45
+ tags.flatten.each do |tag|
46
+ tagged_values = Array(tag_hash["tagged"] || self.tags["tagged"])
47
+ tag_hash["tagged"] = tagged_values + [tag]
48
+ end
49
+ tag(tag_hash, &block)
50
+ end
51
+
52
+ def push_tags(*tags)
53
+ tagged(*tags)
54
+ end
55
+
56
+ def pop_tags(size = 1)
57
+ tagged_values = Array(@tags["tagged"])
58
+ tagged_values = (tagged_values.size > size ? tagged_values[0, tagged_values.size - size] : nil)
59
+ tag("tagged" => tagged_values)
60
+ end
61
+
62
+ def clear_tags!
63
+ tag("tagged" => nil)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # Monkey patch for ActiveSupport::TaggedLogger so it doesn't blow up when
5
+ # a Lumberjack logger is trying to be wrapped. This module will be automatically
6
+ # included in ActiveSupport::TaggedLogger if activesupport is already loaded.
7
+ module TaggedLogging
8
+ class << self
9
+ def included(base)
10
+ base.singleton_class.send(:prepend, ClassMethods)
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def new(logger)
16
+ if logger.is_a?(Lumberjack::Logger)
17
+ logger = logger.tagged_logger! unless logger.respond_to?(:tagged)
18
+ logger
19
+ else
20
+ super
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ if defined?(ActiveSupport::TaggedLogging)
28
+ ActiveSupport::TaggedLogging.include(Lumberjack::TaggedLogging)
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ class Tags
5
+ class << self
6
+ # Transform hash keys to strings. This method exists for optimization and backward compatibility.
7
+ # If a hash already has string keys, it will be returned as is.
8
+ def stringify_keys(hash)
9
+ return nil if hash.nil?
10
+ if hash.keys.all? { |key| key.is_a?(String) }
11
+ hash
12
+ elsif hash.respond_to?(:transform_keys)
13
+ hash.transform_keys(&:to_s)
14
+ else
15
+ copy = {}
16
+ hash.each do |key, value|
17
+ copy[key.to_s] = value
18
+ end
19
+ copy
20
+ end
21
+ end
22
+
23
+ # Ensure keys are strings and expand any values in a hash that are Proc's by calling them and replacing
24
+ # the value with the result. This allows setting global tags with runtime values.
25
+ def expand_runtime_values(hash)
26
+ return nil if hash.nil?
27
+ if hash.all? { |key, value| key.is_a?(String) && !value.is_a?(Proc) }
28
+ return hash
29
+ end
30
+
31
+ copy = {}
32
+ hash.each do |key, value|
33
+ if value.is_a?(Proc) && (value.arity == 0 || value.arity == -1)
34
+ value = value.call
35
+ end
36
+ copy[key.to_s] = value
37
+ end
38
+ copy
39
+ end
40
+ end
41
+ end
42
+ end
@@ -4,70 +4,118 @@ module Lumberjack
4
4
  # A template converts entries to strings. Templates can contain the following place holders to
5
5
  # reference log entry values:
6
6
  #
7
- # * <tt>:time</tt>
8
- # * <tt>:severity</tt>
9
- # * <tt>:progname</tt>
10
- # * <tt>:unit_of_work_id</tt>
11
- # * <tt>:message</tt>
7
+ # * :time
8
+ # * :severity
9
+ # * :progname
10
+ # * :tags
11
+ # * :message
12
+ #
13
+ # Any other words prefixed with a colon will be substituted with the value of the tag with that name.
14
+ # If your tag name contains characters other than alpha numerics and the underscore, you must surround it
15
+ # with curly brackets: `:{http.request-id}`.
12
16
  class Template
13
- TEMPLATE_ARGUMENT_ORDER = %w(:time :severity :progname :pid :unit_of_work_id :message).freeze
14
- DEFAULT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S."
15
- MILLISECOND_FORMAT = "%03d"
16
- MICROSECOND_FORMAT = "%06d"
17
-
17
+ TEMPLATE_ARGUMENT_ORDER = %w[:time :severity :progname :pid :message :tags].freeze
18
+ MILLISECOND_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%3N"
19
+ MICROSECOND_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N"
20
+ PLACEHOLDER_PATTERN = /:(([a-z0-9_]+)|({[^}]+}))/i.freeze
21
+
18
22
  # Create a new template from the markup. The +first_line+ argument is used to format only the first
19
23
  # line of a message. Additional lines will be added to the message unformatted. If you wish to format
20
- # the additional lines, use the <tt>:additional_lines</tt> options to specify a template. Note that you'll need
24
+ # the additional lines, use the :additional_lines options to specify a template. Note that you'll need
21
25
  # to provide the line separator character in this template if you want to keep the message on multiple lines.
22
26
  #
23
27
  # The time will be formatted as YYYY-MM-DDTHH:MM:SSS.SSS by default. If you wish to change the format, you
24
- # can specify the <tt>:time_format</tt> option which can be either a time format template as documented in
28
+ # can specify the :time_format option which can be either a time format template as documented in
25
29
  # +Time#strftime+ or the values +:milliseconds+ or +:microseconds+ to use the standard format with the
26
30
  # specified precision.
27
31
  #
28
32
  # Messages will have white space stripped from both ends.
29
33
  def initialize(first_line, options = {})
30
- @first_line_template = compile(first_line)
34
+ @first_line_template, @first_line_tags = compile(first_line)
31
35
  additional_lines = options[:additional_lines] || "#{Lumberjack::LINE_SEPARATOR}:message"
32
- @additional_line_template = compile(additional_lines)
36
+ @additional_line_template, @additional_line_tags = compile(additional_lines)
33
37
  # Formatting the time is relatively expensive, so only do it if it will be used
34
38
  @template_include_time = first_line.include?(":time") || additional_lines.include?(":time")
35
- @time_format = options[:time_format] || :milliseconds
39
+ self.datetime_format = (options[:time_format] || :milliseconds)
40
+ end
41
+
42
+ def datetime_format=(format)
43
+ if format == :milliseconds
44
+ format = MILLISECOND_TIME_FORMAT
45
+ elsif format == :microseconds
46
+ format = MICROSECOND_TIME_FORMAT
47
+ end
48
+ @time_formatter = Formatter::DateTimeFormatter.new(format)
36
49
  end
37
-
50
+
51
+ def datetime_format
52
+ @time_formatter.format
53
+ end
54
+
38
55
  # Convert an entry into a string using the template.
39
56
  def call(entry)
40
- lines = entry.message.strip.split(Lumberjack::LINE_SEPARATOR)
41
- formatted_time = format_time(entry.time) if @template_include_time
42
- message = @first_line_template % [formatted_time, entry.severity_label, entry.progname, entry.pid, entry.unit_of_work_id, lines.shift]
43
- lines.each do |line|
44
- message << @additional_line_template % [formatted_time, entry.severity_label, entry.progname, entry.pid, entry.unit_of_work_id, line]
57
+ return entry unless entry.is_a?(LogEntry)
58
+
59
+ first_line = entry.message.to_s
60
+ additional_lines = nil
61
+ if first_line.include?(Lumberjack::LINE_SEPARATOR)
62
+ additional_lines = first_line.split(Lumberjack::LINE_SEPARATOR)
63
+ first_line = additional_lines.shift
64
+ end
65
+
66
+ formatted_time = @time_formatter.call(entry.time) if @template_include_time
67
+ format_args = [formatted_time, entry.severity_label, entry.progname, entry.pid, first_line]
68
+ tag_arguments = tag_args(entry.tags, @first_line_tags)
69
+ message = (@first_line_template % (format_args + tag_arguments))
70
+ message.rstrip! if message.end_with?(" ")
71
+
72
+ if additional_lines && !additional_lines.empty?
73
+ tag_arguments = tag_args(entry.tags, @additional_line_tags) unless @additional_line_tags == @first_line_tags
74
+ additional_lines.each do |line|
75
+ format_args[format_args.size - 1] = line
76
+ line_message = (@additional_line_template % (format_args + tag_arguments)).rstrip
77
+ line_message.rstrip! if line_message.end_with?(" ")
78
+ message << line_message
79
+ end
45
80
  end
46
81
  message
47
82
  end
48
-
83
+
49
84
  private
50
85
 
51
- def format_time(time) #:nodoc:
52
- if @time_format.is_a?(String)
53
- time.strftime(@time_format)
54
- elsif @time_format == :milliseconds
55
- time.strftime(DEFAULT_TIME_FORMAT) << MILLISECOND_FORMAT % (time.usec / 1000.0).round
56
- else
57
- time.strftime(DEFAULT_TIME_FORMAT) << MICROSECOND_FORMAT % time.usec
86
+ def tag_args(tags, tag_vars)
87
+ return [nil] * (tag_vars.size + 1) if tags.nil? || tags.size == 0
88
+
89
+ tags_string = ""
90
+ tags.each do |name, value|
91
+ unless value.nil? || tag_vars.include?(name)
92
+ value = value.to_s
93
+ value = value.gsub(Lumberjack::LINE_SEPARATOR, " ") if value.include?(Lumberjack::LINE_SEPARATOR)
94
+ tags_string << "[#{name}:#{value}] "
95
+ end
96
+ end
97
+
98
+ args = [tags_string.chop]
99
+ tag_vars.each do |name|
100
+ args << tags[name]
58
101
  end
102
+ args
59
103
  end
60
-
104
+
61
105
  # Compile the template string into a value that can be used with sprintf.
62
106
  def compile(template) #:nodoc:
63
- template.gsub(/:[a-z0-9_]+/) do |match|
64
- position = TEMPLATE_ARGUMENT_ORDER.index(match)
107
+ tag_vars = []
108
+ template = template.gsub(PLACEHOLDER_PATTERN) do |match|
109
+ var_name = match.sub("{", "").sub("}", "")
110
+ position = TEMPLATE_ARGUMENT_ORDER.index(var_name)
65
111
  if position
66
112
  "%#{position + 1}$s"
67
113
  else
68
- match
114
+ tag_vars << var_name[1, var_name.length]
115
+ "%#{TEMPLATE_ARGUMENT_ORDER.size + tag_vars.size}$s"
69
116
  end
70
117
  end
118
+ [template, tag_vars]
71
119
  end
72
120
  end
73
121
  end