twiglet 2.4.0 → 3.0.4
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/dependabot.yml +22 -0
- data/.github/workflows/ruby.yml +1 -1
- data/.rubocop.yml +1 -1
- data/Gemfile +1 -1
- 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/version.rb +1 -1
- data/test/logger_test.rb +28 -28
- data/twiglet.gemspec +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 501c866a8fafcb3a32d633ce10040ebecd946459a272c87fa925189fbc61caa4
|
4
|
+
data.tar.gz: 8bae82697cd8c606d82c7de19774de485ce0d87d524513de7ba50869218af4a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a522314e822964fcda9508fa7c70456eecef93a89c51e5a43b8baa3d580de3dfc05a3872df8c81a9997825d011f058a6de38d33fe66967b7bbea1d6626c263c0
|
7
|
+
data.tar.gz: 83171f318329ac354dc0d5c823d192e1896b8d3f2cb6b6b927b13ec171174f366f89a4cb6f124752ac7bc2ca3d318e8ab8d87b87e0471ed8d911387c7ebafefe
|
@@ -0,0 +1,22 @@
|
|
1
|
+
version: 2
|
2
|
+
updates:
|
3
|
+
- package-ecosystem: bundler
|
4
|
+
directory: "/"
|
5
|
+
schedule:
|
6
|
+
interval: daily
|
7
|
+
time: "07:00"
|
8
|
+
open-pull-requests-limit: 99
|
9
|
+
labels:
|
10
|
+
- dependencies
|
11
|
+
ignore:
|
12
|
+
- dependency-name: simplecov
|
13
|
+
versions:
|
14
|
+
- ">= 0.18"
|
15
|
+
- package-ecosystem: github-actions
|
16
|
+
directory: "/"
|
17
|
+
schedule:
|
18
|
+
interval: daily
|
19
|
+
time: "07:00"
|
20
|
+
open-pull-requests-limit: 99
|
21
|
+
labels:
|
22
|
+
- dependencies
|
data/.github/workflows/ruby.yml
CHANGED
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
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/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
|
|
@@ -285,12 +285,12 @@ describe Twiglet::Logger do
|
|
285
285
|
describe 'dotted keys' do
|
286
286
|
it 'should be able to convert dotted keys to nested objects' do
|
287
287
|
@logger.debug({
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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
294
|
log = read_json(@buffer)
|
295
295
|
|
296
296
|
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
@@ -302,11 +302,11 @@ describe Twiglet::Logger do
|
|
302
302
|
|
303
303
|
it 'should be able to mix dotted keys and nested objects' do
|
304
304
|
@logger.debug({
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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
310
|
log = read_json(@buffer)
|
311
311
|
|
312
312
|
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
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.4
|
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-25 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Like a log, only smaller.
|
14
14
|
email:
|
@@ -18,6 +18,7 @@ extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
19
19
|
files:
|
20
20
|
- ".github/CODEOWNERS"
|
21
|
+
- ".github/dependabot.yml"
|
21
22
|
- ".github/workflows/gem-publish.yml"
|
22
23
|
- ".github/workflows/ruby.yml"
|
23
24
|
- ".github/workflows/version-forget-me-not.yml"
|
@@ -59,14 +60,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
60
|
requirements:
|
60
61
|
- - ">="
|
61
62
|
- !ruby/object:Gem::Version
|
62
|
-
version: '2.
|
63
|
+
version: '2.6'
|
63
64
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
65
|
requirements:
|
65
66
|
- - ">="
|
66
67
|
- !ruby/object:Gem::Version
|
67
68
|
version: '0'
|
68
69
|
requirements: []
|
69
|
-
rubygems_version: 3.
|
70
|
+
rubygems_version: 3.1.4
|
70
71
|
signing_key:
|
71
72
|
specification_version: 4
|
72
73
|
summary: Twiglet
|