dry-logger 1.0.0.rc1 → 1.0.0

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.
@@ -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.rc1"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
data/lib/dry/logger.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dry/logger/global"
3
4
  require "dry/logger/constants"
5
+ require "dry/logger/clock"
4
6
  require "dry/logger/dispatcher"
5
7
 
6
8
  require "dry/logger/formatters/string"
@@ -40,74 +42,58 @@ module Dry
40
42
  # logger.info(Hello: "World!")
41
43
  # # {"progname":"my_app","severity":"INFO","time":"2022-11-06T10:11:29Z","Hello":"World!"}
42
44
  #
45
+ # @example Setting up multiple backends
46
+ # logger = Dry.Logger(:my_app)
47
+ # add_backend(formatter: :string, template: :details)
48
+ # add_backend(formatter: :string, template: :details)
49
+ #
50
+ # @example Setting up conditional logging
51
+ # logger = Dry.Logger(:my_app) { |setup|
52
+ # setup.add_backend(formatter: :string, template: :details) { |b| b.log_if = :error?.to_proc }
53
+ # }
54
+ #
55
+ # @param [String, Symbol] id The dispatcher id, can be used as progname in log entries
56
+ # @param [Hash] options Options for backends and formatters
57
+ # @option options [Symbol] :level (:info) The minimum level that should be logged,
58
+ # @option options [Symbol] :stream (optional) The output stream, default is $stdout
59
+ # @option options [Symbol, Class, #call] :formatter (:string) The default formatter or its id,
60
+ # @option options [String, Symbol] :template (:default) The default template that should be used
61
+ # @option options [Boolean] :colorize (false) Enable/disable colorized severity
62
+ # @option options [Hash<Symbol=>Symbol>] :severity_colors ({}) A severity=>color mapping
63
+ # @option options [#call] :on_crash (Dry::Logger::Dispatcher::ON_CRASH) A crash-handling proc.
64
+ # This is used whenever logging crashes.
65
+ #
43
66
  # @since 1.0.0
44
- # @return [Dispatcher]
45
67
  # @api public
46
- def self.Logger(id, **opts, &block)
47
- Logger::Dispatcher.setup(id, **opts, &block)
68
+ # @return [Dispatcher]
69
+ def self.Logger(id, **options, &block)
70
+ Logger::Dispatcher.setup(id, **options, &block)
48
71
  end
49
72
 
50
73
  module Logger
51
- # Register a new formatter
52
- #
53
- # @example
54
- # class MyFormatter < Dry::Logger::Formatters::Structured
55
- # def format(entry)
56
- # "WOAH: #{entry.message}"
57
- # end
58
- # end
59
- #
60
- # Dry::Logger.register_formatter(MyFormatter)
61
- #
62
- # @since 1.0.0
63
- # @return [Hash]
64
- # @api public
65
- def self.register_formatter(name, formatter)
66
- formatters[name] = formatter
67
- formatters
68
- end
69
-
70
- # Build a logging backend instance
71
- #
72
- # @since 1.0.0
73
- # @return [Backends::Stream]
74
- # @api private
75
- def self.new(stream: $stdout, **opts)
76
- backend =
77
- case stream
78
- when IO, StringIO then Backends::IO
79
- when String, Pathname then Backends::File
80
- else
81
- raise ArgumentError, "unsupported stream type #{stream.class}"
82
- end
74
+ extend Global
83
75
 
84
- formatter_opt = opts[:formatter]
85
-
86
- formatter =
87
- case formatter_opt
88
- when Symbol then formatters.fetch(formatter_opt).new(**opts)
89
- when Class then formatter_opt.new(**opts)
90
- when NilClass then formatters[:string].new(**opts)
91
- when ::Logger::Formatter then formatter_opt
92
- else
93
- raise ArgumentError, "unsupported formatter option #{formatter_opt.inspect}"
94
- end
76
+ # Built-in formatters
77
+ register_formatter(:string, Formatters::String)
78
+ register_formatter(:rack, Formatters::Rack)
79
+ register_formatter(:json, Formatters::JSON)
95
80
 
96
- backend_opts = opts.select { |key, _| BACKEND_OPT_KEYS.include?(key) }
81
+ # Built-in templates
82
+ register_template(:default, "%<message>s %<payload>s")
97
83
 
98
- backend.new(stream: stream, **backend_opts, formatter: formatter)
99
- end
84
+ register_template(:details, "[%<progname>s] [%<severity>s] [%<time>s] %<message>s %<payload>s")
100
85
 
101
- # Internal formatters registry
102
- #
103
- # @since 1.0.0
104
- # @api private
105
- def self.formatters
106
- @formatters ||= {}
107
- end
86
+ register_template(:crash, <<~STR)
87
+ [%<progname>s] [%<severity>s] [%<time>s] Logging crashed
88
+ %<log_entry>s
89
+ %<message>s (%<exception>s)
90
+ %<backtrace>s
91
+ STR
108
92
 
109
- register_formatter(:string, Formatters::String)
110
- register_formatter(:rack, Formatters::Rack)
111
- register_formatter(:json, Formatters::JSON)
93
+ register_template(:rack, <<~STR)
94
+ [%<progname>s] [%<severity>s] [%<time>s] \
95
+ %<verb>s %<status>s %<elapsed>s %<ip>s %<path>s %<length>s %<payload>s
96
+ %<params>s
97
+ STR
112
98
  end
113
99
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.rc1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-07 00:00:00.000000000 Z
11
+ date: 2022-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -36,17 +36,23 @@ files:
36
36
  - README.md
37
37
  - dry-logger.gemspec
38
38
  - lib/dry/logger.rb
39
+ - lib/dry/logger/backends/core.rb
39
40
  - lib/dry/logger/backends/file.rb
40
41
  - lib/dry/logger/backends/io.rb
42
+ - lib/dry/logger/backends/proxy.rb
41
43
  - lib/dry/logger/backends/stream.rb
44
+ - lib/dry/logger/clock.rb
42
45
  - lib/dry/logger/constants.rb
43
46
  - lib/dry/logger/dispatcher.rb
44
47
  - lib/dry/logger/entry.rb
45
48
  - lib/dry/logger/filter.rb
49
+ - lib/dry/logger/formatters/colors.rb
46
50
  - lib/dry/logger/formatters/json.rb
47
51
  - lib/dry/logger/formatters/rack.rb
48
52
  - lib/dry/logger/formatters/string.rb
49
53
  - lib/dry/logger/formatters/structured.rb
54
+ - lib/dry/logger/formatters/template.rb
55
+ - lib/dry/logger/global.rb
50
56
  - lib/dry/logger/version.rb
51
57
  homepage: https://dry-rb.org/gems/dry-logger
52
58
  licenses:
@@ -67,9 +73,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
73
  version: '3.0'
68
74
  required_rubygems_version: !ruby/object:Gem::Requirement
69
75
  requirements:
70
- - - ">"
76
+ - - ">="
71
77
  - !ruby/object:Gem::Version
72
- version: 1.3.1
78
+ version: '0'
73
79
  requirements: []
74
80
  rubygems_version: 3.1.6
75
81
  signing_key: