dry-logger 1.0.0.rc2 → 1.0.1

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.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Logger
5
+ module Formatters
6
+ # Shell colorizer
7
+ #
8
+ # This was ported from hanami-utils
9
+ #
10
+ # @since 1.0.0
11
+ # @api private
12
+ class Colors
13
+ # Unknown color code error
14
+ #
15
+ # @since 1.0.0
16
+ class UnknownColorCodeError < StandardError
17
+ def initialize(code)
18
+ super("Unknown color code: `#{code.inspect}'")
19
+ end
20
+ end
21
+
22
+ # Escapes codes for terminals to output strings in colors
23
+ #
24
+ # @since 1.2.0
25
+ # @api private
26
+ COLORS = {black: 30,
27
+ red: 31,
28
+ green: 32,
29
+ yellow: 33,
30
+ blue: 34,
31
+ magenta: 35,
32
+ cyan: 36,
33
+ gray: 37}.freeze
34
+
35
+ # @api private
36
+ def self.evaluate(input)
37
+ COLORS.keys.reduce(input.dup) { |output, color|
38
+ output.gsub!("<#{color}>", start(color))
39
+ output.gsub!("</#{color}>", stop)
40
+ output
41
+ }
42
+ end
43
+
44
+ # Colorizes output
45
+ # 8 colors available: black, red, green, yellow, blue, magenta, cyan, and gray
46
+ #
47
+ # @param input [#to_s] the string to colorize
48
+ # @param color [Symbol] the color
49
+ #
50
+ # @raise [UnknownColorError] if the color code is unknown
51
+ #
52
+ # @return [String] the colorized string
53
+ #
54
+ # @since 1.0.0
55
+ # @api private
56
+ def self.call(color, input)
57
+ "#{start(color)}#{input}#{stop}"
58
+ end
59
+
60
+ # @since 1.0.0
61
+ # @api private
62
+ def self.start(color)
63
+ "\e[#{self[color]}m"
64
+ end
65
+
66
+ # @since 1.0.0
67
+ # @api private
68
+ def self.stop
69
+ "\e[0m"
70
+ end
71
+
72
+ # Helper method to translate between color names and terminal escape codes
73
+ #
74
+ # @since 1.0.0
75
+ # @api private
76
+ #
77
+ # @raise [UnknownColorError] if the color code is unknown
78
+ def self.[](code)
79
+ COLORS.fetch(code) { raise UnknownColorCodeError, code }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+
5
+ require "dry/logger/constants"
4
6
  require "dry/logger/formatters/structured"
5
7
 
6
8
  module Dry
@@ -16,7 +18,31 @@ module Dry
16
18
  # @since 0.1.0
17
19
  # @api private
18
20
  def format(entry)
19
- "#{::JSON.generate(entry.as_json)}#{NEW_LINE}"
21
+ hash = format_values(entry).compact
22
+ hash.update(hash.delete(:exception)) if entry.exception?
23
+ ::JSON.dump(hash)
24
+ end
25
+
26
+ # @since 0.1.0
27
+ # @api private
28
+ def format_severity(value)
29
+ value.upcase
30
+ end
31
+
32
+ # @since 0.1.0
33
+ # @api private
34
+ def format_exception(value)
35
+ {
36
+ exception: value.class,
37
+ message: value.message,
38
+ backtrace: value.backtrace || EMPTY_ARRAY
39
+ }
40
+ end
41
+
42
+ # @since 0.1.0
43
+ # @api private
44
+ def format_time(value)
45
+ value.getutc.iso8601
20
46
  end
21
47
  end
22
48
  end
@@ -12,10 +12,18 @@ module Dry
12
12
  #
13
13
  # @see String
14
14
  class Rack < String
15
+ # @see String#initialize
15
16
  # @since 1.0.0
16
17
  # @api private
17
- def format_entry(entry)
18
- [*entry.payload.except(:params).values, entry[:params]].compact.join(SEPARATOR)
18
+ def initialize(**options)
19
+ super
20
+ @template = Template[Logger.templates[:rack]]
21
+ end
22
+
23
+ # @api 1.0.0
24
+ # @api private
25
+ def format_params(value)
26
+ return value unless value.empty?
19
27
  end
20
28
  end
21
29
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/logger/formatters/structured"
3
+ require "set"
4
+
5
+ require_relative "template"
6
+ require_relative "structured"
4
7
 
5
8
  module Dry
6
9
  module Logger
@@ -12,10 +15,6 @@ module Dry
12
15
  # @since 1.0.0
13
16
  # @api public
