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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5c096685f8199a216c865178cc4a5633975fa45e9b158e0a25254ec8edaa6b5
4
- data.tar.gz: 48e24dcd35809d47677964fe48df6d9e6c32bc0bda00cbfe3ee0e0dd76681660
3
+ metadata.gz: e066ca44b228d611e0170f99488de6fa110d6ad2bdfea5de5e5f9e7513143019
4
+ data.tar.gz: d563a0fa78660c91e0757a66194269cfc565075d5a6f69356ebd1aec2cb18d6e
5
5
  SHA512:
6
- metadata.gz: f298b5262eb94c6312122519bfcc17ced78512dfe53fe1db097c95aa68aa6cf717e7da0bde929e74c6d5df9f58938ce22e4e66167d96031e9d1530baded90311
7
- data.tar.gz: e8413dddf934581b0fadaca47c108f53145ba8a5ad4603d9b02cfa0a6fbe89c1b29c933c51205a5e171619e69502a3e1f9658e6b19b763306e52f04768b4c0ce
6
+ metadata.gz: f856780a1ef1fd89828a835c27f7c33b1fd4c5e518c5be8d9251c503d60e75f73b0a8b30e9dbeffdfea90dcc05bf9848fd31fb7ce972eba4b96c63bef6320165
7
+ data.tar.gz: 835f8a12cb10da5a459d4a8cc0b32f8e0c29afff000db89a1014c3686709bf6cd074e344ebcf6a4378c285f024c481df483527a8ec7e92b8fe79aad360628d4a
data/CHANGELOG.md CHANGED
@@ -1,13 +1,38 @@
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.1 2022-11-23
4
+
5
+
6
+ ### Fixed
7
+
8
+ - Support for `log_if` in proxied loggers (via 81115320b490034ddf9dfe4f3775322b9271e0cd) (@solnic)
9
+ - Support exceptions and payloads in proxied loggers (via 93b3fd59ebbdc7e63620eb064694d58455df831f) (@solnic)
10
+
11
+
12
+ [Compare v1.0.0...v1.0.1](https://github.com/dry-rb/dry-logger/compare/v1.0.0...v1.0.1)
13
+
14
+ ## 1.0.0 2022-11-17
4
15
 
5
16
  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.
17
+ dispatchers that can log to different destinations and plenty more.
7
18
 
8
19
 
9
20
  ### Added
10
21
 
22
+ - Support arbitrary logging backends through proxy (via #12) (@solnic)
23
+ - Support for conditional logging when using arbitrary logging backends (via #13) (@solnic)
24
+ - Support for registering templates via `Dry::Logger.register_template` (via #14) (@solnic)
25
+ - Support for payload keys as template tokens (via #14) (@solnic)
26
+ - Support for payload value formatter methods, ie if there's `:verb` token your formatter can implement `format_verb(value)` (via #14) (@solnic)
27
+ - Support block-based setup (via #16) (@solnic)
28
+ - Support for defining cherry-picked keys from the payload in string templates (via #17) (@solnic)
29
+ - 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)
30
+ - Support for colorized output using color tags in templates (via #18) (@solnic)
31
+ - Support for `colorize: true` logger option which enables severity coloring in string formatter (via #18) (@solnic)
32
+ - `:details` template: `"[%<progname>s] [%<severity>s] [%<time>s] %<message>s %<payload>s"` (@solnic)
33
+ - A new option `on_crash` for setting up a logger-crash handling proc (via #21) (@solnic)
34
+ - Handle logger crashes by default using a simple `$stdout` logger (via #21) (@solnic)
35
+ - Support for regular logger backends that don't support `log?` predicate (@solnic)
11
36
  - Support for providing a string template for log entries via `template` option (via #7) (@solnic)
12
37
  - `:rack` string log formatter which inlines request info and displays params at the end (@solnic)
13
38
  - 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,65 @@
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
+ LOG_METHODS.each do |method|
19
+ define_method(method) do |entry|
20
+ if entry.exception?
21
+ if __supports_payload__?(method)
22
+ __getobj__.public_send(method, entry.exception, **entry.payload.except(:exception))
23
+ else
24
+ __getobj__.public_send(method, entry.exception)
25
+ end
26
+ elsif __supports_payload__?(method)
27
+ if entry.message
28
+ __getobj__.public_send(method, entry.message, **entry.payload)
29
+ else
30
+ __getobj__.public_send(method, **entry.payload)
31
+ end
32
+ else
33
+ __getobj__.public_send(method, entry.message)
34
+ end
35
+ end
36
+ end
37
+
38
+ # @since 1.0.0
39
+ # @api private
40
+ def log?(entry)
41
+ if log_if
42
+ log_if.call(entry)
43
+ else
44
+ true
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # @since 1.0.0
51
+ # @api private
52
+ def __supports_payload__?(method)
53
+ __supported_methods__[method] ||= __getobj__.method(method)
54
+ .parameters.last&.first.equal?(:keyrest)
55
+ end
56
+
57
+ # @since 1.0.0
58
+ # @api private
59
+ def __supported_methods__
60
+ @__supported_methods__ ||= {}
61
+ end
62
+ end
63
+ end
64
+ end
65
+ 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
@@ -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,17 +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
216
  each_backend do |backend|
165
- backend.__send__(severity, entry) if !backend.respond_to?(:log?) || backend.log?(entry)
217
+ backend.__send__(severity, entry) if backend.log?(entry)
218
+ rescue StandardError => e
219
+ on_crash.(progname: id, exception: e, message: message, payload: payload)
166
220
  end
167
221
  end
168
222
 
223
+ true
224
+ rescue StandardError => e
225
+ on_crash.(progname: id, exception: e, message: message, payload: payload)
169
226
  true
170
227
  end
171
228
 
@@ -182,11 +239,11 @@ module Dry
182
239
  #
183
240
  # @since 1.0.0
184
241
  # @api public
185
- def tagged(tag)
186
- context[:tag] = tag
242
+ def tagged(*tags)
243
+ @tags.concat(tags)
187
244
  yield
188
245
  ensure
189
- context.delete(:tag)
246
+ @tags = []
190
247
  end
191
248
 
192
249
  # Add a new backend to an existing dispatcher
@@ -200,15 +257,27 @@ module Dry
200
257
  # @return [Dispatcher]
201
258
  # @api public
202
259
  def add_backend(instance = nil, **backend_options)
203
- 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
+
204
266
  yield(backend) if block_given?
267
+
205
268
  backends << backend
206
269
  self
207
270
  end
208
271
 
272
+ # @since 1.0.0
273
+ # @api public
274
+ def inspect
275
+ %(#<#{self.class} id=#{id} options=#{options} backends=#{backends}>)
276
+ end
277
+
209
278
  # @since 1.0.0
210
279
  # @api private
211
- def each_backend(*_args, &block)
280
+ def each_backend(&block)
212
281
  mutex.synchronize do
213
282
  backends.each(&block)
214
283
  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