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
@@ -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