14
17
  class String < Structured
15
- # @since 1.0.0
16
- # @api private
17
- SEPARATOR = " "
18
-
19
18
  # @since 1.0.0
20
19
  # @api private
21
20
  HASH_SEPARATOR = ","
@@ -24,9 +23,16 @@ module Dry
24
23
  # @api private
25
24
  EXCEPTION_SEPARATOR = ": "
26
25
 
27
- # @since 1.0.0
26
+ # @since 1.2.0
28
27
  # @api private
29
- DEFAULT_TEMPLATE = "%<message>s"
28
+ DEFAULT_SEVERITY_COLORS = {
29
+ DEBUG => :cyan,
30
+ INFO => :magenta,
31
+ WARN => :yellow,
32
+ ERROR => :red,
33
+ FATAL => :red,
34
+ UNKNOWN => :blue
35
+ }.freeze
30
36
 
31
37
  # @since 1.0.0
32
38
  # @api private
@@ -34,47 +40,108 @@ module Dry
34
40
 
35
41
  # @since 1.0.0
36
42
  # @api private
37
- def initialize(template: DEFAULT_TEMPLATE, **options)
43
+ def initialize(template: Logger.templates[:default], **options)
38
44
  super(**options)
39
- @template = template
45
+ @template = Template[template]
46
+ end
47
+
48
+ # @since 1.0.0
49
+ # @api private
50
+ def colorize?
51
+ options[:colorize].equal?(true)
40
52
  end
41
53
 
42
54
  private
43
55
 
44
56
  # @since 1.0.0
45
57
  # @api private
46
- def format(entry)
47
- "#{template % entry.meta.merge(message: format_entry(entry))}#{NEW_LINE}"
58
+ def format_severity(value)
59
+ output = value.upcase
60
+
61
+ if colorize?
62
+ Colors.call(severity_colors[LEVELS[value]], output)
63
+ else
64
+ output
65
+ end
48
66
  end
49
67
 
50
68
  # @since 1.0.0
51
69
  # @api private
52
- def format_entry(entry)
70
+ def format(entry)
53
71
  if entry.exception?
54
- format_exception(entry)
55
- elsif entry.message
56
- if entry.payload.empty?
57
- entry.message
58
- else
59
- "#{entry.message}#{SEPARATOR}#{format_payload(entry)}"
60
- end
72
+ head = template % template_data(entry, exclude: %i[exception])
73
+ tail = format_exception(entry.exception)
74
+
75
+ "#{head}#{NEW_LINE}#{TAB}#{tail}"
61
76
  else
62
- format_payload(entry)
77
+ template % template_data(entry)
63
78
  end
64
79
  end
65
80
 
66
81
  # @since 1.0.0
67
82
  # @api private
68
- def format_exception(entry)
69
- hash = entry.payload
70
- message = hash.values_at(:error, :message).compact.join(EXCEPTION_SEPARATOR)
71
- "#{message}#{NEW_LINE}#{hash[:backtrace].map { |line| "from #{line}" }.join(NEW_LINE)}"
83
+ def format_tags(value)
84
+ Array(value)
85
+ .map { |tag|
86
+ case tag
87
+ when Hash then format_payload(tag)
88
+ else
89
+ tag.to_s
90
+ end
91
+ }
92
+ .join(SEPARATOR)
93
+ end
94
+
95
+ # @since 1.0.0
96
+ # @api private
97
+ def format_exception(value)
98
+ [
99
+ "#{value.message} (#{value.class})",
100
+ format_backtrace(value.backtrace || EMPTY_BACKTRACE)
101
+ ].join(NEW_LINE)
102
+ end
103
+
104
+ # @since 1.0.0
105
+ # @api private
106
+ def format_payload(payload)
107
+ payload.map { |key, value| "#{key}=#{value.inspect}" }.join(SEPARATOR)
108
+ end
109
+
110
+ # @since 1.0.0
111
+ # @api private
112
+ def format_backtrace(value)
113
+ value.map { |line| "#{TAB}#{line}" }.join(NEW_LINE)
114
+ end
115
+
116
+ # @since 1.0.0
117
+ # @api private
118
+ def template_data(entry, exclude: EMPTY_ARRAY)
119
+ data = format_values(entry)
120
+ payload = data.except(:message, *entry.meta.keys, *template.tokens, *exclude)
121
+ data[:payload] = format_payload(payload)
122
+
123
+ if template.include?(:tags)
124
+ data[:tags] = format_tags(entry.tags)
125
+ end
126
+
127
+ if data[:message]
128
+ data.except(*payload.keys)
129
+ elsif template.include?(:message)
130
+ data[:message] = data.delete(:payload)
131
+ data[:payload] = nil
132
+ data
133
+ else
134
+ data
135
+ end
72
136
  end
