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.
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