dry-logger 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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: []