73
137
 
74
138
  # @since 1.0.0
75
139
  # @api private
76
- def format_payload(entry)
77
- entry.map { |key, value| "#{key}=#{value.inspect}" }.join(HASH_SEPARATOR)
140
+ def severity_colors
141
+ @severity_colors ||= DEFAULT_SEVERITY_COLORS.merge(
142
+ (options[:severity_colors] || EMPTY_HASH)
143
+ .to_h { |key, value| [LEVELS[key.to_s], value] }
144
+ )
78
145
  end
79
146
  end
80
147
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "logger"
4
+
5
+ require "dry/logger/constants"
4
6
  require "dry/logger/filter"
5
7
 
6
8
  module Dry
@@ -8,6 +10,8 @@ module Dry
8
10
  module Formatters
9
11
  # Default structured formatter which receives {Logger::Entry} from the backends.
10
12
  #
13
+ # This class can be used as the base class for your custom formatters.
14
+ #
11
15
  # @see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger/Formatter.html
12
16
  #
13
17
  # @since 1.0.0
@@ -21,10 +25,6 @@ module Dry
21
25
  # @api private
22
26
  NOOP_FILTER = -> message { message }
23
27
 
24
- # @since 1.0.0
25
- # @api private
26
- NEW_LINE = $/ # rubocop:disable Style/SpecialGlobalVars
27
-
28
28
  # @since 1.0.0
29
29
  # @api private
30
30
  attr_reader :filter
@@ -52,7 +52,7 @@ module Dry
52
52
  # @return [String]
53
53
  # @api public
54
54
  def call(_severity, _time, _progname, entry)
55
- format(entry.filter(filter))
55
+ format(entry.filter(filter)) + NEW_LINE
56
56
  end
57
57
 
58
58
  # Format entry into a loggable object
@@ -63,7 +63,18 @@ module Dry
63
63
  # @return [Entry]
64
64
  # @api public
65
65
  def format(entry)
66
+ format_values(entry)
67
+ end
68
+
69
+ # @since 1.0.0
70
+ # @api private
71
+ def format_values(entry)
66
72
  entry
73
+ .to_h
74
+ .map { |key, value|
75
+ [key, respond_to?(meth = "format_#{key}", true) ? __send__(meth, value) : value]
76
+ }
77
+ .to_h
67
78
  end
68
79
  end
