twiglet 2.1.0 → 2.2.3
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 +4 -4
- data/.github/workflows/ruby.yml +3 -2
- data/.gitignore +1 -0
- data/README.md +3 -1
- data/lib/twiglet/formatter.rb +69 -0
- data/lib/twiglet/logger.rb +25 -74
- data/lib/twiglet/version.rb +1 -1
- data/test/formatter_test.rb +31 -0
- data/test/logger_test.rb +69 -4
- metadata +5 -4
- data/Gemfile.lock +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd959654140f88727254fcf4d660dd017e8a00e12f3a68eabf6359246f7aeb97
|
4
|
+
data.tar.gz: ab14bddf0d5b9138634000a0fad1e7d4fd5d13d56e2e604aef87a74e2c0c7d78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1effc27ff4f08c25d1b92789e7b7937da24d652c922bf0bc404a4ca106d5c670897b0e24bbee17ee0fd1d1e584457bba06034e5fb0432edf6d517cd6dc3f801b
|
7
|
+
data.tar.gz: 5e6a270d224fb73e077670036b2540a23787ac4d9cde08c8aa0912c2c94e2ec392e49b8d39e134d13c4a76b3e6294729b5241ac651504e9099e69509b8ddf044
|
data/.github/workflows/ruby.yml
CHANGED
@@ -3,8 +3,6 @@ name: Ruby CI
|
|
3
3
|
on:
|
4
4
|
push:
|
5
5
|
branches:
|
6
|
-
- '*' # matches every branch
|
7
|
-
- '*/*' # matches every branch containing a single '/'
|
8
6
|
|
9
7
|
jobs:
|
10
8
|
build:
|
@@ -30,3 +28,6 @@ jobs:
|
|
30
28
|
- name: Run all tests
|
31
29
|
run: bundle exec rake test
|
32
30
|
shell: bash
|
31
|
+
- name: Run example_app
|
32
|
+
run: bundle exec ruby example_app.rb
|
33
|
+
shell: bash
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -22,7 +22,9 @@ A hash can optionally be passed in as a keyword argument for `default_properties
|
|
22
22
|
|
23
23
|
You may also provide an optional `output` keyword argument which should be an object with a `puts` method - like `$stdout`.
|
24
24
|
|
25
|
-
|
25
|
+
In addition, you can provide another optional keyword argument called `now`, which should be a function returning a `Time` string in ISO8601 format.
|
26
|
+
|
27
|
+
Lastly, you may provide the optional keyword argument `level` to initialize the logger with a severity threshold. Alternatively, the threshold can be updated at runtime by calling the `level` instance method.
|
26
28
|
|
27
29
|
The defaults for both `output` and `now` should serve for most uses, though you may want to override them for testing as we have done [here](test/logger_test.rb).
|
28
30
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require_relative '../hash_extensions'
|
3
|
+
|
4
|
+
module Twiglet
|
5
|
+
class Formatter < ::Logger::Formatter
|
6
|
+
Hash.include HashExtensions
|
7
|
+
|
8
|
+
def initialize(service_name,
|
9
|
+
default_properties: {},
|
10
|
+
now: -> { Time.now.utc })
|
11
|
+
@service_name = service_name
|
12
|
+
@now = now
|
13
|
+
@default_properties = default_properties
|
14
|
+
|
15
|
+
super()
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(severity, _time, _progname, msg)
|
19
|
+
level = severity.downcase
|
20
|
+
log(level: level, message: msg)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def log(level:, message:)
|
26
|
+
case message
|
27
|
+
when String
|
28
|
+
log_text(level, message: message)
|
29
|
+
when Hash
|
30
|
+
log_object(level, message: message)
|
31
|
+
else
|
32
|
+
raise('Message must be String or Hash')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def log_text(level, message:)
|
37
|
+
raise('The \'message\' property of log object must not be empty') if message.strip.empty?
|
38
|
+
|
39
|
+
message = { message: message }
|
40
|
+
log_message(level, message: message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def log_object(level, message:)
|
44
|
+
message = message.transform_keys(&:to_sym)
|
45
|
+
message.key?(:message) || raise('Log object must have a \'message\' property')
|
46
|
+
message[:message].strip.empty? && raise('The \'message\' property of log object must not be empty')
|
47
|
+
|
48
|
+
log_message(level, message: message)
|
49
|
+
end
|
50
|
+
|
51
|
+
def log_message(level, message:)
|
52
|
+
base_message = {
|
53
|
+
"@timestamp": @now.call.iso8601(3),
|
54
|
+
service: {
|
55
|
+
name: @service_name
|
56
|
+
},
|
57
|
+
log: {
|
58
|
+
level: level
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
base_message
|
63
|
+
.deep_merge(@default_properties.to_nested)
|
64
|
+
.deep_merge(message.to_nested)
|
65
|
+
.to_json
|
66
|
+
.concat("\n")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/twiglet/logger.rb
CHANGED
@@ -1,112 +1,63 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'logger'
|
3
4
|
require 'time'
|
4
5
|
require 'json'
|
6
|
+
require_relative 'formatter'
|
5
7
|
require_relative '../hash_extensions'
|
6
8
|
|
7
9
|
module Twiglet
|
8
|
-
class Logger
|
10
|
+
class Logger < ::Logger
|
9
11
|
Hash.include HashExtensions
|
10
12
|
|
11
13
|
def initialize(
|
12
14
|
service_name,
|
13
15
|
default_properties: {},
|
14
16
|
now: -> { Time.now.utc },
|
15
|
-
output: $stdout
|
17
|
+
output: $stdout,
|
18
|
+
level: Logger::DEBUG
|
16
19
|
)
|
17
20
|
@service_name = service_name
|
18
21
|
@now = now
|
19
22
|
@output = output
|
23
|
+
@level = level
|
20
24
|
|
21
25
|
raise 'Service name is mandatory' \
|
22
|
-
unless
|
26
|
+
unless service_name.is_a?(String) && !service_name.strip.empty?
|
23
27
|
|
24
|
-
|
28
|
+
formatter = Twiglet::Formatter.new(service_name, default_properties: default_properties, now: now)
|
29
|
+
super(output, formatter: formatter, level: level)
|
25
30
|
end
|
26
31
|
|
27
|
-
def
|
28
|
-
log(level: 'debug', message: message)
|
29
|
-
end
|
30
|
-
|
31
|
-
def info(message)
|
32
|
-
log(level: 'info', message: message)
|
33
|
-
end
|
34
|
-
|
35
|
-
def warning(message)
|
36
|
-
log(level: 'warning', message: message)
|
37
|
-
end
|
38
|
-
|
39
|
-
alias_method :warn, :warning
|
40
|
-
|
41
|
-
def error(message, error = nil)
|
32
|
+
def error(message = {}, error = nil, &block)
|
42
33
|
if error
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
34
|
+
error_fields = {
|
35
|
+
'error': {
|
36
|
+
'message': error.message
|
37
|
+
}
|
38
|
+
}
|
39
|
+
add_stack_trace(error_fields, error)
|
40
|
+
message.is_a?(Hash) ? message.merge!(error_fields) : error_fields.merge!(message: message)
|
49
41
|
end
|
50
42
|
|
51
|
-
|
52
|
-
end
|
53
|
-
|
54
|
-
def critical(message)
|
55
|
-
log(level: 'critical', message: message)
|
43
|
+
super(message, &block)
|
56
44
|
end
|
57
45
|
|
58
|
-
alias_method :fatal, :critical
|
59
|
-
|
60
46
|
def with(default_properties)
|
61
47
|
Logger.new(@service_name,
|
62
48
|
default_properties: default_properties,
|
63
49
|
now: @now,
|
64
|
-
output: @output
|
50
|
+
output: @output,
|
51
|
+
level: @level)
|
65
52
|
end
|
66
53
|
|
67
|
-
|
54
|
+
alias_method :warning, :warn
|
55
|
+
alias_method :critical, :fatal
|
68
56
|
|
69
|
-
|
70
|
-
case message
|
71
|
-
when String
|
72
|
-
log_text(level, message: message)
|
73
|
-
when Hash
|
74
|
-
log_object(level, message: message)
|
75
|
-
else
|
76
|
-
raise('Message must be String or Hash')
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def log_text(level, message:)
|
81
|
-
raise('The \'message\' property of log object must not be empty') if message.strip.empty?
|
82
|
-
|
83
|
-
message = { message: message }
|
84
|
-
log_message(level, message: message)
|
85
|
-
end
|
86
|
-
|
87
|
-
def log_object(level, message:)
|
88
|
-
message = message.transform_keys(&:to_sym)
|
89
|
-
message.key?(:message) || raise('Log object must have a \'message\' property')
|
90
|
-
message[:message].strip.empty? && raise('The \'message\' property of log object must not be empty')
|
91
|
-
|
92
|
-
log_message(level, message: message)
|
93
|
-
end
|
94
|
-
|
95
|
-
def log_message(level, message:)
|
96
|
-
base_message = {
|
97
|
-
"@timestamp": @now.call.iso8601(3),
|
98
|
-
service: {
|
99
|
-
name: @service_name
|
100
|
-
},
|
101
|
-
log: {
|
102
|
-
level: level
|
103
|
-
}
|
104
|
-
}
|
57
|
+
private
|
105
58
|
|
106
|
-
|
107
|
-
|
108
|
-
.deep_merge(message.to_nested)
|
109
|
-
.to_json
|
59
|
+
def add_stack_trace(hash_to_add_to, error)
|
60
|
+
hash_to_add_to[:error][:stack_trace] = error.backtrace.join("\n") if error.backtrace
|
110
61
|
end
|
111
62
|
end
|
112
63
|
end
|
data/lib/twiglet/version.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'json'
|
5
|
+
require_relative '../lib/twiglet/formatter'
|
6
|
+
|
7
|
+
describe Twiglet::Formatter do
|
8
|
+
before do
|
9
|
+
@now = -> { Time.utc(2020, 5, 11, 15, 1, 1) }
|
10
|
+
@formatter = Twiglet::Formatter.new('petshop', now: @now)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'initializes an instance of a Ruby Logger Formatter' do
|
14
|
+
assert @formatter.is_a?(::Logger::Formatter)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'returns a formatted log from a string message' do
|
18
|
+
msg = @formatter.call('warn', nil, nil, 'shop is running low on dog food')
|
19
|
+
expected_log = {
|
20
|
+
"@timestamp" => '2020-05-11T15:01:01.000Z',
|
21
|
+
"service" => {
|
22
|
+
"name" => 'petshop'
|
23
|
+
},
|
24
|
+
"log" => {
|
25
|
+
"level" => 'warn'
|
26
|
+
},
|
27
|
+
"message" => 'shop is running low on dog food'
|
28
|
+
}
|
29
|
+
assert_equal JSON.parse(msg), expected_log
|
30
|
+
end
|
31
|
+
end
|
data/test/logger_test.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'minitest/autorun'
|
4
4
|
require_relative '../lib/twiglet/logger'
|
5
5
|
|
6
|
+
# rubocop:disable Metrics/BlockLength
|
6
7
|
describe Twiglet::Logger do
|
7
8
|
before do
|
8
9
|
@now = -> { Time.utc(2020, 5, 11, 15, 1, 1) }
|
@@ -15,10 +16,10 @@ describe Twiglet::Logger do
|
|
15
16
|
LEVELS = [
|
16
17
|
{ method: :debug, level: 'debug' },
|
17
18
|
{ method: :info, level: 'info' },
|
18
|
-
{ method: :warning, level: '
|
19
|
-
{ method: :warn, level: '
|
20
|
-
{ method: :critical, level: '
|
21
|
-
{ method: :fatal, level: '
|
19
|
+
{ method: :warning, level: 'warn' },
|
20
|
+
{ method: :warn, level: 'warn' },
|
21
|
+
{ method: :critical, level: 'fatal' },
|
22
|
+
{ method: :fatal, level: 'fatal' },
|
22
23
|
{ method: :error, level: 'error' }
|
23
24
|
].freeze
|
24
25
|
|
@@ -28,6 +29,13 @@ describe Twiglet::Logger do
|
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
32
|
+
it 'conforms to the standard Ruby Logger API' do
|
33
|
+
[:debug, :debug?, :info, :info?, :warn, :warn?, :fatal, :fatal?, :error, :error?,
|
34
|
+
:level, :level=, :sev_threshold=].each do |call|
|
35
|
+
assert @logger.respond_to?(call), "Logger does not respond to #{call}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
31
39
|
describe 'JSON logging' do
|
32
40
|
it 'should throw an error with an empty message' do
|
33
41
|
assert_raises RuntimeError do
|
@@ -123,6 +131,21 @@ describe Twiglet::Logger do
|
|
123
131
|
assert_equal 'Guinea pigs arrived', log[:message]
|
124
132
|
end
|
125
133
|
|
134
|
+
it "should log multiple messages properly" do
|
135
|
+
@logger.debug({message: 'hi'})
|
136
|
+
@logger.info({message: 'there'})
|
137
|
+
|
138
|
+
expected_output =
|
139
|
+
'{"@timestamp":"2020-05-11T15:01:01.000Z",'\
|
140
|
+
'"service":{"name":"petshop"},"log":{"level":"debug"},"message":"hi"}'\
|
141
|
+
"\n"\
|
142
|
+
'{"@timestamp":"2020-05-11T15:01:01.000Z",'\
|
143
|
+
'"service":{"name":"petshop"},"log":{"level":"info"},"message":"there"}'\
|
144
|
+
"\n"\
|
145
|
+
|
146
|
+
assert_equal expected_output, @buffer.string
|
147
|
+
end
|
148
|
+
|
126
149
|
it 'should be able to convert dotted keys to nested objects' do
|
127
150
|
@logger.debug({
|
128
151
|
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
@@ -193,6 +216,17 @@ describe Twiglet::Logger do
|
|
193
216
|
assert_match 'logger_test.rb', actual_log[:error][:stack_trace].lines.first
|
194
217
|
end
|
195
218
|
|
219
|
+
it 'should log an error without backtrace' do
|
220
|
+
e = StandardError.new('Connection timed-out')
|
221
|
+
@logger.error({message: 'Artificially raised exception'}, e)
|
222
|
+
|
223
|
+
actual_log = read_json(@buffer)
|
224
|
+
|
225
|
+
assert_equal 'Artificially raised exception', actual_log[:message]
|
226
|
+
assert_equal 'Connection timed-out', actual_log[:error][:message]
|
227
|
+
refute actual_log[:error].key?(:stack_trace)
|
228
|
+
end
|
229
|
+
|
196
230
|
LEVELS.each do |attrs|
|
197
231
|
it "should correctly log level when calling #{attrs[:method]}" do
|
198
232
|
@logger.public_send(attrs[:method], {message: 'a log message'})
|
@@ -247,6 +281,36 @@ describe Twiglet::Logger do
|
|
247
281
|
end
|
248
282
|
end
|
249
283
|
|
284
|
+
describe 'logging with a block' do
|
285
|
+
LEVELS.each do |attrs|
|
286
|
+
it "should correctly log the block when calling #{attrs[:method]}" do
|
287
|
+
block = proc { 'a block log message' }
|
288
|
+
@logger.public_send(attrs[:method], &block)
|
289
|
+
actual_log = read_json(@buffer)
|
290
|
+
|
291
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
292
|
+
assert_equal 'a block log message', actual_log[:message]
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
describe 'logger level' do
|
298
|
+
[
|
299
|
+
{ expression: :info, level: 1 },
|
300
|
+
{ expression: 'warn', level: 2 },
|
301
|
+
{ expression: Logger::DEBUG, level: 0 }
|
302
|
+
].each do |args|
|
303
|
+
it "sets the severity threshold to level #{args[:level]}" do
|
304
|
+
@logger.level = args[:expression]
|
305
|
+
assert_equal args[:level], @logger.level
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
it 'initializes the logger with the provided level' do
|
310
|
+
assert_equal Logger::WARN, Twiglet::Logger.new('petshop', level: :warn).level
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
250
314
|
private
|
251
315
|
|
252
316
|
def read_json(buffer)
|
@@ -254,3 +318,4 @@ describe Twiglet::Logger do
|
|
254
318
|
JSON.parse(buffer.read, symbolize_names: true)
|
255
319
|
end
|
256
320
|
end
|
321
|
+
# rubocop:enable Metrics/BlockLength
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: twiglet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Simply Business
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Like a log, only smaller.
|
14
14
|
email:
|
@@ -24,15 +24,16 @@ files:
|
|
24
24
|
- ".ruby-version"
|
25
25
|
- CODE_OF_CONDUCT.md
|
26
26
|
- Gemfile
|
27
|
-
- Gemfile.lock
|
28
27
|
- LICENSE
|
29
28
|
- RATIONALE.md
|
30
29
|
- README.md
|
31
30
|
- Rakefile
|
32
31
|
- example_app.rb
|
33
32
|
- lib/hash_extensions.rb
|
33
|
+
- lib/twiglet/formatter.rb
|
34
34
|
- lib/twiglet/logger.rb
|
35
35
|
- lib/twiglet/version.rb
|
36
|
+
- test/formatter_test.rb
|
36
37
|
- test/hash_extensions_test.rb
|
37
38
|
- test/logger_test.rb
|
38
39
|
- twiglet.gemspec
|
@@ -55,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
56
|
- !ruby/object:Gem::Version
|
56
57
|
version: '0'
|
57
58
|
requirements: []
|
58
|
-
rubygems_version: 3.0.
|
59
|
+
rubygems_version: 3.0.8
|
59
60
|
signing_key:
|
60
61
|
specification_version: 4
|
61
62
|
summary: Twiglet
|
data/Gemfile.lock
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
GIT
|
2
|
-
remote: https://github.com/simplybusiness/simplycop.git
|
3
|
-
revision: 02b417277e3ff9eeab34d6b7c30a95a17d876731
|
4
|
-
specs:
|
5
|
-
simplycop (0.5.4)
|
6
|
-
rubocop (~> 0.80.0)
|
7
|
-
rubocop-rails
|
8
|
-
rubocop-rspec
|
9
|
-
|
10
|
-
GEM
|
11
|
-
remote: https://rubygems.org/
|
12
|
-
specs:
|
13
|
-
activesupport (6.0.3.1)
|
14
|
-
concurrent-ruby (~> 1.0, >= 1.0.2)
|
15
|
-
i18n (>= 0.7, < 2)
|
16
|
-
minitest (~> 5.1)
|
17
|
-
tzinfo (~> 1.1)
|
18
|
-
zeitwerk (~> 2.2, >= 2.2.2)
|
19
|
-
ast (2.4.0)
|
20
|
-
concurrent-ruby (1.1.6)
|
21
|
-
i18n (1.8.2)
|
22
|
-
concurrent-ruby (~> 1.0)
|
23
|
-
jaro_winkler (1.5.4)
|
24
|
-
minitest (5.14.0)
|
25
|
-
parallel (1.19.1)
|
26
|
-
parser (2.7.1.3)
|
27
|
-
ast (~> 2.4.0)
|
28
|
-
rack (2.2.2)
|
29
|
-
rainbow (3.0.0)
|
30
|
-
rake (13.0.1)
|
31
|
-
rexml (3.2.4)
|
32
|
-
rubocop (0.80.1)
|
33
|
-
jaro_winkler (~> 1.5.1)
|
34
|
-
parallel (~> 1.10)
|
35
|
-
parser (>= 2.7.0.1)
|
36
|
-
rainbow (>= 2.2.2, < 4.0)
|
37
|
-
rexml
|
38
|
-
ruby-progressbar (~> 1.7)
|
39
|
-
unicode-display_width (>= 1.4.0, < 1.7)
|
40
|
-
rubocop-rails (2.5.2)
|
41
|
-
activesupport
|
42
|
-
rack (>= 1.1)
|
43
|
-
rubocop (>= 0.72.0)
|
44
|
-
rubocop-rspec (1.39.0)
|
45
|
-
rubocop (>= 0.68.1)
|
46
|
-
ruby-progressbar (1.10.1)
|
47
|
-
thread_safe (0.3.6)
|
48
|
-
tzinfo (1.2.7)
|
49
|
-
thread_safe (~> 0.1)
|
50
|
-
unicode-display_width (1.6.1)
|
51
|
-
zeitwerk (2.3.0)
|
52
|
-
|
53
|
-
PLATFORMS
|
54
|
-
ruby
|
55
|
-
|
56
|
-
DEPENDENCIES
|
57
|
-
minitest
|
58
|
-
rake
|
59
|
-
simplycop!
|
60
|
-
|
61
|
-
BUNDLED WITH
|
62
|
-
2.1.4
|