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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 343929c403f114dcb22a13c0406ef82982800e9bed8e58d483a86ab3442d7f13
4
- data.tar.gz: 2a224fba0dc7f8d5771219ba0bee0eef0322cefe11e14fbb49ca066319e68bca
3
+ metadata.gz: 730cb488952659e0a7499948d6f77e3ed61b7be38e462d7a000479d7e6a8c7f2
4
+ data.tar.gz: 81ddc5b02fc960cb3650f1305e19bfc33d21488722455c8bfdcb3e411cb07bf4
5
5
  SHA512:
6
- metadata.gz: e945a3fde7a2bc2fb8731ae46aabac59df1c8da6208d125d09759af04cc848f5b09453c66b7f65da49ecc79094809ea76746c1610b25f47980833965f43a8037
7
- data.tar.gz: 7af951d7152c84f8dbbec03a2c57bbd527b10791a39d585597885378036e823594b5d9b50754b765a445811fc46c3fb34d546a7c33a3a75f8de60b70bad90b88
6
+ metadata.gz: 02d8e2eb6e2029a3c29269db001e5e3f50eacf1605efa4cdd62ae01a1e8349b80073e0b591761ed3bc82f7acda5b139a2a03c120c32b3716c27c2d054eee0a64
7
+ data.tar.gz: 2f11fec53ac321f0157f3c620944d460a189fa4da112a5b2d617a21171533dcacf17370bf1288d2ef1e4574ab4f35518aca2e280a7063df669768d1c3dda73b6
data/CHANGELOG.md CHANGED
@@ -1,13 +1,27 @@
1
1
  <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
2
 
3
- ## 1.0.0.rc1 2022-11-07
3
+ ## 1.0.0
4
4
 
5
5
  This is a port of the original Hanami logger from hanami-utils extended with support for logging
6
- dispatchers that can log to different destinations.
6
+ dispatchers that can log to different destinations and plenty more.
7
7
 
8
8
 
9
9
  ### Added
10
10
 
