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 +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE +20 -0
- data/README.md +30 -0
- data/dry-logger.gemspec +34 -0
- data/lib/dry/logger/backends/file.rb +18 -0
- data/lib/dry/logger/backends/io.rb +15 -0
- data/lib/dry/logger/backends/stream.rb +47 -0
- data/lib/dry/logger/constants.rb +43 -0
- data/lib/dry/logger/dispatcher.rb +226 -0
- data/lib/dry/logger/entry.rb +159 -0
- data/lib/dry/logger/filter.rb +82 -0
- data/lib/dry/logger/formatters/json.rb +24 -0
- data/lib/dry/logger/formatters/rack.rb +23 -0
- data/lib/dry/logger/formatters/string.rb +82 -0
- data/lib/dry/logger/formatters/structured.rb +71 -0
- data/lib/dry/logger/version.rb +7 -0
- data/lib/dry/logger.rb +113 -0
- metadata +78 -0
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 [][chat]
|
9
|
+
|
10
|
+
[][gem]
|
11
|
+
[][actions]
|
12
|
+
[][codacy]
|
13
|
+
[][codacy]
|
14
|
+
[][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.
|
data/dry-logger.gemspec
ADDED
@@ -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,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
|
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: []
|