dry-logger 1.0.0.rc2 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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