twiglet 2.0.0.pre.alpha.1 → 2.2.1
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/CODEOWNERS +0 -1
- data/.github/workflows/ruby.yml +3 -2
- data/.rubocop.yml +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +31 -1
- data/example_app.rb +3 -0
- data/lib/twiglet/formatter.rb +69 -0
- data/lib/twiglet/logger.rb +20 -47
- data/lib/twiglet/version.rb +1 -1
- data/test/formatter_test.rb +31 -0
- data/test/logger_test.rb +267 -143
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3cb9302ac1ae427b94de457a976ace7c30d30b875676f43e91b4e206056cedf9
|
4
|
+
data.tar.gz: e436939ff209613f1a4f0c64c33f06a0cb3dcf36c9faa20b25bfc8c829946a26
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06f15914e02513b777e473c6472186f1d189dae59df2e8e59a5f00efad3d7a45d4cf053b96e862c0b9337b70c2c672de12fe73d9e766ceaa047d483005f5000b
|
7
|
+
data.tar.gz: 35fbe15d0b18895fdb5443e2493d212c66989fea77eea204b2cb900535da78133e001c638981e856deacca3c905427222fa2a7073f895c1b26dbb270b63397c8
|
data/.github/CODEOWNERS
CHANGED
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/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -40,6 +40,24 @@ 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
|
+
|
54
|
+
Errors can be logged as well, and this will log the error message and backtrace in the relevant ECS compliant fields:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
db_err = StandardError.new('Connection timed-out')
|
58
|
+
logger.error({ message: 'DB connection failed.' }, db_err)
|
59
|
+
```
|
60
|
+
|
43
61
|
Add log event specific information simply as attributes in a hash:
|
44
62
|
|
45
63
|
```ruby
|
@@ -61,6 +79,18 @@ This writes:
|
|
61
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"}}
|
62
80
|
```
|
63
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
|
+
|
64
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:
|
65
95
|
|
66
96
|
```ruby
|
@@ -126,7 +156,7 @@ First: Please read our project [Code of Conduct](../CODE_OF_CONDUCT.md).
|
|
126
156
|
Second: run the tests and make sure your changes don't break anything:
|
127
157
|
|
128
158
|
```bash
|
129
|
-
|
159
|
+
bundle exec rake test
|
130
160
|
```
|
131
161
|
|
132
162
|
Then please feel free to submit a PR.
|
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,36 +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
|
-
def error(message, error = nil)
|
30
|
+
def error(message = {}, error = nil, &block)
|
40
31
|
if error
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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)
|
45
39
|
end
|
46
40
|
|
47
|
-
|
48
|
-
end
|
49
|
-
|
50
|
-
def critical(message)
|
51
|
-
log(level: 'critical', message: message)
|
41
|
+
super(message, &block)
|
52
42
|
end
|
53
43
|
|
54
44
|
def with(default_properties)
|
@@ -58,30 +48,13 @@ module Twiglet
|
|
58
48
|
output: @output)
|
59
49
|
end
|
60
50
|
|
61
|
-
|
62
|
-
|
63
|
-
def log(level:, message:)
|
64
|
-
raise 'Message must be a Hash' unless message.is_a?(Hash)
|
65
|
-
|
66
|
-
message = message.transform_keys(&:to_sym)
|
67
|
-
message.key?(:message) || raise('Log object must have a \'message\' property')
|
51
|
+
alias_method :warning, :warn
|
52
|
+
alias_method :critical, :fatal
|
68
53
|
|
69
|
-
|
70
|
-
|
71
|
-
base_message = {
|
72
|
-
service: {
|
73
|
-
name: @service_name
|
74
|
-
},
|
75
|
-
"@timestamp": @now.call.iso8601(3),
|
76
|
-
log: {
|
77
|
-
level: level
|
78
|
-
}
|
79
|
-
}
|
54
|
+
private
|
80
55
|
|
81
|
-
|
82
|
-
|
83
|
-
.deep_merge(message.to_nested)
|
84
|
-
.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
|
85
58
|
end
|
86
59
|
end
|
87
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,174 +13,296 @@ 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
|
-
log = read_json(@buffer)
|
124
|
-
|
125
|
-
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
126
|
-
assert_equal 'customer bought a dog', log[:message]
|
127
|
-
assert_equal 'Barker', log[:pet][:name]
|
128
|
-
assert_equal 'dog', log[:pet][:species]
|
129
|
-
assert_equal 'Bitsa', log[:pet][:breed]
|
130
|
-
end
|
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
|
+
})
|
131
109
|
|
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
|
110
|
+
# do stuff
|
111
|
+
purchase_logger.info({
|
112
|
+
message: 'customer bought a dog',
|
113
|
+
pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
|
114
|
+
})
|
147
115
|
|
148
|
-
|
149
|
-
log = {
|
150
|
-
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
151
|
-
}
|
152
|
-
event = {}
|
153
|
-
log['event'] = event
|
154
|
-
log['message'] = 'customer bought a dog'
|
155
|
-
pet = {}
|
156
|
-
pet['name'] = 'Barker'
|
157
|
-
pet['breed'] = 'Bitsa'
|
158
|
-
pet[:species] = 'dog'
|
159
|
-
log[:pet] = pet
|
160
|
-
|
161
|
-
@logger.debug(log)
|
162
|
-
actual_log = read_json(@buffer)
|
163
|
-
|
164
|
-
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', actual_log[:trace][:id]
|
165
|
-
assert_equal 'customer bought a dog', actual_log[:message]
|
166
|
-
assert_equal 'Barker', actual_log[:pet][:name]
|
167
|
-
assert_equal 'dog', actual_log[:pet][:species]
|
168
|
-
assert_equal 'Bitsa', actual_log[:pet][:breed]
|
169
|
-
end
|
116
|
+
log = read_json @buffer
|
170
117
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
124
|
+
|
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)
|
130
|
+
|
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","service":{"name":"petshop"},"log":{"level":"debug"},"message":"hi"}'\
|
140
|
+
"\n"\
|
141
|
+
'{"@timestamp":"2020-05-11T15:01:01.000Z","service":{"name":"petshop"},"log":{"level":"info"},"message":"there"}'\
|
142
|
+
"\n"\
|
143
|
+
|
144
|
+
assert_equal expected_output, @buffer.string
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should be able to convert dotted keys to nested objects' do
|
148
|
+
@logger.debug({
|
149
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
150
|
+
message: 'customer bought a dog',
|
151
|
+
"pet.name": 'Barker',
|
152
|
+
"pet.species": 'dog',
|
153
|
+
"pet.breed": 'Bitsa'
|
154
|
+
})
|
155
|
+
log = read_json(@buffer)
|
156
|
+
|
157
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
158
|
+
assert_equal 'customer bought a dog', log[:message]
|
159
|
+
assert_equal 'Barker', log[:pet][:name]
|
160
|
+
assert_equal 'dog', log[:pet][:species]
|
161
|
+
assert_equal 'Bitsa', log[:pet][:breed]
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'should be able to mix dotted keys and nested objects' do
|
165
|
+
@logger.debug({
|
166
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
167
|
+
message: 'customer bought a dog',
|
168
|
+
pet: {name: 'Barker', breed: 'Bitsa'},
|
169
|
+
"pet.species": 'dog'
|
170
|
+
})
|
171
|
+
log = read_json(@buffer)
|
172
|
+
|
173
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
174
|
+
assert_equal 'customer bought a dog', log[:message]
|
175
|
+
assert_equal 'Barker', log[:pet][:name]
|
176
|
+
assert_equal 'dog', log[:pet][:species]
|
177
|
+
assert_equal 'Bitsa', log[:pet][:breed]
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'should work with mixed string and symbol properties' do
|
181
|
+
log = {
|
182
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
183
|
+
}
|
184
|
+
event = {}
|
185
|
+
log['event'] = event
|
186
|
+
log['message'] = 'customer bought a dog'
|
187
|
+
pet = {}
|
188
|
+
pet['name'] = 'Barker'
|
189
|
+
pet['breed'] = 'Bitsa'
|
190
|
+
pet[:species] = 'dog'
|
191
|
+
log[:pet] = pet
|
192
|
+
|
193
|
+
@logger.debug(log)
|
194
|
+
actual_log = read_json(@buffer)
|
195
|
+
|
196
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', actual_log[:trace][:id]
|
197
|
+
assert_equal 'customer bought a dog', actual_log[:message]
|
198
|
+
assert_equal 'Barker', actual_log[:pet][:name]
|
199
|
+
assert_equal 'dog', actual_log[:pet][:species]
|
200
|
+
assert_equal 'Bitsa', actual_log[:pet][:breed]
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'should log an error with backtrace' do
|
204
|
+
begin
|
205
|
+
1 / 0
|
206
|
+
rescue StandardError => e
|
207
|
+
@logger.error({message: 'Artificially raised exception'}, e)
|
208
|
+
end
|
209
|
+
|
210
|
+
actual_log = read_json(@buffer)
|
211
|
+
|
212
|
+
assert_equal 'Artificially raised exception', actual_log[:message]
|
213
|
+
assert_equal 'divided by 0', actual_log[:error][:message]
|
214
|
+
assert_match 'logger_test.rb', actual_log[:error][:stack_trace].lines.first
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should log an error without backtrace' do
|
218
|
+
e = StandardError.new('Connection timed-out')
|
175
219
|
@logger.error({message: 'Artificially raised exception'}, e)
|
220
|
+
|
221
|
+
actual_log = read_json(@buffer)
|
222
|
+
|
223
|
+
assert_equal 'Artificially raised exception', actual_log[:message]
|
224
|
+
assert_equal 'Connection timed-out', actual_log[:error][:message]
|
225
|
+
refute actual_log[:error].key?(:stack_trace)
|
226
|
+
end
|
227
|
+
|
228
|
+
LEVELS.each do |attrs|
|
229
|
+
it "should correctly log level when calling #{attrs[:method]}" do
|
230
|
+
@logger.public_send(attrs[:method], {message: 'a log message'})
|
231
|
+
actual_log = read_json(@buffer)
|
232
|
+
|
233
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
234
|
+
assert_equal 'a log message', actual_log[:message]
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
describe 'text logging' do
|
240
|
+
it 'should throw an error with an empty message' do
|
241
|
+
assert_raises RuntimeError do
|
242
|
+
@logger.info('')
|
243
|
+
end
|
176
244
|
end
|
177
245
|
|
178
|
-
|
246
|
+
it 'should log mandatory attributes' do
|
247
|
+
@logger.error('Out of pets exception')
|
248
|
+
actual_log = read_json(@buffer)
|
179
249
|
|
180
|
-
|
181
|
-
|
182
|
-
|
250
|
+
expected_log = {
|
251
|
+
message: 'Out of pets exception',
|
252
|
+
"@timestamp": '2020-05-11T15:01:01.000Z',
|
253
|
+
service: {
|
254
|
+
name: 'petshop'
|
255
|
+
},
|
256
|
+
log: {
|
257
|
+
level: 'error'
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
assert_equal expected_log, actual_log
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'should log the provided message' do
|
265
|
+
@logger.error('Emergency! Emergency!')
|
266
|
+
log = read_json(@buffer)
|
267
|
+
|
268
|
+
assert_equal 'Emergency! Emergency!', log[:message]
|
269
|
+
end
|
270
|
+
|
271
|
+
LEVELS.each do |attrs|
|
272
|
+
it "should correctly log level when calling #{attrs[:method]}" do
|
273
|
+
@logger.public_send(attrs[:method], 'a log message')
|
274
|
+
actual_log = read_json(@buffer)
|
275
|
+
|
276
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
277
|
+
assert_equal 'a log message', actual_log[:message]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe 'logging with a block' do
|
283
|
+
LEVELS.each do |attrs|
|
284
|
+
it "should correctly log the block when calling #{attrs[:method]}" do
|
285
|
+
block = proc { 'a block log message' }
|
286
|
+
@logger.public_send(attrs[:method], &block)
|
287
|
+
actual_log = read_json(@buffer)
|
288
|
+
|
289
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
290
|
+
assert_equal 'a block log message', actual_log[:message]
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
describe 'logger level' do
|
296
|
+
[
|
297
|
+
{ expression: :info, level: 1 },
|
298
|
+
{ expression: 'warn', level: 2 },
|
299
|
+
{ expression: Logger::DEBUG, level: 0 }
|
300
|
+
].each do |args|
|
301
|
+
it "sets the severity threshold to level #{args[:level]}" do
|
302
|
+
@logger.level = args[:expression]
|
303
|
+
assert_equal args[:level], @logger.level
|
304
|
+
end
|
305
|
+
end
|
183
306
|
end
|
184
307
|
|
185
308
|
private
|
@@ -189,3 +312,4 @@ describe Twiglet::Logger do
|
|
189
312
|
JSON.parse(buffer.read, symbolize_names: true)
|
190
313
|
end
|
191
314
|
end
|
315
|
+
# 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.1
|
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:
|
@@ -31,8 +31,10 @@ files:
|
|
31
31
|
- Rakefile
|
32
32
|
- example_app.rb
|
33
33
|
- lib/hash_extensions.rb
|
34
|
+
- lib/twiglet/formatter.rb
|
34
35
|
- lib/twiglet/logger.rb
|
35
36
|
- lib/twiglet/version.rb
|
37
|
+
- test/formatter_test.rb
|
36
38
|
- test/hash_extensions_test.rb
|
37
39
|
- test/logger_test.rb
|
38
40
|
- twiglet.gemspec
|
@@ -51,9 +53,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
53
|
version: '2.6'
|
52
54
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
55
|
requirements:
|
54
|
-
- - "
|
56
|
+
- - ">="
|
55
57
|
- !ruby/object:Gem::Version
|
56
|
-
version:
|
58
|
+
version: '0'
|
57
59
|
requirements: []
|
58
60
|
rubygems_version: 3.1.2
|
59
61
|
signing_key:
|