twiglet 2.3.10 → 3.0.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 +1 -1
- data/.rubocop.yml +1 -1
- data/Gemfile +1 -1
- data/Makefile +15 -3
- data/README.md +15 -4
- data/example_app.rb +25 -25
- data/examples/rack/request_logger.rb +30 -13
- data/examples/rack/request_logger_test.rb +38 -15
- data/lib/hash_extensions.rb +3 -3
- data/lib/twiglet/formatter.rb +2 -27
- data/lib/twiglet/logger.rb +3 -1
- data/lib/twiglet/message.rb +24 -0
- data/lib/twiglet/version.rb +1 -1
- data/test/logger_test.rb +67 -60
- data/test/message_test.rb +31 -0
- data/twiglet.gemspec +1 -1
- 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: caa9b2047bc7a8ca268f0ba1b84a983b110234769db49c549f1d61fb21bbc730
|
4
|
+
data.tar.gz: c3eaa4009b34d4d01b4b0b7a90fc5ba1b2e760abb7b137fd92f4679c22de57ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d3ab7e57c1e99ac93349dcc2e75787e08d5f5c5fc6e242a03f286f17ae892ff38f759eacff900bbec46508fe559c5172958e1b94452e6af807e239ae195e29f2
|
7
|
+
data.tar.gz: 4c57202266f609af726de4cc9a60b38f9bcc414dbcdfb5ce26e7f39bb18ebb07f8292310a9fa6a853a49ab9a2f18d53837d77f2321c5046224884aa0b72c5f99
|
data/.github/workflows/ruby.yml
CHANGED
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/Makefile
CHANGED
@@ -1,9 +1,21 @@
|
|
1
|
-
.PHONY: all build test
|
1
|
+
.PHONY: all build clean shell test
|
2
2
|
|
3
|
-
all: build test
|
3
|
+
all: clean build test
|
4
4
|
|
5
5
|
build:
|
6
6
|
docker build -t simplybusiness/ruby-dev:2.6.5 .
|
7
7
|
|
8
|
+
clean:
|
9
|
+
rm -f *~
|
10
|
+
|
11
|
+
shell:
|
12
|
+
docker run -it \
|
13
|
+
-v `pwd`:/var/app \
|
14
|
+
simplybusiness/ruby-dev:2.6.5 \
|
15
|
+
bash
|
16
|
+
|
8
17
|
test:
|
9
|
-
docker run -it
|
18
|
+
docker run -it \
|
19
|
+
-v `pwd`:/var/app \
|
20
|
+
simplybusiness/ruby-dev:2.6.5 \
|
21
|
+
bundle exec rake test
|
data/README.md
CHANGED
@@ -11,13 +11,13 @@ gem install twiglet
|
|
11
11
|
|
12
12
|
## How to use
|
13
13
|
|
14
|
-
|
14
|
+
### Instantiate the logger
|
15
15
|
|
16
16
|
```ruby
|
17
17
|
require 'twiglet/logger'
|
18
18
|
logger = Twiglet::Logger.new('service name')
|
19
19
|
```
|
20
|
-
|
20
|
+
#### Optional initialization parameters
|
21
21
|
A hash can optionally be passed in as a keyword argument for `default_properties`. This hash must be in the Elastic Common Schema format and will be present in every log message created by this Twiglet logger object.
|
22
22
|
|
23
23
|
You may also provide an optional `output` keyword argument which should be an object with a `puts` method - like `$stdout`.
|
@@ -28,7 +28,7 @@ Lastly, you may provide the optional keyword argument `level` to initialize the
|
|
28
28
|
|
29
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).
|
30
30
|
|
31
|
-
|
31
|
+
### Invoke the Logger
|
32
32
|
|
33
33
|
```ruby
|
34
34
|
logger.error({ event: { action: 'startup' }, message: "Emergency! There's an Emergency going on" })
|
@@ -56,6 +56,7 @@ This will write to STDOUT a JSON string:
|
|
56
56
|
|
57
57
|
A message is always required unless a block is provided. The message can be an object or a string.
|
58
58
|
|
59
|
+
#### Error logging
|
59
60
|
An optional error can also be provided, in which case the error message and backtrace will be logged in the relevant ECS compliant fields:
|
60
61
|
|
61
62
|
```ruby
|
@@ -72,7 +73,8 @@ These will both result in the same JSON string written to STDOUT:
|
|
72
73
|
{"ecs":{"version":"1.5.0"},"@timestamp":"2020-08-21T15:44:37.890Z","service":{"name":"service name"},"log":{"level":"error"},"message":"DB connection failed.","error":{"message":"Connection timed-out"}}
|
73
74
|
```
|
74
75
|
|
75
|
-
|
76
|
+
#### Custom fields
|
77
|
+
Log custom event-specific information simply as attributes in a hash:
|
76
78
|
|
77
79
|
```ruby
|
78
80
|
logger.info({
|
@@ -129,6 +131,15 @@ which will print:
|
|
129
131
|
{"service":{"name":"service name"},"@timestamp":"2020-05-14T10:58:30.780+01:00","log":{"level":"error"},"event":{"action":"HTTP request"},"trace":{"id":"126bb6fa-28a2-470f-b013-eefbf9182b2d"},"message":"Error 500 in /pets/buy","http":{"request":{"method":"post","url.path":"/pet/buy"},"response":{"status_code":500}}}
|
130
132
|
```
|
131
133
|
|
134
|
+
### Log formatting
|
135
|
+
Some third party applications will allow you to optionally specify a [log formatter](https://ruby-doc.org/stdlib-2.4.0/libdoc/logger/rdoc/Logger/Formatter.html).
|
136
|
+
Supplying a Twiglet log formatter will format those third party logs so that they are ECS compliant and have the same default parameters as your application's internal logs.
|
137
|
+
|
138
|
+
To access the formatter:
|
139
|
+
```ruby
|
140
|
+
logger.formatter
|
141
|
+
```
|
142
|
+
|
132
143
|
## Use of dotted keys (DEPRECATED)
|
133
144
|
|
134
145
|
Writing nested json objects could be confusing. This library has a built-in feature to convert dotted keys into nested objects, so if you log like this:
|
data/example_app.rb
CHANGED
@@ -8,27 +8,27 @@ logger = Twiglet::Logger.new('petshop')
|
|
8
8
|
|
9
9
|
# Start our petshop
|
10
10
|
logger.info({
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
event: {
|
12
|
+
action: 'startup'
|
13
|
+
},
|
14
|
+
message: "Ready to go, listening on port #{PORT}",
|
15
|
+
server: {
|
16
|
+
port: PORT
|
17
|
+
}
|
18
|
+
})
|
19
19
|
|
20
20
|
# Use text logging
|
21
21
|
logger.info("Ready to go, listening on port #{PORT}")
|
22
22
|
#
|
23
23
|
# We get a request
|
24
24
|
request_logger = logger.with({
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
25
|
+
event: {
|
26
|
+
action: 'HTTP request'
|
27
|
+
},
|
28
|
+
trace: {
|
29
|
+
id: '126bb6fa-28a2-470f-b013-eefbf9182b2d'
|
30
|
+
}
|
31
|
+
})
|
32
32
|
|
33
33
|
# Oh noes!
|
34
34
|
db_err = StandardError.new('Connection timed-out')
|
@@ -37,16 +37,16 @@ request_logger.error({ message: 'DB connection failed.' }, db_err) if db_err
|
|
37
37
|
|
38
38
|
# We return an error to the requester
|
39
39
|
request_logger.info({
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
40
|
+
message: 'Internal Server Error',
|
41
|
+
http: {
|
42
|
+
request: {
|
43
|
+
method: 'get'
|
44
|
+
},
|
45
|
+
response: {
|
46
|
+
status_code: 500
|
47
|
+
}
|
48
|
+
}
|
49
|
+
})
|
50
50
|
|
51
51
|
# Logging with an empty message is an anti-pattern and is therefore forbidden
|
52
52
|
# Both of the following lines would throw an error
|
@@ -11,7 +11,7 @@ class RequestLogger
|
|
11
11
|
[status, headers, body]
|
12
12
|
rescue StandardError => e
|
13
13
|
log_error(env, 500, e)
|
14
|
-
|
14
|
+
raise e
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
@@ -26,24 +26,41 @@ class RequestLogger
|
|
26
26
|
@logger.error(fields, error)
|
27
27
|
end
|
28
28
|
|
29
|
+
# https://www.elastic.co/guide/en/ecs/1.5/ecs-field-reference.html
|
29
30
|
def get_fields(env, status)
|
30
31
|
message = "#{env['REQUEST_METHOD']}: #{env['PATH_INFO']}"
|
31
32
|
|
32
33
|
{
|
33
|
-
http:
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
},
|
41
|
-
response: {
|
42
|
-
status: status,
|
43
|
-
body: { bytes: env['CONTENT_LENGTH'] }
|
44
|
-
}
|
34
|
+
http: http_fields(env, status),
|
35
|
+
url: url_fields(env),
|
36
|
+
client: {
|
37
|
+
ip: env['HTTP_TRUE_CLIENT_IP'] || env['REMOTE_ADDR']
|
38
|
+
},
|
39
|
+
user_agent: {
|
40
|
+
original: env['HTTP_USER_AGENT']
|
45
41
|
},
|
46
42
|
message: message
|
47
43
|
}
|
48
44
|
end
|
45
|
+
|
46
|
+
def http_fields(env, status)
|
47
|
+
{
|
48
|
+
request: {
|
49
|
+
method: env['REQUEST_METHOD'],
|
50
|
+
mime_type: env['HTTP_ACCEPT']
|
51
|
+
},
|
52
|
+
response: {
|
53
|
+
status: status
|
54
|
+
},
|
55
|
+
version: env['HTTP_VERSION']
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def url_fields(env)
|
60
|
+
{
|
61
|
+
path: env['PATH_INFO'],
|
62
|
+
query: env['QUERY_STRING'],
|
63
|
+
domain: env['SERVER_NAME']
|
64
|
+
}
|
65
|
+
end
|
49
66
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'minitest/autorun'
|
2
|
+
require_relative '../../lib/twiglet/logger'
|
2
3
|
require_relative './request_logger'
|
3
4
|
require 'rack'
|
4
5
|
|
@@ -14,23 +15,43 @@ describe RequestLogger do
|
|
14
15
|
end
|
15
16
|
|
16
17
|
it 'logs the request data' do
|
17
|
-
request.get("/some/path?some_var=1"
|
18
|
+
request.get("/some/path?some_var=1", 'HTTP_ACCEPT' => 'application/json',
|
19
|
+
'REMOTE_ADDR' => '0.0.0.0',
|
20
|
+
'HTTP_VERSION' => 'HTTP/1.1',
|
21
|
+
'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh)')
|
18
22
|
log = JSON.parse(output.string)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
|
24
|
+
expected_log = {
|
25
|
+
"log" => { "level" => "info" },
|
26
|
+
"http" => {
|
27
|
+
"request" => {
|
28
|
+
"method" => "GET",
|
29
|
+
"mime_type" => 'application/json'
|
30
|
+
},
|
31
|
+
"response" => {
|
32
|
+
"status" => 200
|
33
|
+
},
|
34
|
+
"version" => 'HTTP/1.1'
|
35
|
+
},
|
36
|
+
"url" => {
|
23
37
|
"path" => "/some/path",
|
24
38
|
"query" => "some_var=1",
|
25
|
-
"
|
39
|
+
"domain" => "example.org"
|
40
|
+
},
|
41
|
+
"client" => {
|
42
|
+
'ip' => '0.0.0.0'
|
26
43
|
},
|
27
|
-
"
|
28
|
-
"
|
29
|
-
|
30
|
-
|
44
|
+
"user_agent" => {
|
45
|
+
"original" => 'Mozilla/5.0 (Macintosh)'
|
46
|
+
},
|
47
|
+
"message" => "GET: /some/path"
|
31
48
|
}
|
32
|
-
|
33
|
-
assert_equal
|
49
|
+
|
50
|
+
assert_equal(log['log'], expected_log['log'])
|
51
|
+
assert_equal(log['http'], expected_log['http'])
|
52
|
+
assert_equal(log['url'], expected_log['url'])
|
53
|
+
assert_equal(log['user_agent'], expected_log['user_agent'])
|
54
|
+
assert_equal(log['message'], expected_log['message'])
|
34
55
|
end
|
35
56
|
|
36
57
|
it 'does not log PII' do
|
@@ -42,10 +63,12 @@ describe RequestLogger do
|
|
42
63
|
end
|
43
64
|
|
44
65
|
it 'logs an error message when a request is bad' do
|
45
|
-
bad_request.get("/some/path")
|
66
|
+
-> { bad_request.get("/some/path") }.must_raise StandardError
|
46
67
|
log = JSON.parse(output.string)
|
47
|
-
assert_equal
|
48
|
-
assert_equal
|
68
|
+
assert_equal log['log']['level'], 'error'
|
69
|
+
assert_equal log['error']['message'], 'some exception'
|
70
|
+
assert_equal log['error']['type'], 'StandardError'
|
71
|
+
assert_includes log['error']['stack_trace'], 'request_logger_test.rb'
|
49
72
|
end
|
50
73
|
end
|
51
74
|
|
data/lib/hash_extensions.rb
CHANGED
@@ -22,8 +22,8 @@ module HashExtensions
|
|
22
22
|
|
23
23
|
def build_nested_object(key, val)
|
24
24
|
key.to_s
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
.split('.')
|
26
|
+
.reverse
|
27
|
+
.reduce(val) { |nested, key_part| Hash[key_part.to_sym, nested] }
|
28
28
|
end
|
29
29
|
end
|
data/lib/twiglet/formatter.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'logger'
|
2
2
|
require_relative '../hash_extensions'
|
3
|
+
require_relative 'message'
|
3
4
|
|
4
5
|
module Twiglet
|
5
6
|
class Formatter < ::Logger::Formatter
|
@@ -17,38 +18,12 @@ module Twiglet
|
|
17
18
|
|
18
19
|
def call(severity, _time, _progname, msg)
|
19
20
|
level = severity.downcase
|
20
|
-
log(level: level, message: msg)
|
21
|
+
log(level: level, message: Message.new(msg))
|
21
22
|
end
|
22
23
|
|
23
24
|
private
|
24
25
|
|
25
26
|
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
27
|
base_message = {
|
53
28
|
ecs: {
|
54
29
|
version: '1.5.0'
|
data/lib/twiglet/logger.rb
CHANGED
@@ -5,6 +5,7 @@ require 'time'
|
|
5
5
|
require 'json'
|
6
6
|
require_relative 'formatter'
|
7
7
|
require_relative '../hash_extensions'
|
8
|
+
require_relative 'message'
|
8
9
|
|
9
10
|
module Twiglet
|
10
11
|
class Logger < ::Logger
|
@@ -33,11 +34,12 @@ module Twiglet
|
|
33
34
|
if error
|
34
35
|
error_fields = {
|
35
36
|
'error': {
|
37
|
+
'type': error.class,
|
36
38
|
'message': error.message
|
37
39
|
}
|
38
40
|
}
|
39
41
|
add_stack_trace(error_fields, error)
|
40
|
-
message =
|
42
|
+
message = Message.new(message).merge(error_fields)
|
41
43
|
end
|
42
44
|
|
43
45
|
super(message, &block)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Twiglet
|
2
|
+
class Message < Hash
|
3
|
+
def initialize(msg)
|
4
|
+
case msg
|
5
|
+
when String
|
6
|
+
self[:message] = msg
|
7
|
+
when Hash
|
8
|
+
replace(msg.transform_keys!(&:to_sym))
|
9
|
+
end
|
10
|
+
|
11
|
+
validate!
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def validate!
|
17
|
+
raise 'Message must be initialized with a String or a non-empty Hash' if empty?
|
18
|
+
|
19
|
+
raise 'Log object must have a \'message\' property' unless self[:message]
|
20
|
+
|
21
|
+
raise 'The \'message\' property of the log object must not be empty' if self[:message].strip.empty?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/twiglet/version.rb
CHANGED
data/test/logger_test.rb
CHANGED
@@ -3,6 +3,16 @@
|
|
3
3
|
require 'minitest/autorun'
|
4
4
|
require_relative '../lib/twiglet/logger'
|
5
5
|
|
6
|
+
LEVELS = [
|
7
|
+
{ method: :debug, level: 'debug' },
|
8
|
+
{ method: :info, level: 'info' },
|
9
|
+
{ method: :warning, level: 'warn' },
|
10
|
+
{ method: :warn, level: 'warn' },
|
11
|
+
{ method: :critical, level: 'fatal' },
|
12
|
+
{ method: :fatal, level: 'fatal' },
|
13
|
+
{ method: :error, level: 'error' }
|
14
|
+
].freeze
|
15
|
+
|
6
16
|
# rubocop:disable Metrics/BlockLength
|
7
17
|
describe Twiglet::Logger do
|
8
18
|
before do
|
@@ -13,16 +23,6 @@ describe Twiglet::Logger do
|
|
13
23
|
output: @buffer)
|
14
24
|
end
|
15
25
|
|
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
|
-
|
26
26
|
it 'should throw an error with an empty service name' do
|
27
27
|
assert_raises RuntimeError do
|
28
28
|
Twiglet::Logger.new(' ')
|
@@ -105,16 +105,16 @@ describe Twiglet::Logger do
|
|
105
105
|
it "should be able to add properties with '.with'" do
|
106
106
|
# Let's add some context to this customer journey
|
107
107
|
purchase_logger = @logger.with({
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
108
|
+
trace: {id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'},
|
109
|
+
customer: {full_name: 'Freda Bloggs'},
|
110
|
+
event: {action: 'pet purchase'}
|
111
|
+
})
|
112
112
|
|
113
113
|
# do stuff
|
114
114
|
purchase_logger.info({
|
115
|
-
|
116
|
-
|
117
|
-
|
115
|
+
message: 'customer bought a dog',
|
116
|
+
pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
|
117
|
+
})
|
118
118
|
|
119
119
|
log = read_json @buffer
|
120
120
|
|
@@ -149,39 +149,6 @@ describe Twiglet::Logger do
|
|
149
149
|
assert_equal expected_output, @buffer.string
|
150
150
|
end
|
151
151
|
|
152
|
-
it 'should be able to convert dotted keys to nested objects' do
|
153
|
-
@logger.debug({
|
154
|
-
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
155
|
-
message: 'customer bought a dog',
|
156
|
-
"pet.name": 'Barker',
|
157
|
-
"pet.species": 'dog',
|
158
|
-
"pet.breed": 'Bitsa'
|
159
|
-
})
|
160
|
-
log = read_json(@buffer)
|
161
|
-
|
162
|
-
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
163
|
-
assert_equal 'customer bought a dog', log[:message]
|
164
|
-
assert_equal 'Barker', log[:pet][:name]
|
165
|
-
assert_equal 'dog', log[:pet][:species]
|
166
|
-
assert_equal 'Bitsa', log[:pet][:breed]
|
167
|
-
end
|
168
|
-
|
169
|
-
it 'should be able to mix dotted keys and nested objects' do
|
170
|
-
@logger.debug({
|
171
|
-
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
172
|
-
message: 'customer bought a dog',
|
173
|
-
pet: {name: 'Barker', breed: 'Bitsa'},
|
174
|
-
"pet.species": 'dog'
|
175
|
-
})
|
176
|
-
log = read_json(@buffer)
|
177
|
-
|
178
|
-
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
179
|
-
assert_equal 'customer bought a dog', log[:message]
|
180
|
-
assert_equal 'Barker', log[:pet][:name]
|
181
|
-
assert_equal 'dog', log[:pet][:species]
|
182
|
-
assert_equal 'Bitsa', log[:pet][:breed]
|
183
|
-
end
|
184
|
-
|
185
152
|
it 'should work with mixed string and symbol properties' do
|
186
153
|
log = {
|
187
154
|
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
@@ -205,6 +172,18 @@ describe Twiglet::Logger do
|
|
205
172
|
assert_equal 'Bitsa', actual_log[:pet][:breed]
|
206
173
|
end
|
207
174
|
|
175
|
+
LEVELS.each do |attrs|
|
176
|
+
it "should correctly log level when calling #{attrs[:method]}" do
|
177
|
+
@logger.public_send(attrs[:method], {message: 'a log message'})
|
178
|
+
actual_log = read_json(@buffer)
|
179
|
+
|
180
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
181
|
+
assert_equal 'a log message', actual_log[:message]
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe 'logging an exception' do
|
208
187
|
it 'should log an error with backtrace' do
|
209
188
|
begin
|
210
189
|
1 / 0
|
@@ -216,6 +195,7 @@ describe Twiglet::Logger do
|
|
216
195
|
|
217
196
|
assert_equal 'Artificially raised exception', actual_log[:message]
|
218
197
|
assert_equal 'divided by 0', actual_log[:error][:message]
|
198
|
+
assert_equal 'ZeroDivisionError', actual_log[:error][:type]
|
219
199
|
assert_match 'logger_test.rb', actual_log[:error][:stack_trace].lines.first
|
220
200
|
end
|
221
201
|
|
@@ -226,6 +206,7 @@ describe Twiglet::Logger do
|
|
226
206
|
actual_log = read_json(@buffer)
|
227
207
|
|
228
208
|
assert_equal 'Artificially raised exception', actual_log[:message]
|
209
|
+
assert_equal 'StandardError', actual_log[:error][:type]
|
229
210
|
assert_equal 'Connection timed-out', actual_log[:error][:message]
|
230
211
|
refute actual_log[:error].key?(:stack_trace)
|
231
212
|
end
|
@@ -237,18 +218,9 @@ describe Twiglet::Logger do
|
|
237
218
|
actual_log = read_json(@buffer)
|
238
219
|
|
239
220
|
assert_equal 'Artificially raised exception with string message', actual_log[:message]
|
221
|
+
assert_equal 'StandardError', actual_log[:error][:type]
|
240
222
|
assert_equal 'Unknown error', actual_log[:error][:message]
|
241
223
|
end
|
242
|
-
|
243
|
-
LEVELS.each do |attrs|
|
244
|
-
it "should correctly log level when calling #{attrs[:method]}" do
|
245
|
-
@logger.public_send(attrs[:method], {message: 'a log message'})
|
246
|
-
actual_log = read_json(@buffer)
|
247
|
-
|
248
|
-
assert_equal attrs[:level], actual_log[:log][:level]
|
249
|
-
assert_equal 'a log message', actual_log[:message]
|
250
|
-
end
|
251
|
-
end
|
252
224
|
end
|
253
225
|
|
254
226
|
describe 'text logging' do
|
@@ -310,6 +282,41 @@ describe Twiglet::Logger do
|
|
310
282
|
end
|
311
283
|
end
|
312
284
|
|
285
|
+
describe 'dotted keys' do
|
286
|
+
it 'should be able to convert dotted keys to nested objects' do
|
287
|
+
@logger.debug({
|
288
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
289
|
+
message: 'customer bought a dog',
|
290
|
+
"pet.name": 'Barker',
|
291
|
+
"pet.species": 'dog',
|
292
|
+
"pet.breed": 'Bitsa'
|
293
|
+
})
|
294
|
+
log = read_json(@buffer)
|
295
|
+
|
296
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
297
|
+
assert_equal 'customer bought a dog', log[:message]
|
298
|
+
assert_equal 'Barker', log[:pet][:name]
|
299
|
+
assert_equal 'dog', log[:pet][:species]
|
300
|
+
assert_equal 'Bitsa', log[:pet][:breed]
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'should be able to mix dotted keys and nested objects' do
|
304
|
+
@logger.debug({
|
305
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
306
|
+
message: 'customer bought a dog',
|
307
|
+
pet: {name: 'Barker', breed: 'Bitsa'},
|
308
|
+
"pet.species": 'dog'
|
309
|
+
})
|
310
|
+
log = read_json(@buffer)
|
311
|
+
|
312
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
313
|
+
assert_equal 'customer bought a dog', log[:message]
|
314
|
+
assert_equal 'Barker', log[:pet][:name]
|
315
|
+
assert_equal 'dog', log[:pet][:species]
|
316
|
+
assert_equal 'Bitsa', log[:pet][:breed]
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
313
320
|
describe 'logger level' do
|
314
321
|
[
|
315
322
|
{ expression: :info, level: 1 },
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require_relative '../lib/twiglet/message'
|
3
|
+
|
4
|
+
describe Twiglet::Message do
|
5
|
+
it 'raises if message is empty' do
|
6
|
+
assert_raises RuntimeError do
|
7
|
+
Twiglet::Message.new(' ')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'raises if message is not provided' do
|
12
|
+
assert_raises RuntimeError do
|
13
|
+
Twiglet::Message.new(foo: 'bar')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'raises on unrecognized inputs' do
|
18
|
+
assert_raises RuntimeError do
|
19
|
+
Twiglet::Message.new(OpenStruct.new(message: 'hello'))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns a message hash from a string' do
|
24
|
+
assert_equal Twiglet::Message.new('hello, world'), { message: 'hello, world' }
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns a message hash with symbolized keys' do
|
28
|
+
input_message = { 'key' => 'value', 'message' => 'hello, world' }
|
29
|
+
assert_equal Twiglet::Message.new(input_message), { key: 'value', message: 'hello, world' }
|
30
|
+
end
|
31
|
+
end
|
data/twiglet.gemspec
CHANGED
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:
|
4
|
+
version: 3.0.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:
|
11
|
+
date: 2021-01-15 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Like a log, only smaller.
|
14
14
|
email:
|
@@ -39,10 +39,12 @@ files:
|
|
39
39
|
- lib/hash_extensions.rb
|
40
40
|
- lib/twiglet/formatter.rb
|
41
41
|
- lib/twiglet/logger.rb
|
42
|
+
- lib/twiglet/message.rb
|
42
43
|
- lib/twiglet/version.rb
|
43
44
|
- test/formatter_test.rb
|
44
45
|
- test/hash_extensions_test.rb
|
45
46
|
- test/logger_test.rb
|
47
|
+
- test/message_test.rb
|
46
48
|
- test/test_coverage.rb
|
47
49
|
- twiglet.gemspec
|
48
50
|
homepage: https://github.com/simplybusiness/twiglet-ruby
|
@@ -57,14 +59,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
59
|
requirements:
|
58
60
|
- - ">="
|
59
61
|
- !ruby/object:Gem::Version
|
60
|
-
version: '2.
|
62
|
+
version: '2.6'
|
61
63
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
64
|
requirements:
|
63
65
|
- - ">="
|
64
66
|
- !ruby/object:Gem::Version
|
65
67
|
version: '0'
|
66
68
|
requirements: []
|
67
|
-
rubygems_version: 3.
|
69
|
+
rubygems_version: 3.2.3
|
68
70
|
signing_key:
|
69
71
|
specification_version: 4
|
70
72
|
summary: Twiglet
|