11
+ - Support arbitrary logging backends through proxy (via #12) (@solnic)
12
+ - Support for conditional logging when using arbitrary logging backends (via #13) (@solnic)
13
+ - Support for registering templates via `Dry::Logger.register_template` (via #14) (@solnic)
14
+ - Support for payload keys as template tokens (via #14) (@solnic)
15
+ - Support for payload value formatter methods, ie if there's `:verb` token your formatter can implement `format_verb(value)` (via #14) (@solnic)
16
+ - Support block-based setup (via #16) (@solnic)
17
+ - Support for defining cherry-picked keys from the payload in string templates (via #17) (@solnic)
18
+ - Support for `%<payload>s` template token. It will be replaced by a formatted payload, excluding any key that you specified explicitly in the template (via #17) (@solnic)
19
+ - Support for colorized output using color tags in templates (via #18) (@solnic)
20
+ - Support for `colorize: true` logger option which enables severity coloring in string formatter (via #18) (@solnic)
21
+ - `:details` template: `"[%<progname>s] [%<severity>s] [%<time>s] %<message>s %<payload>s"` (@solnic)
22
+ - A new option `on_crash` for setting up a logger-crash handling proc (via #21) (@solnic)
23
+ - Handle logger crashes by default using a simple `$stdout` logger (via #21) (@solnic)
24
+ - Support for regular logger backends that don't support `log?` predicate (@solnic)
11
25
  - Support for providing a string template for log entries via `template` option (via #7) (@solnic)
12
26
  - `:rack` string log formatter which inlines request info and displays params at the end (@solnic)
13
27
  - Conditional log dispatch via `#log_if` backend's predicate (via #9) (@solnic)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/logger/constants"
4
+
5
+ module Dry
6
+ module Logger
7
+ module Backends
8
+ module Core
9
+ # Return a proc used by the log? predicate
10
+ #
11
+ # @since 1.0.0
12
+ # @api private
13
+ attr_reader :log_if
14
+
15
+ # Set a predicate proc that checks if an entry should be logged by a given backend
16
+ #
17
+ # The predicate will receive {Entry} as its argument and should return true/false
18
+ #
19
+ # @param [Proc, #to_proc] spec A proc-like object
20
+ # @since 1.0.0
21
+ # @api public
22
+ def log_if=(spec)
23
+ @log_if = spec&.to_proc
24
+ end
25
+
26
+ # @since 1.0.0
27
+ # @api private
28
+ def log?(entry)
29
+ if log_if
30
+ log_if.call(entry)
31
+ else
32
+ true
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ require "dry/logger/constants"
6
+ require "dry/logger/backends/core"
7
+
8
+ module Dry
9
+ module Logger
10
+ module Backends
11
+ # Logger proxy is used for regular loggers that don't work with log entries
12
+ #
13
+ # @since 1.0.0
14
+ # @api private
15
+ class Proxy < SimpleDelegator
16
+ include Core
17
+
18
+ # @since 0.1.0
19
+ # @api public
20
+ attr_accessor :log_if
21
+
22
+ LOG_METHODS.each do |method|
23
+ define_method(method) { |entry| __getobj__.public_send(method, entry.message) }
24
+ end
25
+
26
+ # @since 1.0.0
27
+ # @api private
28
+ def log?(entry)
29
+ if log_if
30
+ log_if.call(entry)
31
+ else
32
+ true
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -3,11 +3,14 @@
3
3
  require "logger"
4
4
 
5
5
  require "dry/logger/constants"
6
+ require "dry/logger/backends/core"
6
7
 
7
8
  module Dry
8
9
  module Logger
9
10
  module Backends
10
11
  class Stream < ::Logger
12
+ include Core
13
+
11
14
  # @since 0.1.0
12
15
  # @api private
13
16
  attr_reader :stream
@@ -16,10 +19,6 @@ module Dry
16
19
  # @api private
17
20
  attr_reader :level
18
21
 
19
- # @since 0.1.0
20
- # @api public
21
- attr_accessor :log_if
22
-
23
22
  # @since 0.1.0
24
23
  # @api private
25
24
  def initialize(stream:, formatter:, level: DEFAULT_LEVEL, progname: nil, log_if: nil)
@@ -33,13 +32,9 @@ module Dry
33
32
  end
34
33
 
35
34
  # @since 1.0.0
36
- # @api private
37
- def log?(entry)
38
- if log_if
39
- log_if.call(entry)
40
- else
41
- true
42
- end
35
+ # @api public
36
+ def inspect
37
+ %(#<#{self.class} stream=#{stream} level=#{level} log_if=#{log_if}>)
43
38
  end
44
39
  end
45
40
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Logger
5
+ # @since 1.0.0
6
+ # @api private
7
+ class Clock
8
+ # @since 1.0.0
9
+ # @api private
10
+ attr_reader :unit
11
+
12
+ # @since 1.0.0
13
+ # @api private
14
+ def initialize(unit: :nanosecond)
15
+ @unit = unit
16
+ end
17
+
18
+ # @since 1.0.0
19
+ # @api private
20
+ def now
21
+ Time.now
22
+ end
23
+
24
+ # @since 1.0.0
25
+ # @api private
26
+ def now_utc
27
+ now.getutc
28
+ end
29
+
30
+ # @since 1.0.0
31
+ # @api private
32
+ def measure
33
+ start = current
34
+ result = yield
35
+ [result, current - start]
36
+ end
37
+
38
+ private
39
+
40
+ # @since 1.0.0
41
+ # @api private
42
+ def current
43
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, unit)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -4,22 +4,67 @@ require "logger"
4
4
 
5
5
  module Dry
6
6
  module Logger
7
- LOG_METHODS = %i[debug error fatal info warn].freeze
7
+ # @since 1.0.0
8
+ # @api private
9
+ NEW_LINE = $/ # rubocop:disable Style/SpecialGlobalVars
10
+
11
+ # @since 1.0.0
12
+ # @api private
13
+ SEPARATOR = " "
14
+
15
+ # @since 1.0.0
16
+ # @api private
17
+ TAB = SEPARATOR * 2
18
+
19
+ # @since 1.0.0
20
+ # @api private
21
+ EMPTY_ARRAY = [].freeze
8
22
 
23
+ # @since 1.0.0
24
+ # @api private
25
+ EMPTY_HASH = {}.freeze
26
+
27
+ # @since 1.0.0
28
+ # @api private
29
+ LOG_METHODS = %i[debug info warn error fatal unknown].freeze
30
+
31
+ # @since 1.0.0
32
+ # @api private
9
33
  BACKEND_METHODS = %i[close].freeze
10
34
 
35
+ # @since 1.0.0
36
+ # @api private
11
37
  DEBUG = ::Logger::DEBUG
38
+
39
+ # @since 1.0.0
40
+ # @api private
12
41
  INFO = ::Logger::INFO
42
+
43
+ # @since 1.0.0
44
+ # @api private
13
45
  WARN = ::Logger::WARN
46
+
47
+ # @since 1.0.0
48
+ # @api private
14
49
  ERROR = ::Logger::ERROR
50
+
51
+ # @since 1.0.0
52
+ # @api private
15
53
  FATAL = ::Logger::FATAL
54
+
55
+ # @since 1.0.0
56
+ # @api private
16
57
  UNKNOWN = ::Logger::UNKNOWN
17
58
 
59
+ # @since 1.0.0
60
+ # @api private
18
61
  LEVEL_RANGE = (DEBUG..UNKNOWN).freeze
19
62
 
63
+ # @since 1.0.0
64
+ # @api private
20
65
  DEFAULT_LEVEL = INFO
21
66
 
22
- # @since 0.1.0
67
+ # @since 1.0.0
23
68
  # @api private
24
69
  LEVELS = Hash
25
70
  .new { |levels, key|
@@ -35,9 +80,16 @@ module Dry
35
80
  )
36
81
  .freeze
37
82
 
38
- DEFAULT_OPTS = {level: DEFAULT_LEVEL, formatter: nil, progname: nil}.freeze
83
+ # @since 1.0.0
84
+ # @api private
85
+ DEFAULT_OPTS = {level: DEFAULT_LEVEL, formatter: nil, progname: nil, log_if: nil}.freeze
39
86
 
87
+ # @since 1.0.0
88
+ # @api private
40
89
  BACKEND_OPT_KEYS = DEFAULT_OPTS.keys.freeze
90
+
91
+ # @since 1.0.0
92
+ # @api private
41
93
  FORMATTER_OPT_KEYS = %i[filter].freeze
42
94
  end
43
95
  end
@@ -4,6 +4,7 @@ require "logger"
4
4
  require "pathname"
5
5
 
6
6
  require "dry/logger/constants"
7
+ require "dry/logger/backends/proxy"
7
8
  require "dry/logger/entry"
8
9
 
9
10
  module Dry
@@ -37,21 +38,48 @@ module Dry
37
38
  # @api private
38
39
  attr_reader :options
39
40
 
41
+ # @since 1.0.0
42
+ # @api private
43
+ attr_reader :clock
44
+
45
+ # @since 1.0.0
46
+ # @api private
47
+ attr_reader :on_crash
48
+
40
49
  # @since 1.0.0
41
50
  # @api private
42
51
  attr_reader :mutex
43
52
 
53
+ # @since 1.0.0
54
+ # @api private
55
+ CRASH_LOGGER = ::Logger.new($stdout).tap { |logger|
56
+ logger.formatter = -> (_, _, _, message) { "#{message}#{NEW_LINE}" }
57
+ logger.level = FATAL
58
+ }.freeze
59
+
60
+ # @since 1.0.0
61
+ # @api private
62
+ ON_CRASH = -> (progname:, exception:, message:, payload:) {
63
+ CRASH_LOGGER.fatal(Logger.templates[:crash] % {
64
+ severity: "FATAL",
65
+ progname: progname,
66
+ time: Time.now,
67
+ log_entry: [message, payload].map(&:to_s).reject(&:empty?).join(SEPARATOR),
68
+ exception: exception.class,
69
+ message: exception.message,
70
+ backtrace: TAB + exception.backtrace.join(NEW_LINE + TAB)
71
+ })
72
+ }
73
+
44
74
  # Set up a dispatcher
45
75
  #
46
76
  # @since 1.0.0
47
- #
48
- # @param [String, Symbol] id The dispatcher id, can be used as progname in log entries
49
- # @param [Hash] options Options that can be used for both the backend and formatter
77
+ # @api private
50
78
  #
51
79
  # @return [Dispatcher]
52
- # @api public
53
80
  def self.setup(id, **options)
54
81
  dispatcher = new(id, **DEFAULT_OPTS, **options)
82
+ yield(dispatcher) if block_given?
55
83
  dispatcher.add_backend if dispatcher.backends.empty?
56
84
  dispatcher
57
85
  end
@@ -64,12 +92,17 @@ module Dry
64
92
 
65
93
  # @since 1.0.0
66
94
  # @api private
67
- def initialize(id, backends: [], context: self.class.default_context, **options)
95
+ def initialize(
96
+ id, backends: [], tags: [], context: self.class.default_context, **options
97
+ )
68
98
  @id = id
69
99
  @backends = backends
70
100
  @options = {**options, progname: id}
71
101
  @mutex = Mutex.new
72
102
  @context = context
103
+ @tags = tags
104
+ @clock = Clock.new(**(options[:clock] || EMPTY_HASH))
105
+ @on_crash = options[:on_crash] || ON_CRASH
73
106
  end
74
107
 
75
108
  # Log an entry with UNKNOWN severity
@@ -128,7 +161,7 @@ module Dry
128
161
 
129
162
  BACKEND_METHODS.each do |name|
130
163
  define_method(name) do
131
- call(name)
164
+ forward(name)
132
165
  end
133
166
  end
134
167
 
@@ -143,8 +176,25 @@ module Dry
143
176
 
144
177
  # Pass logging to all configured backends
145
178
  #
179
+ # @example logging a message
180
+ # logger.log(:info, "Hello World")
181
+ #
182
+ # @example logging payload
183
+ # logger.log(:info, verb: "GET", path: "/users")
184
+ #
185
+ # @example logging message and payload
186
+ # logger.log(:info, "User index request", verb: "GET", path: "/users")
187
+ #
188
+ # @example logging exception
189
+ # begin
190
+ # # things that may raise
191
+ # rescue => e
192
+ # logger.log(:error, e)
193
+ # raise e
194
+ # end
195
+ #
146
196
  # @param [Symbol] severity The log severity name
147
- # @param [String,Symbol,Array] message Optional message object
197
+ # @param [String] message Optional message
148
198
  # @param [Hash] payload Optional log entry payload
149
199
  #
150
200
  # @since 1.0.0
@@ -155,15 +205,24 @@ module Dry
155
205
  when Hash then log(severity, nil, **message)
156
206
  else
157
207
  entry = Entry.new(
208
+ clock: clock,
158
209
  progname: id,
159
210
  severity: severity,
211
+ tags: @tags,
160
212
  message: message,
161
213
  payload: {**context, **payload}
162
214
  )
163
215
 
164
- each_backend { |backend| backend.__send__(severity, entry) if backend.log?(entry) }
216
+ each_backend do |backend|
217
+ backend.__send__(severity, entry) if backend.log?(entry)
218
+ rescue StandardError => e
219
+ on_crash.(progname: id, exception: e, message: message, payload: payload)
220
+ end
165
221
  end
166
222
 
223
+ true
224
+ rescue StandardError => e
225
+ on_crash.(progname: id, exception: e, message: message, payload: payload)
167
226
  true
168
227
  end
169
228
 
@@ -180,11 +239,11 @@ module Dry
180
239
  #
181
240
  # @since 1.0.0
182
241
  # @api public
183
- def tagged(tag)
184
- context[:tag] = tag
242
+ def tagged(*tags)
243
+ @tags.concat(tags)
185
244
  yield
186
245
  ensure
187
- context.delete(:tag)
246
+ @tags = []
188
247
  end
189
248
 
190
249
  # Add a new backend to an existing dispatcher
@@ -198,15 +257,27 @@ module Dry
198
257
  # @return [Dispatcher]
199
258
  # @api public
200
259
  def add_backend(instance = nil, **backend_options)
201
- backend = instance || Dry::Logger.new(**options, **backend_options)
260
+ backend =
261
+ case (instance ||= Dry::Logger.new(**options, **backend_options))
262
+ when Backends::Stream then instance
263
+ else Backends::Proxy.new(instance)
264
+ end
265
+
202
266
  yield(backend) if block_given?
267
+
203
268
  backends << backend
204
269
  self
205
270
  end
206
271
 
272
+ # @since 1.0.0
273
+ # @api public
274
+ def inspect
275
+ %(#<#{self.class} id=#{id} options=#{options} backends=#{backends}>)
276
+ end
277
+
207
278
  # @since 1.0.0
208
279
  # @api private
209
- def each_backend(*_args, &block)
280
+ def each_backend(&block)
210
281
  mutex.synchronize do
211
282
  backends.each(&block)
212
283
  end
@@ -217,7 +288,7 @@ module Dry
217
288
  # @since 1.0.0
218
289
  # @return [true]
219
290
  # @api private
220
- def call(meth, ...)
291
+ def forward(meth, ...)
221
292
  each_backend { |backend| backend.public_send(meth, ...) }
222
293
  true
223
294
  end
@@ -10,14 +10,6 @@ module Dry
10
10
  class Entry
11
11
  include Enumerable
12
12
 
13
- # @since 1.0.0
14
- # @api private
15
- EMPTY_PAYLOAD = {}.freeze
16
-
17
- # @since 1.0.0
18
- # @api private
19
- EMPTY_BACKTRACE = [].freeze
20
-
21
13
  # @since 1.0.0
22
14
  # @api public
23
15
  attr_reader :progname
@@ -28,16 +20,19 @@ module Dry
28
20
 
29
21
  # @since 1.0.0
30
22
  # @api public
31
- attr_reader :level
23
+ attr_reader :tags
32
24
 
33
25
  # @since 1.0.0
34
26
  # @api public
35
- attr_reader :time
27
+ attr_reader :level
36
28
 
37
29
  # @since 1.0.0
38
30
  # @api public
39
31
  attr_reader :message
40
- alias_method :exception, :message
32
+
33
+ # @since 1.0.0
34
+ # @api public
35
+ attr_reader :exception
41
36
 
42
37
  # @since 1.0.0
43
38
  # @api public
@@ -45,14 +40,23 @@ module Dry
45
40
 
46
41
  # @since 1.0.0
47
42
  # @api private
48
- def initialize(progname:, severity:, time: Time.now, message: nil, payload: EMPTY_PAYLOAD)
43
+ attr_reader :clock
44
+
45
+ # @since 1.0.0
46
+ # @api private
47
+ # rubocop:disable Metrics/ParameterLists
48
+ def initialize(clock:, progname:, severity:, tags: EMPTY_ARRAY, message: nil,
49
+ payload: EMPTY_HASH)
50
+ @clock = clock
49
51
  @progname = progname
50
- @severity = severity.to_s.upcase # TODO: this doesn't feel right
52
+ @severity = severity.to_s
53
+ @tags = tags
51
54
  @level = LEVELS.fetch(severity.to_s)
52
- @time = time
53
- @message = message
55
+ @message = message unless message.is_a?(Exception)
56
+ @exception = message if message.is_a?(Exception)
54
57
  @payload = build_payload(payload)
55
58
  end
59
+ # rubocop:enable Metrics/ParameterLists
56
60
 
57
61
  # @since 1.0.0
58
62
  # @api public
@@ -99,7 +103,7 @@ module Dry
99
103
  # @since 1.0.0
100
104
  # @api public
101
105
  def exception?
102
- message.is_a?(Exception)
106
+ !exception.nil?
103
107
  end
104
108
 
105
109
  # @since 1.0.0
@@ -109,28 +113,21 @@ module Dry
109
113
  end
110
114
 
111
115
  # @since 1.0.0
112
- # @api private
113
- def to_h
114
- @to_h ||= meta.merge(payload)
116
+ # @api public
117
+ def tag?(value)
118
+ tags.include?(value)
115
119
  end
116
120
 
117
121
  # @since 1.0.0
118
122
  # @api private
119
123
  def meta
120
- @meta ||= {progname: progname, severity: severity, time: time}
124
+ @meta ||= {progname: progname, severity: severity, time: clock.now}
121
125
  end
122
126
 
123
127
  # @since 1.0.0
124
128
  # @api private
125
- def utc_time
126
- @utc_time ||= time.utc.iso8601
127
- end
128
-
129
- # @since 1.0.0
130
- # @api private
131
- def as_json
132
- # TODO: why are we enforcing UTC in JSON but not in String?
133
- @as_json ||= to_h.merge(message: message, time: utc_time).compact
129
+ def to_h
130
+ @to_h ||= meta.merge(message: message, **payload)
134
131
  end
135
132
 
136
133
  # @since 1.0.0
@@ -146,10 +143,7 @@ module Dry
146
143
  # @api private
147
144
  def build_payload(payload)
148
145
  if exception?
149
- {message: exception.message,
150
- backtrace: exception.backtrace || EMPTY_BACKTRACE,
151
- error: exception.class,
152
- **payload}
146
+ {exception: exception, **payload}
153
147
  else
154
148
  payload
155
149
  end
@@ -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