twiglet 2.0.0 → 2.2.2
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/.rubocop.yml +1 -1
- data/README.md +23 -0
- data/example_app.rb +3 -0
- data/lib/twiglet/formatter.rb +69 -0
- data/lib/twiglet/logger.rb +20 -51
- data/lib/twiglet/version.rb +1 -1
- data/test/formatter_test.rb +31 -0
- data/test/logger_test.rb +263 -155
- metadata +4 -3
- 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: 9bd7e1ea9d6464abb4dcb96e8d2679ce6c82ebc289c55cb5989d8f8f657f065f
|
4
|
+
data.tar.gz: 2b2d8e963cf250c0b746dd9d56c40368e31f67b1deaf6cd63c9dee9a731ce804
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8277fd64f609bb4edbf6c95e0f97dc56784a1ea63415c1b261dd9649a69fd6cfee54abbb0c91dfb2232f78f0a14b9c539592d4ca58d57d566350f2a0dbceb648
|
7
|
+
data.tar.gz: e9daae207235db6582162e7d2682cf69ea59e5dc09796b7d804bed6f31775ff07ceaff4b38518dd7c6d6a1ec40759c20dadcbebd388417187bcccb66c09fe793
|
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/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -40,6 +40,17 @@ This will write to STDOUT a JSON string:
|
|
40
40
|
|
41
41
|
Obviously the timestamp will be different.
|
42
42
|
|
43
|
+
Alternatively, if you just want to log some error message in text format
|
44
|
+
```ruby
|
45
|
+
logger.error( "Emergency! There's an Emergency going on")
|
46
|
+
```
|
47
|
+
|
48
|
+
This will write to STDOUT a JSON string:
|
49
|
+
|
50
|
+
```json
|
51
|
+
{"service":{"name":"service name"},"@timestamp":"2020-05-14T10:54:59.164+01:00","log":{"level":"error"}, "message":"Emergency! There's an Emergency going on"}
|
52
|
+
```
|
53
|
+
|
43
54
|
Errors can be logged as well, and this will log the error message and backtrace in the relevant ECS compliant fields:
|
44
55
|
|
45
56
|
```ruby
|
@@ -68,6 +79,18 @@ This writes:
|
|
68
79
|
{"service":{"name":"service name"},"@timestamp":"2020-05-14T10:56:49.527+01:00","log":{"level":"info"},"event":{"action":"HTTP request"},"message":"GET /pets success","trace":{"id":"1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb"},"http":{"request":{"method":"get"},"response":{"status_code":200}},"url":{"path":"/pets"}}
|
69
80
|
```
|
70
81
|
|
82
|
+
Similar to error you can use text logging here as:
|
83
|
+
|
84
|
+
```
|
85
|
+
logger.info('GET /pets success')
|
86
|
+
```
|
87
|
+
This writes:
|
88
|
+
|
89
|
+
```json
|
90
|
+
{"service":{"name":"service name"},"@timestamp":"2020-05-14T10:56:49.527+01:00","log":{"level":"info"}}
|
91
|
+
```
|
92
|
+
|
93
|
+
|
71
94
|
It may be that when making a series of logs that write information about a single event, you may want to avoid duplication by creating an event specific logger that includes the context:
|
72
95
|
|
73
96
|
```ruby
|
data/example_app.rb
CHANGED
@@ -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,11 +1,13 @@
|
|
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(
|
@@ -19,40 +21,24 @@ module Twiglet
|
|
19
21
|
@output = output
|
20
22
|
|
21
23
|
raise 'Service name is mandatory' \
|
22
|
-
unless
|
24
|
+
unless service_name.is_a?(String) && !service_name.strip.empty?
|
23
25
|
|
24
|
-
|
26
|
+
formatter = Twiglet::Formatter.new(service_name, default_properties: default_properties, now: now)
|
27
|
+
super(output, formatter: formatter)
|
25
28
|
end
|
26
29
|
|
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)
|
30
|
+
def error(message = {}, error = nil, &block)
|
42
31
|
if error
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
32
|
+
error_fields = {
|
33
|
+
'error': {
|
34
|
+
'message': error.message
|
35
|
+
}
|
36
|
+
}
|
37
|
+
add_stack_trace(error_fields, error)
|
38
|
+
message.is_a?(Hash) ? message.merge!(error_fields) : error_fields.merge!(message: message)
|
49
39
|
end
|
50
40
|
|
51
|
-
|
52
|
-
end
|
53
|
-
|
54
|
-
def critical(message)
|
55
|
-
log(level: 'critical', message: message)
|
41
|
+
super(message, &block)
|
56
42
|
end
|
57
43
|
|
58
44
|
def with(default_properties)
|
@@ -62,30 +48,13 @@ module Twiglet
|
|
62
48
|
output: @output)
|
63
49
|
end
|
64
50
|
|
65
|
-
|
66
|
-
|
67
|
-
def log(level:, message:)
|
68
|
-
raise 'Message must be a Hash' unless message.is_a?(Hash)
|
51
|
+
alias_method :warning, :warn
|
52
|
+
alias_method :critical, :fatal
|
69
53
|
|
70
|
-
|
71
|
-
message.key?(:message) || raise('Log object must have a \'message\' property')
|
72
|
-
|
73
|
-
message[:message].strip.empty? && raise('The \'message\' property of log object must not be empty')
|
74
|
-
|
75
|
-
base_message = {
|
76
|
-
service: {
|
77
|
-
name: @service_name
|
78
|
-
},
|
79
|
-
"@timestamp": @now.call.iso8601(3),
|
80
|
-
log: {
|
81
|
-
level: level
|
82
|
-
}
|
83
|
-
}
|
54
|
+
private
|
84
55
|
|
85
|
-
|
86
|
-
|
87
|
-
.deep_merge(message.to_nested)
|
88
|
-
.to_json
|
56
|
+
def add_stack_trace(hash_to_add_to, error)
|
57
|
+
hash_to_add_to[:error][:stack_trace] = error.backtrace.join("\n") if error.backtrace
|
89
58
|
end
|
90
59
|
end
|
91
60
|
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) }
|
@@ -12,191 +13,297 @@ describe Twiglet::Logger do
|
|
12
13
|
output: @buffer)
|
13
14
|
end
|
14
15
|
|
16
|
+
LEVELS = [
|
17
|
+
{ method: :debug, level: 'debug' },
|
18
|
+
{ method: :info, level: 'info' },
|
19
|
+
{ method: :warning, level: 'warn' },
|
20
|
+
{ method: :warn, level: 'warn' },
|
21
|
+
{ method: :critical, level: 'fatal' },
|
22
|
+
{ method: :fatal, level: 'fatal' },
|
23
|
+
{ method: :error, level: 'error' }
|
24
|
+
].freeze
|
25
|
+
|
15
26
|
it 'should throw an error with an empty service name' do
|
16
27
|
assert_raises RuntimeError do
|
17
28
|
Twiglet::Logger.new(' ')
|
18
29
|
end
|
19
30
|
end
|
20
31
|
|
21
|
-
it '
|
22
|
-
|
23
|
-
|
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}"
|
24
36
|
end
|
25
37
|
end
|
26
38
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
describe 'JSON logging' do
|
40
|
+
it 'should throw an error with an empty message' do
|
41
|
+
assert_raises RuntimeError do
|
42
|
+
@logger.info({message: ''})
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should log mandatory attributes' do
|
47
|
+
@logger.error({message: 'Out of pets exception'})
|
48
|
+
actual_log = read_json(@buffer)
|
49
|
+
|
50
|
+
expected_log = {
|
51
|
+
message: 'Out of pets exception',
|
52
|
+
"@timestamp": '2020-05-11T15:01:01.000Z',
|
53
|
+
service: {
|
54
|
+
name: 'petshop'
|
55
|
+
},
|
56
|
+
log: {
|
57
|
+
level: 'error'
|
58
|
+
}
|
39
59
|
}
|
40
|
-
}
|
41
60
|
|
42
|
-
|
43
|
-
|
61
|
+
assert_equal expected_log, actual_log
|
62
|
+
end
|
44
63
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
64
|
+
it 'should log the provided message' do
|
65
|
+
@logger.error({event:
|
66
|
+
{action: 'exception'},
|
67
|
+
message: 'Emergency! Emergency!'})
|
68
|
+
log = read_json(@buffer)
|
50
69
|
|
51
|
-
|
52
|
-
|
53
|
-
|
70
|
+
assert_equal 'exception', log[:event][:action]
|
71
|
+
assert_equal 'Emergency! Emergency!', log[:message]
|
72
|
+
end
|
54
73
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
output = StringIO.new
|
68
|
-
logger = Twiglet::Logger.new('petshop',
|
69
|
-
now: @now,
|
70
|
-
output: output,
|
71
|
-
default_properties: extra_properties)
|
72
|
-
|
73
|
-
logger.error({message: 'GET /cats'})
|
74
|
-
log = read_json output
|
75
|
-
|
76
|
-
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
77
|
-
assert_equal 'petshop', log[:service][:name]
|
78
|
-
assert_equal 'shop', log[:service][:type]
|
79
|
-
assert_equal 'get', log[:request][:method]
|
80
|
-
assert_equal 200, log[:response][:status_code]
|
81
|
-
end
|
74
|
+
it 'should log scoped properties defined at creation' do
|
75
|
+
extra_properties = {
|
76
|
+
trace: {
|
77
|
+
id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
78
|
+
},
|
79
|
+
service: {
|
80
|
+
type: 'shop'
|
81
|
+
},
|
82
|
+
request: {method: 'get'},
|
83
|
+
response: {status_code: 200}
|
84
|
+
}
|
82
85
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
event: {action: 'pet purchase'}
|
89
|
-
})
|
90
|
-
|
91
|
-
# do stuff
|
92
|
-
purchase_logger.info({
|
93
|
-
message: 'customer bought a dog',
|
94
|
-
pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
|
95
|
-
})
|
96
|
-
|
97
|
-
log = read_json @buffer
|
98
|
-
|
99
|
-
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
100
|
-
assert_equal 'Freda Bloggs', log[:customer][:full_name]
|
101
|
-
assert_equal 'pet purchase', log[:event][:action]
|
102
|
-
assert_equal 'customer bought a dog', log[:message]
|
103
|
-
assert_equal 'Barker', log[:pet][:name]
|
104
|
-
end
|
86
|
+
output = StringIO.new
|
87
|
+
logger = Twiglet::Logger.new('petshop',
|
88
|
+
now: @now,
|
89
|
+
output: output,
|
90
|
+
default_properties: extra_properties)
|
105
91
|
|
106
|
-
|
107
|
-
|
108
|
-
message['message'] = 'Guinea pigs arrived'
|
109
|
-
@logger.debug(message)
|
110
|
-
log = read_json(@buffer)
|
92
|
+
logger.error({message: 'GET /cats'})
|
93
|
+
log = read_json output
|
111
94
|
|
112
|
-
|
113
|
-
|
95
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
96
|
+
assert_equal 'petshop', log[:service][:name]
|
97
|
+
assert_equal 'shop', log[:service][:type]
|
98
|
+
assert_equal 'get', log[:request][:method]
|
99
|
+
assert_equal 200, log[:response][:status_code]
|
100
|
+
end
|
114
101
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
102
|
+
it "should be able to add properties with '.with'" do
|
103
|
+
# Let's add some context to this customer journey
|
104
|
+
purchase_logger = @logger.with({
|
105
|
+
trace: {id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'},
|
106
|
+
customer: {full_name: 'Freda Bloggs'},
|
107
|
+
event: {action: 'pet purchase'}
|
108
|
+
})
|
109
|
+
|
110
|
+
# do stuff
|
111
|
+
purchase_logger.info({
|
112
|
+
message: 'customer bought a dog',
|
113
|
+
pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
|
114
|
+
})
|
115
|
+
|
116
|
+
log = read_json @buffer
|
117
|
+
|
118
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
119
|
+
assert_equal 'Freda Bloggs', log[:customer][:full_name]
|
120
|
+
assert_equal 'pet purchase', log[:event][:action]
|
121
|
+
assert_equal 'customer bought a dog', log[:message]
|
122
|
+
assert_equal 'Barker', log[:pet][:name]
|
123
|
+
end
|
131
124
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
"pet.species": 'dog'
|
138
|
-
})
|
139
|
-
log = read_json(@buffer)
|
140
|
-
|
141
|
-
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
142
|
-
assert_equal 'customer bought a dog', log[:message]
|
143
|
-
assert_equal 'Barker', log[:pet][:name]
|
144
|
-
assert_equal 'dog', log[:pet][:species]
|
145
|
-
assert_equal 'Bitsa', log[:pet][:breed]
|
146
|
-
end
|
125
|
+
it "should log 'message' string property" do
|
126
|
+
message = {}
|
127
|
+
message['message'] = 'Guinea pigs arrived'
|
128
|
+
@logger.debug(message)
|
129
|
+
log = read_json(@buffer)
|
147
130
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
131
|
+
assert_equal 'Guinea pigs arrived', log[:message]
|
132
|
+
end
|
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
|
+
|
149
|
+
it 'should be able to convert dotted keys to nested objects' do
|
150
|
+
@logger.debug({
|
151
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
152
|
+
message: 'customer bought a dog',
|
153
|
+
"pet.name": 'Barker',
|
154
|
+
"pet.species": 'dog',
|
155
|
+
"pet.breed": 'Bitsa'
|
156
|
+
})
|
157
|
+
log = read_json(@buffer)
|
158
|
+
|
159
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
160
|
+
assert_equal 'customer bought a dog', log[:message]
|
161
|
+
assert_equal 'Barker', log[:pet][:name]
|
162
|
+
assert_equal 'dog', log[:pet][:species]
|
163
|
+
assert_equal 'Bitsa', log[:pet][:breed]
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'should be able to mix dotted keys and nested objects' do
|
167
|
+
@logger.debug({
|
168
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
169
|
+
message: 'customer bought a dog',
|
170
|
+
pet: {name: 'Barker', breed: 'Bitsa'},
|
171
|
+
"pet.species": 'dog'
|
172
|
+
})
|
173
|
+
log = read_json(@buffer)
|
174
|
+
|
175
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
176
|
+
assert_equal 'customer bought a dog', log[:message]
|
177
|
+
assert_equal 'Barker', log[:pet][:name]
|
178
|
+
assert_equal 'dog', log[:pet][:species]
|
179
|
+
assert_equal 'Bitsa', log[:pet][:breed]
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should work with mixed string and symbol properties' do
|
183
|
+
log = {
|
184
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
185
|
+
}
|
186
|
+
event = {}
|
187
|
+
log['event'] = event
|
188
|
+
log['message'] = 'customer bought a dog'
|
189
|
+
pet = {}
|
190
|
+
pet['name'] = 'Barker'
|
191
|
+
pet['breed'] = 'Bitsa'
|
192
|
+
pet[:species] = 'dog'
|
193
|
+
log[:pet] = pet
|
194
|
+
|
195
|
+
@logger.debug(log)
|
196
|
+
actual_log = read_json(@buffer)
|
197
|
+
|
198
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', actual_log[:trace][:id]
|
199
|
+
assert_equal 'customer bought a dog', actual_log[:message]
|
200
|
+
assert_equal 'Barker', actual_log[:pet][:name]
|
201
|
+
assert_equal 'dog', actual_log[:pet][:species]
|
202
|
+
assert_equal 'Bitsa', actual_log[:pet][:breed]
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'should log an error with backtrace' do
|
206
|
+
begin
|
207
|
+
1 / 0
|
208
|
+
rescue StandardError => e
|
209
|
+
@logger.error({message: 'Artificially raised exception'}, e)
|
210
|
+
end
|
211
|
+
|
212
|
+
actual_log = read_json(@buffer)
|
213
|
+
|
214
|
+
assert_equal 'Artificially raised exception', actual_log[:message]
|
215
|
+
assert_equal 'divided by 0', actual_log[:error][:message]
|
216
|
+
assert_match 'logger_test.rb', actual_log[:error][:stack_trace].lines.first
|
217
|
+
end
|
170
218
|
|
171
|
-
|
172
|
-
|
173
|
-
1 / 0
|
174
|
-
rescue StandardError => e
|
219
|
+
it 'should log an error without backtrace' do
|
220
|
+
e = StandardError.new('Connection timed-out')
|
175
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)
|
176
228
|
end
|
177
229
|
|
178
|
-
|
230
|
+
LEVELS.each do |attrs|
|
231
|
+
it "should correctly log level when calling #{attrs[:method]}" do
|
232
|
+
@logger.public_send(attrs[:method], {message: 'a log message'})
|
233
|
+
actual_log = read_json(@buffer)
|
179
234
|
|
180
|
-
|
181
|
-
|
182
|
-
|
235
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
236
|
+
assert_equal 'a log message', actual_log[:message]
|
237
|
+
end
|
238
|
+
end
|
183
239
|
end
|
184
240
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
]
|
241
|
+
describe 'text logging' do
|
242
|
+
it 'should throw an error with an empty message' do
|
243
|
+
assert_raises RuntimeError do
|
244
|
+
@logger.info('')
|
245
|
+
end
|
246
|
+
end
|
192
247
|
|
193
|
-
|
194
|
-
|
195
|
-
@logger.public_send(attrs[:method], {message: 'a log message'})
|
248
|
+
it 'should log mandatory attributes' do
|
249
|
+
@logger.error('Out of pets exception')
|
196
250
|
actual_log = read_json(@buffer)
|
197
251
|
|
198
|
-
|
199
|
-
|
252
|
+
expected_log = {
|
253
|
+
message: 'Out of pets exception',
|
254
|
+
"@timestamp": '2020-05-11T15:01:01.000Z',
|
255
|
+
service: {
|
256
|
+
name: 'petshop'
|
257
|
+
},
|
258
|
+
log: {
|
259
|
+
level: 'error'
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
assert_equal expected_log, actual_log
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'should log the provided message' do
|
267
|
+
@logger.error('Emergency! Emergency!')
|
268
|
+
log = read_json(@buffer)
|
269
|
+
|
270
|
+
assert_equal 'Emergency! Emergency!', log[:message]
|
271
|
+
end
|
272
|
+
|
273
|
+
LEVELS.each do |attrs|
|
274
|
+
it "should correctly log level when calling #{attrs[:method]}" do
|
275
|
+
@logger.public_send(attrs[:method], 'a log message')
|
276
|
+
actual_log = read_json(@buffer)
|
277
|
+
|
278
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
279
|
+
assert_equal 'a log message', actual_log[:message]
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
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
|
200
307
|
end
|
201
308
|
end
|
202
309
|
|
@@ -207,3 +314,4 @@ describe Twiglet::Logger do
|
|
207
314
|
JSON.parse(buffer.read, symbolize_names: true)
|
208
315
|
end
|
209
316
|
end
|
317
|
+
# 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.2
|
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-25 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
|
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
|