dry-logger 1.0.0.rc1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 343929c403f114dcb22a13c0406ef82982800e9bed8e58d483a86ab3442d7f13
4
+ data.tar.gz: 2a224fba0dc7f8d5771219ba0bee0eef0322cefe11e14fbb49ca066319e68bca
5
+ SHA512:
6
+ metadata.gz: e945a3fde7a2bc2fb8731ae46aabac59df1c8da6208d125d09759af04cc848f5b09453c66b7f65da49ecc79094809ea76746c1610b25f47980833965f43a8037
7
+ data.tar.gz: 7af951d7152c84f8dbbec03a2c57bbd527b10791a39d585597885378036e823594b5d9b50754b765a445811fc46c3fb34d546a7c33a3a75f8de60b70bad90b88
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
+
3
+ ## 1.0.0.rc1 2022-11-07
4
+
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.
7
+
8
+
9
+ ### Added
10
+
11
+ - Support for providing a string template for log entries via `template` option (via #7) (@solnic)
12
+ - `:rack` string log formatter which inlines request info and displays params at the end (@solnic)
13
+ - Conditional log dispatch via `#log_if` backend's predicate (via #9) (@solnic)
14
+ - Add support for shared context and tagged log entries (via #10) (@solnic)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2022 dry-rb team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ <!--- this file is synced from dry-rb/template-gem project -->
2
+ [gem]: https://rubygems.org/gems/dry-logger
3
+ [actions]: https://github.com/dry-rb/dry-logger/actions
4
+ [codacy]: https://www.codacy.com/gh/dry-rb/dry-logger
5
+ [chat]: https://dry-rb.zulipchat.com
6
+ [inchpages]: http://inch-ci.org/github/dry-rb/dry-logger
7
+
8
+ # dry-logger [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
9
+
10
+ [![Gem Version](https://badge.fury.io/rb/dry-logger.svg)][gem]
11
+ [![CI Status](https://github.com/dry-rb/dry-logger/workflows/ci/badge.svg)][actions]
12
+ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/5aae4837b97044cfa4537f083ad584e9)][codacy]
13
+ [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/5aae4837b97044cfa4537f083ad584e9)][codacy]
14
+ [![Inline docs](http://inch-ci.org/github/dry-rb/dry-logger.svg?branch=main)][inchpages]
15
+
16
+ ## Links
17
+
18
+ * [User documentation](https://dry-rb.org/gems/dry-logger)
19
+ * [API documentation](http://rubydoc.info/gems/dry-logger)
20
+
21
+ ## Supported Ruby versions
22
+
23
+ This library officially supports the following Ruby versions:
24
+
25
+ * MRI `>= 2.7.0`
26
+ * jruby `>= 9.3` (postponed until 2.7 is supported)
27
+
28
+ ## License
29
+
30
+ See `LICENSE` file.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this file is synced from dry-rb/template-gem project
4
+
5
+ lib = File.expand_path("lib", __dir__)
6
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
7
+ require "dry/logger/version"
8
+
9
+ Gem::Specification.new do |spec|
10
+ spec.name = "dry-logger"
11
+ spec.authors = ["Luca Guidi"]
12
+ spec.email = ["me@lucaguidi.com"]
13
+ spec.license = "MIT"
14
+ spec.version = Dry::Logger::VERSION.dup
15
+
16
+ spec.summary = "Logging for Ruby"
17
+ spec.description = spec.summary
18
+ spec.homepage = "https://dry-rb.org/gems/dry-logger"
19
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-logger.gemspec", "lib/**/*"]
20
+ spec.bindir = "bin"
21
+ spec.executables = []
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
25
+ spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-logger/blob/main/CHANGELOG.md"
26
+ spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-logger"
27
+ spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-logger/issues"
28
+
29
+ spec.required_ruby_version = ">= 3.0"
30
+
31
+ # to update dependencies edit project.yml
32
+
33
+ spec.add_development_dependency "rspec"
34
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ require "dry/logger/backends/stream"
6
+
7
+ module Dry
8
+ module Logger
9
+ module Backends
10
+ class File < Stream
11
+ def initialize(stream:, **opts)
12
+ Pathname(stream).dirname.mkpath
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/logger/backends/stream"
4
+
5
+ module Dry
6
+ module Logger
7
+ module Backends
8
+ class IO < Stream
9
+ def close
10
+ super unless stream.equal?($stdout)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ require "dry/logger/constants"
6
+
7
+ module Dry
8
+ module Logger
9
+ module Backends
10
+ class Stream < ::Logger
11
+ # @since 0.1.0
12
+ # @api private
13
+ attr_reader :stream
14
+
15
+ # @since 0.1.0
16
+ # @api private
17
+ attr_reader :level
18
+
19
+ # @since 0.1.0
20
+ # @api public
21
+ attr_accessor :log_if
22
+
23
+ # @since 0.1.0
24
+ # @api private
25
+ def initialize(stream:, formatter:, level: DEFAULT_LEVEL, progname: nil, log_if: nil)
26
+ super(stream, progname: progname)
27
+
28
+ @stream = stream
29
+ @level = LEVELS[level]
30
+
31
+ self.log_if = log_if
32
+ self.formatter = formatter
33
+ end
34
+
35
+ # @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
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Dry
6
+ module Logger
7
+ LOG_METHODS = %i[debug error fatal info warn].freeze
8
+
9
+ BACKEND_METHODS = %i[close].freeze
10
+
11
+ DEBUG = ::Logger::DEBUG
12
+ INFO = ::Logger::INFO
13
+ WARN = ::Logger::WARN
14
+ ERROR = ::Logger::ERROR
15
+ FATAL = ::Logger::FATAL
16
+ UNKNOWN = ::Logger::UNKNOWN
17
+
18
+ LEVEL_RANGE = (DEBUG..UNKNOWN).freeze
19
+
20
+ DEFAULT_LEVEL = INFO
21
+
22
+ # @since 0.1.0
23
+ # @api private
24
+ LEVELS = Hash
25
+ .new { |levels, key|
26
+ LEVEL_RANGE.include?(key) ? key : levels.fetch(key.to_s.downcase, DEFAULT_LEVEL)
27
+ }
28
+ .update(
29
+ "debug" => DEBUG,
30
+ "info" => INFO,
31
+ "warn" => WARN,
32
+ "error" => ERROR,
33
+ "fatal" => FATAL,
34
+ "unknown" => UNKNOWN
35
+ )
36
+ .freeze
37
+
38
+ DEFAULT_OPTS = {level: DEFAULT_LEVEL, formatter: nil, progname: nil}.freeze
39
+
40
+ BACKEND_OPT_KEYS = DEFAULT_OPTS.keys.freeze
41
+ FORMATTER_OPT_KEYS = %i[filter].freeze
42
+ end
43
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "pathname"
5
+
6
+ require "dry/logger/constants"
7
+ require "dry/logger/entry"
8
+
9
+ module Dry
10
+ module Logger
11
+ # Logger dispatcher routes log entries to configured logging backends
12
+ #
13
+ # @since 1.0.0
14
+ # @api public
15
+ class Dispatcher
16
+ # @since 1.0.0
17
+ # @api private
18
+ attr_reader :id
19
+
20
+ # (EXPERIMENTAL) Shared payload context
21
+ #
22
+ # @example
23
+ # logger.context[:component] = "test"
24
+ #
25
+ # logger.info "Hello World"
26
+ # # Hello World component=test
27
+ #
28
+ # @since 1.0.0
29
+ # @api public
30
+ attr_reader :context
31
+
32
+ # @since 1.0.0
33
+ # @api private
34
+ attr_reader :backends
35
+
36
+ # @since 1.0.0
37
+ # @api private
38
+ attr_reader :options
39
+
40
+ # @since 1.0.0
41
+ # @api private
42
+ attr_reader :mutex
43
+
44
+ # Set up a dispatcher
45
+ #
46
+ # @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
50
+ #
51
+ # @return [Dispatcher]
52
+ # @api public
53
+ def self.setup(id, **options)
54
+ dispatcher = new(id, **DEFAULT_OPTS, **options)
55
+ dispatcher.add_backend if dispatcher.backends.empty?
56
+ dispatcher
57
+ end
58
+
59
+ # @since 1.0.0
60
+ # @api private
61
+ def self.default_context
62
+ Thread.current[:__dry_logger__] ||= {}
63
+ end
64
+
65
+ # @since 1.0.0
66
+ # @api private
67
+ def initialize(id, backends: [], context: self.class.default_context, **options)
68
+ @id = id
69
+ @backends = backends
70
+ @options = {**options, progname: id}
71
+ @mutex = Mutex.new
72
+ @context = context
73
+ end
74
+
75
+ # Log an entry with UNKNOWN severity
76
+ #
77
+ # @see Dispatcher#log
78
+ # @api public
79
+ # @return [true]
80
+ def unknown(message = nil, **payload)
81
+ log(:unknown, message, **payload)
82
+ end
83
+
84
+ # Log an entry with DEBUG severity
85
+ #
86
+ # @see Dispatcher#log
87
+ # @api public
88
+ # @return [true]
89
+ def debug(message = nil, **payload)
90
+ log(:debug, message, **payload)
91
+ end
92
+
93
+ # Log an entry with INFO severity
94
+ #
95
+ # @see Dispatcher#log
96
+ # @api public
97
+ # @return [true]
98
+ def info(message = nil, **payload)
99
+ log(:info, message, **payload)
100
+ end
101
+
102
+ # Log an entry with WARN severity
103
+ #
104
+ # @see Dispatcher#log
105
+ # @api public
106
+ # @return [true]
107
+ def warn(message = nil, **payload)
108
+ log(:warn, message, **payload)
109
+ end
110
+
111
+ # Log an entry with ERROR severity
112
+ #
113
+ # @see Dispatcher#log
114
+ # @api public
115
+ # @return [true]
116
+ def error(message = nil, **payload)
117
+ log(:error, message, **payload)
118
+ end
119
+
120
+ # Log an entry with FATAL severity
121
+ #
122
+ # @see Dispatcher#log
123
+ # @api public
124
+ # @return [true]
125
+ def fatal(message = nil, **payload)
126
+ log(:fatal, message, **payload)
127
+ end
128
+
129
+ BACKEND_METHODS.each do |name|
130
+ define_method(name) do
131
+ call(name)
132
+ end
133
+ end
134
+
135
+ # Return severity level
136
+ #
137
+ # @since 1.0.0
138
+ # @return [Integer]
139
+ # @api public
140
+ def level
141
+ LEVELS[options[:level]]
142
+ end
143
+
144
+ # Pass logging to all configured backends
145
+ #
146
+ # @param [Symbol] severity The log severity name
147
+ # @param [String,Symbol,Array] message Optional message object
148
+ # @param [Hash] payload Optional log entry payload
149
+ #
150
+ # @since 1.0.0
151
+ # @return [true]
152
+ # @api public
153
+ def log(severity, message = nil, **payload)
154
+ case message
155
+ when Hash then log(severity, nil, **message)
156
+ else
157
+ entry = Entry.new(
158
+ progname: id,
159
+ severity: severity,
160
+ message: message,
161
+ payload: {**context, **payload}
162
+ )
163
+
164
+ each_backend { |backend| backend.__send__(severity, entry) if backend.log?(entry) }
165
+ end
166
+
167
+ true
168
+ end
169
+
170
+ # (EXPERIMENTAL) Tagged logging withing the provided block
171
+ #
172
+ # @example
173
+ # logger.tagged("red") do
174
+ # logger.info "Hello World"
175
+ # # Hello World tag=red
176
+ # end
177
+ #
178
+ # logger.info "Hello Again"
179
+ # # Hello Again
180
+ #
181
+ # @since 1.0.0
182
+ # @api public
183
+ def tagged(tag)
184
+ context[:tag] = tag
185
+ yield
186
+ ensure
187
+ context.delete(:tag)
188
+ end
189
+
190
+ # Add a new backend to an existing dispatcher
191
+ #
192
+ # @example
193
+ # logger.add_backend(template: "ERROR: %<message>s") { |b|
194
+ # b.log_if = -> entry { entry.error? }
195
+ # }
196
+ #
197
+ # @since 1.0.0
198
+ # @return [Dispatcher]
199
+ # @api public
200
+ def add_backend(instance = nil, **backend_options)
201
+ backend = instance || Dry::Logger.new(**options, **backend_options)
202
+ yield(backend) if block_given?
203
+ backends << backend
204
+ self
205
+ end
206
+
207
+ # @since 1.0.0
208
+ # @api private
209
+ def each_backend(*_args, &block)
210
+ mutex.synchronize do
211
+ backends.each(&block)
212
+ end
213
+ end
214
+
215
+ # Pass logging to all configured backends
216
+ #
217
+ # @since 1.0.0
218
+ # @return [true]
219
+ # @api private
220
+ def call(meth, ...)
221
+ each_backend { |backend| backend.public_send(meth, ...) }
222
+ true
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "dry/logger/constants"
5
+
6
+ module Dry
7
+ module Logger
8
+ # @since 1.0.0
9
+ # @api public
10
+ class Entry
11
+ include Enumerable
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
+ # @since 1.0.0
22
+ # @api public
23
+ attr_reader :progname
24
+
25
+ # @since 1.0.0
26
+ # @api public
27
+ attr_reader :severity
28
+
29
+ # @since 1.0.0
30
+ # @api public
31
+ attr_reader :level
32
+
33
+ # @since 1.0.0
34
+ # @api public
35
+ attr_reader :time
36
+
37
+ # @since 1.0.0
38
+ # @api public
39
+ attr_reader :message
40
+ alias_method :exception, :message
41
+
42
+ # @since 1.0.0
43
+ # @api public
44
+ attr_reader :payload
45
+
46
+ # @since 1.0.0
47
+ # @api private
48
+ def initialize(progname:, severity:, time: Time.now, message: nil, payload: EMPTY_PAYLOAD)
49
+ @progname = progname
50
+ @severity = severity.to_s.upcase # TODO: this doesn't feel right
51
+ @level = LEVELS.fetch(severity.to_s)
52
+ @time = time
53
+ @message = message
54
+ @payload = build_payload(payload)
55
+ end
56
+
57
+ # @since 1.0.0
58
+ # @api public
59
+ def each(&block)
60
+ payload.each(&block)
61
+ end
62
+
63
+ # @since 1.0.0
64
+ # @api public
65
+ def [](name)
66
+ payload[name]
67
+ end
68
+
69
+ # @since 1.0.0
70
+ # @api public
71
+ def debug?
72
+ level.equal?(DEBUG)
73
+ end
74
+
75
+ # @since 1.0.0
76
+ # @api public
77
+ def info?
78
+ level.equal?(INFO)
79
+ end
80
+
81
+ # @since 1.0.0
82
+ # @api public
83
+ def warn?
84
+ level.equal?(WARN)
85
+ end
86
+
87
+ # @since 1.0.0
88
+ # @api public
89
+ def error?
90
+ level.equal?(ERROR)
91
+ end
92
+
93
+ # @since 1.0.0
94
+ # @api public
95
+ def fatal?
96
+ level.equal?(FATAL)
97
+ end
98
+
99
+ # @since 1.0.0
100
+ # @api public
101
+ def exception?
102
+ message.is_a?(Exception)
103
+ end
104
+
105
+ # @since 1.0.0
106
+ # @api public
107
+ def key?(name)
108
+ payload.key?(name)
109
+ end
110
+
111
+ # @since 1.0.0
112
+ # @api private
113
+ def to_h
114
+ @to_h ||= meta.merge(payload)
115
+ end
116
+
117
+ # @since 1.0.0
118
+ # @api private
119
+ def meta
120
+ @meta ||= {progname: progname, severity: severity, time: time}
121
+ end
122
+
123
+ # @since 1.0.0
124
+ # @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
134
+ end
135
+
136
+ # @since 1.0.0
137
+ # @api private
138
+ def filter(filter)
139
+ @payload = filter.call(payload)
140
+ self
141
+ end
142
+
143
+ private
144
+
145
+ # @since 1.0.0
146
+ # @api private
147
+ def build_payload(payload)
148
+ if exception?
149
+ {message: exception.message,
150
+ backtrace: exception.backtrace || EMPTY_BACKTRACE,
151
+ error: exception.class,
152
+ **payload}
153
+ else
154
+ payload
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Logger
5
+ # Filtering logic
6
+ # Originaly copied from hanami/utils (see Hanami::Logger)
7
+ #
8
+ # @since 0.1.0
9
+ # @api private
10
+ class Filter
11
+ # @since 0.1.0
12
+ # @api private
13
+ def initialize(filters = [])
14
+ @filters = filters
15
+ end
16
+
17
+ # @since 0.1.0
18
+ # @api private
19
+ def call(hash)
20
+ _filtered_keys(hash).each do |key|
21
+ *keys, last = _actual_keys(hash, key.split("."))
22
+ keys.inject(hash, :fetch)[last] = "[FILTERED]"
23
+ end
24
+
25
+ hash
26
+ end
27
+
28
+ private
29
+
30
+ # @since 0.1.0
31
+ # @api private
32
+ attr_reader :filters
33
+
34
+ # @since 0.1.0
35
+ # @api private
36
+ def _filtered_keys(hash)
37
+ _key_paths(hash).select { |key|
38
+ filters.any? { |filter|
39
+ key =~ /(\.|\A)#{filter}(\.|\z)/
40
+ }
41
+ }
42
+ end
43
+
44
+ # @since 0.1.0
45
+ # @api private
46
+ def _key_paths(hash, base = nil)
47
+ hash.inject([]) do |results, (k, v)|
48
+ results + (_key_paths?(v) ? _key_paths(v, _build_path(base, k)) : [_build_path(base, k)])
49
+ end
50
+ end
51
+
52
+ # @since 0.1.0
53
+ # @api private
54
+ def _build_path(base, key)
55
+ [base, key.to_s].compact.join(".")
56
+ end
57
+
58
+ # @since 0.1.0
59
+ # @api private
60
+ def _actual_keys(hash, keys)
61
+ search_in = hash
62
+
63
+ keys.inject([]) do |res, key|
64
+ correct_key = search_in.key?(key.to_sym) ? key.to_sym : key
65
+ search_in = search_in[correct_key]
66
+ res + [correct_key]
67
+ end
68
+ end
69
+
70
+ # Check if the given value can be iterated (`Enumerable`) and that isn't a `File`.
71
+ # This is useful to detect closed `Tempfiles`.
72
+ #
73
+ # @since 0.1.0
74
+ # @api private
75
+ #
76
+ # @see https://github.com/hanami/utils/pull/342
77
+ def _key_paths?(value)
78
+ value.is_a?(Enumerable) && !value.is_a?(File)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "dry/logger/formatters/structured"
5
+
6
+ module Dry
7
+ module Logger
8
+ module Formatters
9
+ # JSON formatter.
10
+ #
11
+ # This formatter returns log entries in JSON format.
12
+ #
13
+ # @since 0.1.0
14
+ # @api public
15
+ class JSON < Structured
16
+ # @since 0.1.0
17
+ # @api private
18
+ def format(entry)
19
+ "#{::JSON.generate(entry.as_json)}#{NEW_LINE}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/logger/formatters/structured"
4
+
5
+ module Dry
6
+ module Logger
7
+ module Formatters
8
+ # Special handling of `:params` in the log entry payload
9
+ #
10
+ # @since 1.0.0
11
+ # @api private
12
+ #
13
+ # @see String
14
+ class Rack < String
15
+ # @since 1.0.0
16
+ # @api private
17
+ def format_entry(entry)
18
+ [*entry.payload.except(:params).values, entry[:params]].compact.join(SEPARATOR)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/logger/formatters/structured"
4
+
5
+ module Dry
6
+ module Logger
7
+ module Formatters
8
+ # Basic string formatter.
9
+ #
10
+ # This formatter returns log entries in key=value format.
11
+ #
12
+ # @since 1.0.0
13
+ # @api public
14
+ class String < Structured
15
+ # @since 1.0.0
16
+ # @api private
17
+ SEPARATOR = " "
18
+
19
+ # @since 1.0.0
20
+ # @api private
21
+ HASH_SEPARATOR = ","
22
+
23
+ # @since 1.0.0
24
+ # @api private
25
+ EXCEPTION_SEPARATOR = ": "
26
+
27
+ # @since 1.0.0
28
+ # @api private
29
+ DEFAULT_TEMPLATE = "%<message>s"
30
+
31
+ # @since 1.0.0
32
+ # @api private
33
+ attr_reader :template
34
+
35
+ # @since 1.0.0
36
+ # @api private
37
+ def initialize(template: DEFAULT_TEMPLATE, **options)
38
+ super(**options)
39
+ @template = template
40
+ end
41
+
42
+ private
43
+
44
+ # @since 1.0.0
45
+ # @api private
46
+ def format(entry)
47
+ "#{template % entry.meta.merge(message: format_entry(entry))}#{NEW_LINE}"
48
+ end
49
+
50
+ # @since 1.0.0
51
+ # @api private
52
+ def format_entry(entry)
53
+ 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
61
+ else
62
+ format_payload(entry)
63
+ end
64
+ end
65
+
66
+ # @since 1.0.0
67
+ # @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)}"
72
+ end
73
+
74
+ # @since 1.0.0
75
+ # @api private
76
+ def format_payload(entry)
77
+ entry.map { |key, value| "#{key}=#{value.inspect}" }.join(HASH_SEPARATOR)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "dry/logger/filter"
5
+
6
+ module Dry
7
+ module Logger
8
+ module Formatters
9
+ # Default structured formatter which receives {Logger::Entry} from the backends.
10
+ #
11
+ # @see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger/Formatter.html
12
+ #
13
+ # @since 1.0.0
14
+ # @api public
15
+ class Structured < ::Logger::Formatter
16
+ # @since 1.0.0
17
+ # @api private
18
+ DEFAULT_FILTERS = [].freeze
19
+
20
+ # @since 1.0.0
21
+ # @api private
22
+ NOOP_FILTER = -> message { message }
23
+
24
+ # @since 1.0.0
25
+ # @api private
26
+ NEW_LINE = $/ # rubocop:disable Style/SpecialGlobalVars
27
+
28
+ # @since 1.0.0
29
+ # @api private
30
+ attr_reader :filter
31
+
32
+ # @since 1.0.0
33
+ # @api private
34
+ attr_reader :options
35
+
36
+ # @since 1.0.0
37
+ # @api private
38
+ def initialize(filters: DEFAULT_FILTERS, **options)
39
+ super()
40
+ @filter = filters.equal?(DEFAULT_FILTERS) ? NOOP_FILTER : Filter.new(filters)
41
+ @options = options
42
+ end
43
+
44
+ # Filter and then format the log entry into a string
45
+ #
46
+ # Custom formatters typically won't have to override this method because
47
+ # the actual formatting logic is implemented as Structured#format
48
+ #
49
+ # @see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger/Formatter.html#method-i-call
50
+ #
51
+ # @since 1.0.0
52
+ # @return [String]
53
+ # @api public
54
+ def call(_severity, _time, _progname, entry)
55
+ format(entry.filter(filter))
56
+ end
57
+
58
+ # Format entry into a loggable object
59
+ #
60
+ # Custom formatters should override this method
61
+ #
62
+ # @api since 1.0.0
63
+ # @return [Entry]
64
+ # @api public
65
+ def format(entry)
66
+ entry
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Logger
5
+ VERSION = "1.0.0.rc1"
6
+ end
7
+ end
data/lib/dry/logger.rb ADDED
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/logger/constants"
4
+ require "dry/logger/dispatcher"
5
+
6
+ require "dry/logger/formatters/string"
7
+ require "dry/logger/formatters/rack"
8
+ require "dry/logger/formatters/json"
9
+
10
+ require "dry/logger/backends/io"
11
+ require "dry/logger/backends/file"
12
+
13
+ module Dry
14
+ # Set up a logger dispatcher
15
+ #
16
+ # @example Basic $stdout string logger
17
+ # logger = Dry.Logger(:my_app)
18
+ #
19
+ # logger.info("Hello World!")
20
+ # # Hello World!
21
+ #
22
+ # @example Customized $stdout string logger
23
+ # logger = Dry.Logger(:my_app, template: "[%<severity>][%<time>s] %<message>s")
24
+ #
25
+ # logger.info("Hello World!")
26
+ # # [INFO][2022-11-06 10:55:12 +0100] Hello World!
27
+ #
28
+ # logger.info(Hello: "World!")
29
+ # # [INFO][2022-11-06 10:55:14 +0100] Hello="World!"
30
+ #
31
+ # logger.warn("Ooops!")
32
+ # # [WARN][2022-11-06 10:55:57 +0100] Ooops!
33
+ #
34
+ # logger.error("Gaaah!")
35
+ # # [ERROR][2022-11-06 10:55:57 +0100] Gaaah!
36
+ #
37
+ # @example Basic $stdout JSON logger
38
+ # logger = Dry.Logger(:my_app, formatter: :json)
39
+ #
40
+ # logger.info(Hello: "World!")
41
+ # # {"progname":"my_app","severity":"INFO","time":"2022-11-06T10:11:29Z","Hello":"World!"}
42
+ #
43
+ # @since 1.0.0
44
+ # @return [Dispatcher]
45
+ # @api public
46
+ def self.Logger(id, **opts, &block)
47
+ Logger::Dispatcher.setup(id, **opts, &block)
48
+ end
49
+
50
+ 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
83
+
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
95
+
96
+ backend_opts = opts.select { |key, _| BACKEND_OPT_KEYS.include?(key) }
97
+
98
+ backend.new(stream: stream, **backend_opts, formatter: formatter)
99
+ end
100
+
101
+ # Internal formatters registry
102
+ #
103
+ # @since 1.0.0
104
+ # @api private
105
+ def self.formatters
106
+ @formatters ||= {}
107
+ end
108
+
109
+ register_formatter(:string, Formatters::String)
110
+ register_formatter(:rack, Formatters::Rack)
111
+ register_formatter(:json, Formatters::JSON)
112
+ end
113
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dry-logger
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.rc1
5
+ platform: ruby
6
+ authors:
7
+ - Luca Guidi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-11-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Logging for Ruby
28
+ email:
29
+ - me@lucaguidi.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE
36
+ - README.md
37
+ - dry-logger.gemspec
38
+ - lib/dry/logger.rb
39
+ - lib/dry/logger/backends/file.rb
40
+ - lib/dry/logger/backends/io.rb
41
+ - lib/dry/logger/backends/stream.rb
42
+ - lib/dry/logger/constants.rb
43
+ - lib/dry/logger/dispatcher.rb
44
+ - lib/dry/logger/entry.rb
45
+ - lib/dry/logger/filter.rb
46
+ - lib/dry/logger/formatters/json.rb
47
+ - lib/dry/logger/formatters/rack.rb
48
+ - lib/dry/logger/formatters/string.rb
49
+ - lib/dry/logger/formatters/structured.rb
50
+ - lib/dry/logger/version.rb
51
+ homepage: https://dry-rb.org/gems/dry-logger
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ allowed_push_host: https://rubygems.org
56
+ changelog_uri: https://github.com/dry-rb/dry-logger/blob/main/CHANGELOG.md
57
+ source_code_uri: https://github.com/dry-rb/dry-logger
58
+ bug_tracker_uri: https://github.com/dry-rb/dry-logger/issues
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">"
71
+ - !ruby/object:Gem::Version
72
+ version: 1.3.1
73
+ requirements: []
74
+ rubygems_version: 3.1.6
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: Logging for Ruby
78
+ test_files: []