69
80
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "dry/logger/constants"
5
+ require_relative "colors"
6
+
7
+ module Dry
8
+ module Logger
9
+ module Formatters
10
+ # Basic string formatter.
11
+ #
12
+ # This formatter returns log entries in key=value format.
13
+ #
14
+ # @since 1.0.0
15
+ # @api public
16
+ class Template
17
+ # @since 1.0.0
18
+ # @api private
19
+ TOKEN_REGEXP = /%<(\w*)>s/.freeze
20
+
21
+ # @since 1.0.0
22
+ # @api private
23
+ MESSAGE_TOKEN = "%<message>s"
24
+
25
+ # @since 1.0.0
26
+ # @api private
27
+ attr_reader :value
28
+
29
+ # @since 1.0.0
30
+ # @api private
31
+ attr_reader :tokens
32
+
33
+ # @since 1.0.0
34
+ # @api private
35
+ def self.[](value)
36
+ cache.fetch(value) {
37
+ cache[value] = (colorized?(value) ? Template::Colorized : Template).new(value)
38
+ }
39
+ end
40
+
41
+ # @since 1.0.0
42
+ # @api private
43
+ private_class_method def self.colorized?(value)
44
+ Colors::COLORS.keys.any? { |color| value.include?("<#{color}>") }
45
+ end
46
+
47
+ # @since 1.0.0
48
+ # @api private
49
+ private_class_method def self.cache
50
+ @cache ||= {}
51
+ end
52
+
53
+ # @since 1.0.0
54
+ # @api private
55
+ class Colorized < Template
56
+ # @since 1.0.0
57
+ # @api private
58
+ def initialize(value)
59
+ super(Colors.evaluate(value))
60
+ end
61
+ end
62
+
63
+ # @since 1.0.0
64
+ # @api private
65
+ def initialize(value)
66
+ @value = value
67
+ @tokens = value.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set
68
+ end
69
+
70
+ # @since 1.0.0
71
+ # @api private
72
+ def %(tokens)
73
+ output = value % tokens
74
+ output.strip!
75
+ output.split(NEW_LINE).map(&:rstrip).join(NEW_LINE)
76
+ end
77
+
78
+ # @since 1.0.0
79
+ # @api private
80
+ def colorize(color, input)
81
+ "\e[#{Colors[color.to_sym]}m#{input}\e[0m"
82
+ end
83
+
84
+ # @since 1.0.0
85
+ # @api private
86
+ def include?(token)
87
+ tokens.include?(token)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Dry
6
+ module Logger
7
+ # Global setup methods
8
+ #
9
+ # @api public
10
+ module Global
11
+ # Register a new formatter
12
+ #
13
+ # @example
14
+ # class MyFormatter < Dry::Logger::Formatters::Structured
15
+ # def format_message(value)
16
+ # "WOAH: #{message}"
17
+ # end
18
+ #
19
+ # def format_time(value)
20
+ # Time.now.strftime("%Y-%m-%d %H:%M:%S")
21
+ # end
22
+ # end
23
+ #
24
+ # Dry::Logger.register_formatter(:my_formatter, MyFormatter)
25
+ #
26
+ # logger = Dry.Logger(:app, formatter: :my_formatter, template: :details)
27
+ #
28
+ # logger.info "Hello World"
29
+ # # [test] [INFO] [2022-11-15 10:06:29] WOAH: Hello World
30
+ #
31
+ # @since 1.0.0
32
+ # @return [Hash]
33
+ # @api public
34
+ def register_formatter(name, formatter)
35
+ formatters[name] = formatter
36
+ formatters
37
+ end
38
+
39
+ # Register a new template
40
+ #
41
+ # @example basic template
42
+ # Dry::Logger.register_template(:request, "[%<severity>s] %<verb>s %<path>s")
43
+ #
44
+ # logger = Dry.Logger(:my_app, template: :request)
45
+ #
46
+ # logger.info(verb: "GET", path: "/users")
47
+ # # [INFO] GET /users
48
+ #
49
+ # @example template with colors
50
+ # Dry::Logger.register_template(
51
+ # :request, "[%<severity>s] <green>%<verb>s</green> <blue>%<path>s</blue>"
52
+ # )
53
+ #
54
+ # @since 1.0.0
55
+ # @return [Hash]
56
+ # @api public
57
+ def register_template(name, template)
58
+ templates[name] = template
59
+ templates
60
+ end
61
+
62
+ # Build a logging backend instance
63
+ #
64
+ # @since 1.0.0
65
+ # @return [Backends::Stream]
66
+ # @api private
67
+ def new(stream: $stdout, **options)
68
+ backend =
69
+ case stream
70
+ when IO, StringIO then Backends::IO
71
+ when String, Pathname then Backends::File
72
+ else
73
+ raise ArgumentError, "unsupported stream type #{stream.class}"
74
+ end
75
+
76
+ formatter_spec = options[:formatter]
77
+ template_spec = options[:template]
78
+
79
+ template =
80
+ case template_spec
81
+ when Symbol then templates.fetch(template_spec)
82
+ when String then template_spec
83
+ when nil then templates[:default]
84
+ else
85
+ raise ArgumentError,
86
+ ":template option must be a Symbol or a String (`#{template_spec}` given)"
87
+ end
88
+
89
+ formatter_options = {**options, template: template}
90
+
91
+ formatter =
92
+ case formatter_spec
93
+ when Symbol then formatters.fetch(formatter_spec).new(**formatter_options)
94
+ when Class then formatter_spec.new(**formatter_options)
95
+ when nil then formatters[:string].new(**formatter_options)
96
+ when ::Logger::Formatter then formatter_spec
97
+ else
98
+ raise ArgumentError, "Unsupported formatter option #{formatter_spec.inspect}"
99
+ end
100
+
101
+ backend_options = options.select { |key, _| BACKEND_OPT_KEYS.include?(key) }
102
+
103
+ backend.new(stream: stream, **backend_options, formatter: formatter)
104
+ end
105
+
106
+ # Internal formatters registry
107
+ #
108
+ # @since 1.0.0
109
+ # @api private
110
+ def formatters
111
+ @formatters ||= {}
112
+ end
113
+
114
+ # Internal templates registry
115
+ #
116
+ # @since 1.0.0
117
+ # @api private
118
+ def templates
119
+ @templates ||= {}
120
+ end
121
+ end
122
+ end
123
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Logger
5
- VERSION = "1.0.0.rc2"
5
+ VERSION = "1.0.1"
6
6
  end
7
7
  end