twiglet 1.1.0 → 2.0.0.pre.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.ruby-version +1 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -0
- data/RATIONALE.md +90 -0
- data/README.md +13 -7
- data/Rakefile +7 -0
- data/example_app.rb +2 -2
- data/lib/hash_extensions.rb +29 -0
- data/lib/twiglet/logger.rb +28 -23
- data/lib/twiglet/version.rb +1 -1
- data/test/{elastic_common_schema_test.rb → hash_extensions_test.rb} +13 -14
- data/test/logger_test.rb +27 -26
- data/twiglet.gemspec +1 -1
- metadata +11 -8
- data/lib/elastic_common_schema.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2bb718963cc1602d548f4f80bbf03a932feac4ceaa5b11bc3b6d19a1ee58e10
|
4
|
+
data.tar.gz: 6ee69671edd4a487a905c45dec2833141fc6a4a7b8702bdffdc2dc0b81e6e9e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ccecc55b8320608406a0dfec87f617e12501dbdebbe62de7b74881afde44439ba789c59b80c1530c9e4325dc424a202c1ea71958adf32ce1bda020103b38a48
|
7
|
+
data.tar.gz: 2762f0a75ac22ae69ef30de4afa022dc91e1504e578ce0fcf3198291e0f4963adbda2ab57e7e5b8a852ff2bfe6f058f3478d23c622a09a0eef98cc2821881c4b
|
data/.github/workflows/ruby.yml
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.5
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
data/RATIONALE.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Twiglet
|
2
|
+
|
3
|
+
Why Twiglet? Because it's a log, only micro-sized.
|
4
|
+
|
5
|
+
This is a machine-readable first, human-readable second, JSON-based logging micro-library suitable for a wide variety of microservice uses.
|
6
|
+
|
7
|
+
This logging library is available in a cornucopia of languages:
|
8
|
+
* Ruby - here
|
9
|
+
* Python - TBC
|
10
|
+
* Node.js - [twiglet-node](https://github.com/simplybusiness/twiglet-node/)
|
11
|
+
|
12
|
+
## Design considerations
|
13
|
+
|
14
|
+
The design goals of this library are:
|
15
|
+
|
16
|
+
- Write logs as JSON
|
17
|
+
- One line per log entry
|
18
|
+
- One JSON object per log entry
|
19
|
+
- Each log entry contains a severity[1]
|
20
|
+
- Each log entry contains an ISO8601 UTC timestamp
|
21
|
+
- Each log entry contains the name of the service that wrote it
|
22
|
+
- Each log entry pertains to an 'event'[2]
|
23
|
+
- Each log entry either a) propagates an existing trace.id or b) creates a new trace.id as a correlation ID if one does not already exist[3]
|
24
|
+
- Stack traces are written inside a single JSON log object rather than sprawling over several lines (TODO:)
|
25
|
+
- Personally Identifiable Information. Don't log it.
|
26
|
+
|
27
|
+
[1] It turns out that there isn't a single authoritative standard for severity levels, we chose to go with DEBUG, INFO, WARNING, ERROR and CRITICAL as our choices.
|
28
|
+
[2] The ‘event’ here merely refers to the action that the customer (or employee, service, or other actor) is currently attempting. It does not refer specifically to Kafka, or CQRS, though that might be the case.
|
29
|
+
[3] A correlation ID is a UUIDv4 string
|
30
|
+
|
31
|
+
## Elastic Common Schema (ECS)
|
32
|
+
https://www.elastic.co/blog/introducing-the-elastic-common-schema
|
33
|
+
We have decided to standardise on the Elastic Common Schema for log attribute names. Whilst some attributes are expected for all logs, service owners should feel free to add relevant entries from the ECS schema if they are needed.
|
34
|
+
All application specific information is embedded in the `message` attribute JSON payload.
|
35
|
+
|
36
|
+
| Attribute name (mandatory) | Description |
|
37
|
+
| -------------------------- | ------------------------------- |
|
38
|
+
| log.level | text, one of DEBUG, INFO, WARNING, ERROR or CRITICAL. |
|
39
|
+
| service.name | text, the name of the service |
|
40
|
+
| @timestamp | text, ISO8601 UTC |
|
41
|
+
| message | text, human-readable summary |
|
42
|
+
|
43
|
+
| Attribute name (optional) | Description |
|
44
|
+
| -------------------------- | ------------------------------- |
|
45
|
+
| error.stack_trace | Stack trace, as JSON[4] |
|
46
|
+
| tags | Array, e.g. ["production"] |
|
47
|
+
| trace.id | text, UUIDv4 - a correlation ID |
|
48
|
+
| (other examples) | ... |
|
49
|
+
|
50
|
+
[4] Helper method to be provided to allow stack trace objects to be represented cleanly as JSON.
|
51
|
+
|
52
|
+
Errors should provide appropriate data using the fields from https://www.elastic.co/guide/en/ecs/current/ecs-error.html
|
53
|
+
If any other fields are provided in a log then these should be from the ECS schema rather than in a custom format, if at all possible.
|
54
|
+
|
55
|
+
## Example log output (prettified)
|
56
|
+
```json
|
57
|
+
{
|
58
|
+
"log": {
|
59
|
+
"level": "INFO"
|
60
|
+
},
|
61
|
+
"service": {
|
62
|
+
"name": "payments"
|
63
|
+
},
|
64
|
+
"@timestamp": "2020-05-07T11:51:33.976Z",
|
65
|
+
"event": {
|
66
|
+
"action": "customer-payment-accepted"
|
67
|
+
},
|
68
|
+
"trace": {
|
69
|
+
"id": "bf6f5ea3-614b-42f5-8e73-43deea2d1838"
|
70
|
+
},
|
71
|
+
"tags": ["staging"],
|
72
|
+
"message": "Pet cat Spot purchased",
|
73
|
+
"user": {
|
74
|
+
"email": "sleepyfox@gmail.com"
|
75
|
+
},
|
76
|
+
"pet": {
|
77
|
+
"type": "cat",
|
78
|
+
"name": "Spot",
|
79
|
+
"colour": "Ginger Tabby"
|
80
|
+
}
|
81
|
+
}
|
82
|
+
```
|
83
|
+
|
84
|
+
# Code of conduct
|
85
|
+
|
86
|
+
Please see the [code of conduct](CODE_OF_CONDUCT.md) for further info.
|
87
|
+
|
88
|
+
# License
|
89
|
+
|
90
|
+
This work is licensed under the MIT license - see the [LICENSE](LICENSE) file for further details.
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Twiglet: Ruby version
|
2
2
|
Like a log, only smaller.
|
3
3
|
|
4
|
-
This library provides a minimal JSON logging interface suitable for use in (micro)services.
|
4
|
+
This library provides a minimal JSON logging interface suitable for use in (micro)services. See the [RATIONALE](RATIONALE.md) for design rationale and an explantion of the Elastic Common Schema that we are using for log attribute naming.
|
5
5
|
|
6
6
|
## Installation
|
7
7
|
|
@@ -15,10 +15,16 @@ Create a new logger like so:
|
|
15
15
|
|
16
16
|
```ruby
|
17
17
|
require 'twiglet/logger'
|
18
|
-
logger = Twiglet::Logger.new(
|
18
|
+
logger = Twiglet::Logger.new('service name')
|
19
19
|
```
|
20
20
|
|
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
|
+
|
23
|
+
You may also provide an optional `output` keyword argument which should be an object with a `puts` method - like `$stdout`.
|
24
|
+
|
25
|
+
Lastly, you can provide another optional keyword argument called `now`, which should be a function returning a `Time` string in ISO8601 format.
|
26
|
+
|
27
|
+
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).
|
22
28
|
|
23
29
|
To use, simply invoke like most other loggers:
|
24
30
|
|
@@ -29,7 +35,7 @@ logger.error({ event: { action: 'startup' }, message: "Emergency! There's an Eme
|
|
29
35
|
This will write to STDOUT a JSON string:
|
30
36
|
|
31
37
|
```json
|
32
|
-
{"service":{"name":"
|
38
|
+
{"service":{"name":"service name"},"@timestamp":"2020-05-14T10:54:59.164+01:00","log":{"level":"error"},"event":{"action":"startup"},"message":"Emergency! There's an Emergency going on"}
|
33
39
|
```
|
34
40
|
|
35
41
|
Obviously the timestamp will be different.
|
@@ -52,7 +58,7 @@ logger.info({
|
|
52
58
|
This writes:
|
53
59
|
|
54
60
|
```json
|
55
|
-
{"service":{"name":"
|
61
|
+
{"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"}}
|
56
62
|
```
|
57
63
|
|
58
64
|
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:
|
@@ -76,7 +82,7 @@ request_logger.error({
|
|
76
82
|
which will print:
|
77
83
|
|
78
84
|
```json
|
79
|
-
{"service":{"name":"
|
85
|
+
{"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}}}
|
80
86
|
```
|
81
87
|
|
82
88
|
## Use of dotted keys
|
@@ -110,7 +116,7 @@ logger.info({
|
|
110
116
|
Both cases would print out exact the same log item:
|
111
117
|
|
112
118
|
```json
|
113
|
-
{"service":{"name":"
|
119
|
+
{"service":{"name":"service name"},"@timestamp":"2020-05-14T10:59:31.183+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"}}
|
114
120
|
```
|
115
121
|
|
116
122
|
## How to contribute
|
data/Rakefile
ADDED
data/example_app.rb
CHANGED
@@ -4,7 +4,7 @@ require_relative 'lib/twiglet/logger'
|
|
4
4
|
|
5
5
|
PORT = 8080
|
6
6
|
|
7
|
-
logger = Logger.new(
|
7
|
+
logger = Twiglet::Logger.new('petshop')
|
8
8
|
|
9
9
|
# Start our petshop
|
10
10
|
logger.info({
|
@@ -45,7 +45,7 @@ request_logger.info({
|
|
45
45
|
}
|
46
46
|
})
|
47
47
|
|
48
|
-
# Logging with
|
48
|
+
# Logging with an empty message is an anti-pattern and is therefore forbidden
|
49
49
|
# Both of the following lines would throw an error
|
50
50
|
# request_logger.error({ message: "" })
|
51
51
|
# logger.debug({ message: " " })
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HashExtensions
|
4
|
+
def to_nested
|
5
|
+
self unless contains_dotted_key?
|
6
|
+
|
7
|
+
keys.reduce({}) do |nested, key|
|
8
|
+
nested.deep_merge(build_nested_object(key, self[key]))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def deep_merge(hash_to_merge)
|
13
|
+
merger = proc { |_, val1, val2| val1.is_a?(Hash) && val2.is_a?(Hash) ? val1.merge(val2, &merger) : val2 }
|
14
|
+
merge(hash_to_merge, &merger)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def contains_dotted_key?
|
20
|
+
keys.any? { |x| x.to_s.include?('.') }
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_nested_object(key, val)
|
24
|
+
key.to_s
|
25
|
+
.split('.')
|
26
|
+
.reverse
|
27
|
+
.reduce(val) { |nested, key_part| Hash[key_part.to_sym, nested] }
|
28
|
+
end
|
29
|
+
end
|
data/lib/twiglet/logger.rb
CHANGED
@@ -2,21 +2,26 @@
|
|
2
2
|
|
3
3
|
require 'time'
|
4
4
|
require 'json'
|
5
|
-
require_relative '../
|
5
|
+
require_relative '../hash_extensions'
|
6
6
|
|
7
7
|
module Twiglet
|
8
8
|
class Logger
|
9
|
-
include
|
10
|
-
|
11
|
-
def initialize(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@
|
9
|
+
Hash.include HashExtensions
|
10
|
+
|
11
|
+
def initialize(
|
12
|
+
service_name,
|
13
|
+
default_properties: {},
|
14
|
+
now: -> { Time.now.utc },
|
15
|
+
output: $stdout
|
16
|
+
)
|
17
|
+
@service_name = service_name
|
18
|
+
@now = now
|
19
|
+
@output = output
|
20
|
+
|
21
|
+
raise 'Service name is mandatory' \
|
22
|
+
unless @service_name.is_a?(String) && !@service_name.strip.empty?
|
23
|
+
|
24
|
+
@default_properties = default_properties
|
20
25
|
end
|
21
26
|
|
22
27
|
def debug(message)
|
@@ -46,11 +51,11 @@ module Twiglet
|
|
46
51
|
log(level: 'critical', message: message)
|
47
52
|
end
|
48
53
|
|
49
|
-
def with(
|
50
|
-
Logger.new(
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
+
def with(default_properties)
|
55
|
+
Logger.new(@service_name,
|
56
|
+
default_properties: default_properties,
|
57
|
+
now: @now,
|
58
|
+
output: @output)
|
54
59
|
end
|
55
60
|
|
56
61
|
private
|
@@ -63,20 +68,20 @@ module Twiglet
|
|
63
68
|
|
64
69
|
message[:message].strip.empty? && raise('The \'message\' property of log object must not be empty')
|
65
70
|
|
66
|
-
|
71
|
+
base_message = {
|
67
72
|
service: {
|
68
|
-
name: @
|
73
|
+
name: @service_name
|
69
74
|
},
|
70
75
|
"@timestamp": @now.call.iso8601(3),
|
71
76
|
log: {
|
72
77
|
level: level
|
73
78
|
}
|
74
79
|
}
|
75
|
-
total_message = total_message.merge(@scoped_properties)
|
76
|
-
.merge!(message)
|
77
|
-
.then { |log_entry| to_nested(log_entry) }
|
78
80
|
|
79
|
-
@output.puts
|
81
|
+
@output.puts base_message
|
82
|
+
.deep_merge(@default_properties.to_nested)
|
83
|
+
.deep_merge(message.to_nested)
|
84
|
+
.to_json
|
80
85
|
end
|
81
86
|
end
|
82
87
|
end
|
data/lib/twiglet/version.rb
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'minitest/autorun'
|
4
|
-
require_relative '../lib/
|
4
|
+
require_relative '../lib/hash_extensions'
|
5
5
|
|
6
|
-
describe
|
6
|
+
describe HashExtensions do
|
7
7
|
before do
|
8
|
-
|
9
|
-
@ecs.extend(ElasticCommonSchema)
|
8
|
+
Hash.include HashExtensions
|
10
9
|
end
|
11
10
|
|
12
11
|
it 'should retain an object without . in any keys' do
|
@@ -21,7 +20,7 @@ describe ElasticCommonSchema do
|
|
21
20
|
"@timestamp": '2020-05-09T15:13:20.736Z'
|
22
21
|
}
|
23
22
|
|
24
|
-
expected =
|
23
|
+
expected = actual.to_nested
|
25
24
|
assert_equal actual, expected
|
26
25
|
end
|
27
26
|
|
@@ -31,7 +30,7 @@ describe ElasticCommonSchema do
|
|
31
30
|
"log.level": 'error'
|
32
31
|
}
|
33
32
|
|
34
|
-
nested =
|
33
|
+
nested = actual.to_nested
|
35
34
|
|
36
35
|
assert_equal 'petshop', nested[:service][:name]
|
37
36
|
assert_equal 'error', nested[:log][:level]
|
@@ -45,7 +44,7 @@ describe ElasticCommonSchema do
|
|
45
44
|
"log.level": 'error'
|
46
45
|
}
|
47
46
|
|
48
|
-
nested =
|
47
|
+
nested = actual.to_nested
|
49
48
|
|
50
49
|
assert_equal 'petshop', nested[:service][:name]
|
51
50
|
assert_equal 'ps001', nested[:service][:id]
|
@@ -61,7 +60,7 @@ describe ElasticCommonSchema do
|
|
61
60
|
"http.response.status_code": 200
|
62
61
|
}
|
63
62
|
|
64
|
-
nested =
|
63
|
+
nested = actual.to_nested
|
65
64
|
|
66
65
|
assert_equal 'get', nested[:http][:request][:method]
|
67
66
|
assert_equal 112, nested[:http][:request][:body][:bytes]
|
@@ -73,7 +72,7 @@ describe ElasticCommonSchema do
|
|
73
72
|
first = { id: 1, name: 'petshop' }
|
74
73
|
second = { level: 'debug', code: 5 }
|
75
74
|
|
76
|
-
actual =
|
75
|
+
actual = first.deep_merge(second)
|
77
76
|
|
78
77
|
assert_equal 1, actual[:id]
|
79
78
|
assert_equal 'petshop', actual[:name]
|
@@ -85,7 +84,7 @@ describe ElasticCommonSchema do
|
|
85
84
|
first = { id: 1, name: 'petshop', level: 'debug' }
|
86
85
|
second = { name: 'petstore', level: 'error', code: 5 }
|
87
86
|
|
88
|
-
actual =
|
87
|
+
actual = first.deep_merge(second)
|
89
88
|
|
90
89
|
assert_equal 1, actual[:id]
|
91
90
|
assert_equal 'petstore', actual[:name]
|
@@ -97,7 +96,7 @@ describe ElasticCommonSchema do
|
|
97
96
|
first = { service: { name: 'petshop' } }
|
98
97
|
second = { service: { id: 'ps001' } }
|
99
98
|
|
100
|
-
actual =
|
99
|
+
actual = first.deep_merge(second)
|
101
100
|
assert_equal 'petshop', actual[:service][:name]
|
102
101
|
assert_equal 'ps001', actual[:service][:id]
|
103
102
|
end
|
@@ -106,7 +105,7 @@ describe ElasticCommonSchema do
|
|
106
105
|
first = { http: { request: { method: 'get', bytes: 124 } } }
|
107
106
|
second = { http: { response: { status_code: 200, bytes: 5001 } } }
|
108
107
|
|
109
|
-
actual =
|
108
|
+
actual = first.deep_merge(second)
|
110
109
|
|
111
110
|
assert_equal 'get', actual[:http][:request][:method]
|
112
111
|
assert_equal 124, actual[:http][:request][:bytes]
|
@@ -118,7 +117,7 @@ describe ElasticCommonSchema do
|
|
118
117
|
first = {}
|
119
118
|
second = { id: 1 }
|
120
119
|
|
121
|
-
actual =
|
120
|
+
actual = first.deep_merge(second)
|
122
121
|
|
123
122
|
assert_equal 1, actual[:id]
|
124
123
|
end
|
@@ -127,7 +126,7 @@ describe ElasticCommonSchema do
|
|
127
126
|
first = { id: 1 }
|
128
127
|
second = {}
|
129
128
|
|
130
|
-
actual =
|
129
|
+
actual = first.deep_merge(second)
|
131
130
|
|
132
131
|
assert_equal 1, actual[:id]
|
133
132
|
end
|
data/test/logger_test.rb
CHANGED
@@ -7,16 +7,14 @@ describe Twiglet::Logger do
|
|
7
7
|
before do
|
8
8
|
@now = -> { Time.utc(2020, 5, 11, 15, 1, 1) }
|
9
9
|
@buffer = StringIO.new
|
10
|
-
@logger = Twiglet::Logger.new(
|
11
|
-
|
12
|
-
|
13
|
-
output: @buffer
|
14
|
-
})
|
10
|
+
@logger = Twiglet::Logger.new('petshop',
|
11
|
+
now: @now,
|
12
|
+
output: @buffer)
|
15
13
|
end
|
16
14
|
|
17
15
|
it 'should throw an error with an empty service name' do
|
18
16
|
assert_raises RuntimeError do
|
19
|
-
Twiglet::Logger.new(
|
17
|
+
Twiglet::Logger.new(' ')
|
20
18
|
end
|
21
19
|
end
|
22
20
|
|
@@ -27,7 +25,7 @@ describe Twiglet::Logger do
|
|
27
25
|
end
|
28
26
|
|
29
27
|
it 'should log mandatory attributes' do
|
30
|
-
@logger.error({
|
28
|
+
@logger.error({message: 'Out of pets exception'})
|
31
29
|
actual_log = read_json(@buffer)
|
32
30
|
|
33
31
|
expected_log = {
|
@@ -45,9 +43,9 @@ describe Twiglet::Logger do
|
|
45
43
|
end
|
46
44
|
|
47
45
|
it 'should log the provided message' do
|
48
|
-
@logger.error({
|
49
|
-
|
50
|
-
|
46
|
+
@logger.error({event:
|
47
|
+
{action: 'exception'},
|
48
|
+
message: 'Emergency! Emergency!'})
|
51
49
|
log = read_json(@buffer)
|
52
50
|
|
53
51
|
assert_equal 'exception', log[:event][:action]
|
@@ -59,22 +57,25 @@ describe Twiglet::Logger do
|
|
59
57
|
trace: {
|
60
58
|
id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
61
59
|
},
|
62
|
-
|
63
|
-
|
60
|
+
service: {
|
61
|
+
type: 'shop'
|
62
|
+
},
|
63
|
+
request: {method: 'get'},
|
64
|
+
response: {status_code: 200}
|
64
65
|
}
|
65
66
|
|
66
67
|
output = StringIO.new
|
67
|
-
logger = Twiglet::Logger.new(
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
logger.error({ message: 'GET /cats' })
|
68
|
+
logger = Twiglet::Logger.new('petshop',
|
69
|
+
now: @now,
|
70
|
+
output: output,
|
71
|
+
default_properties: extra_properties)
|
72
|
+
|
73
|
+
logger.error({message: 'GET /cats'})
|
75
74
|
log = read_json output
|
76
75
|
|
77
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]
|
78
79
|
assert_equal 'get', log[:request][:method]
|
79
80
|
assert_equal 200, log[:response][:status_code]
|
80
81
|
end
|
@@ -82,15 +83,15 @@ describe Twiglet::Logger do
|
|
82
83
|
it "should be able to add properties with '.with'" do
|
83
84
|
# Let's add some context to this customer journey
|
84
85
|
purchase_logger = @logger.with({
|
85
|
-
trace: {
|
86
|
-
customer: {
|
87
|
-
event: {
|
86
|
+
trace: {id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'},
|
87
|
+
customer: {full_name: 'Freda Bloggs'},
|
88
|
+
event: {action: 'pet purchase'}
|
88
89
|
})
|
89
90
|
|
90
91
|
# do stuff
|
91
92
|
purchase_logger.info({
|
92
93
|
message: 'customer bought a dog',
|
93
|
-
pet: {
|
94
|
+
pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
|
94
95
|
})
|
95
96
|
|
96
97
|
log = read_json @buffer
|
@@ -132,7 +133,7 @@ describe Twiglet::Logger do
|
|
132
133
|
@logger.debug({
|
133
134
|
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
134
135
|
message: 'customer bought a dog',
|
135
|
-
pet: {
|
136
|
+
pet: {name: 'Barker', breed: 'Bitsa'},
|
136
137
|
"pet.species": 'dog'
|
137
138
|
})
|
138
139
|
log = read_json(@buffer)
|
@@ -171,7 +172,7 @@ describe Twiglet::Logger do
|
|
171
172
|
begin
|
172
173
|
1 / 0
|
173
174
|
rescue StandardError => e
|
174
|
-
@logger.error({
|
175
|
+
@logger.error({message: 'Artificially raised exception'}, e)
|
175
176
|
end
|
176
177
|
|
177
178
|
actual_log = read_json(@buffer)
|
data/twiglet.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |gem|
|
|
9
9
|
gem.version = Twiglet::VERSION
|
10
10
|
gem.authors = ['Simply Business']
|
11
11
|
gem.email = ['tech@simplybusiness.co.uk']
|
12
|
-
gem.homepage = 'https://github.com/simplybusiness/twiglet'
|
12
|
+
gem.homepage = 'https://github.com/simplybusiness/twiglet-ruby'
|
13
13
|
|
14
14
|
gem.summary = 'Twiglet'
|
15
15
|
gem.description = 'Like a log, only smaller.'
|
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: 2.0.0.pre.alpha.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-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Like a log, only smaller.
|
14
14
|
email:
|
@@ -21,19 +21,22 @@ files:
|
|
21
21
|
- ".github/workflows/ruby.yml"
|
22
22
|
- ".gitignore"
|
23
23
|
- ".rubocop.yml"
|
24
|
+
- ".ruby-version"
|
24
25
|
- CODE_OF_CONDUCT.md
|
25
26
|
- Gemfile
|
26
27
|
- Gemfile.lock
|
27
28
|
- LICENSE
|
29
|
+
- RATIONALE.md
|
28
30
|
- README.md
|
31
|
+
- Rakefile
|
29
32
|
- example_app.rb
|
30
|
-
- lib/
|
33
|
+
- lib/hash_extensions.rb
|
31
34
|
- lib/twiglet/logger.rb
|
32
35
|
- lib/twiglet/version.rb
|
33
|
-
- test/
|
36
|
+
- test/hash_extensions_test.rb
|
34
37
|
- test/logger_test.rb
|
35
38
|
- twiglet.gemspec
|
36
|
-
homepage: https://github.com/simplybusiness/twiglet
|
39
|
+
homepage: https://github.com/simplybusiness/twiglet-ruby
|
37
40
|
licenses:
|
38
41
|
- Copyright SimplyBusiness
|
39
42
|
metadata: {}
|
@@ -48,11 +51,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
48
51
|
version: '2.6'
|
49
52
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
53
|
requirements:
|
51
|
-
- - "
|
54
|
+
- - ">"
|
52
55
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
56
|
+
version: 1.3.1
|
54
57
|
requirements: []
|
55
|
-
rubygems_version: 3.
|
58
|
+
rubygems_version: 3.1.2
|
56
59
|
signing_key:
|
57
60
|
specification_version: 4
|
58
61
|
summary: Twiglet
|
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ElasticCommonSchema
|
4
|
-
def to_nested(log)
|
5
|
-
log unless contains_dotted_key?(log)
|
6
|
-
|
7
|
-
log.keys.reduce({}) do |nested, key|
|
8
|
-
deep_merge(nested, build_nested_object(key, log[key]))
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
def deep_merge(hash1, hash2)
|
13
|
-
merger = proc { |_, val1, val2| val1.is_a?(Hash) && val2.is_a?(Hash) ? val1.merge(val2, &merger) : val2 }
|
14
|
-
hash1.merge(hash2, &merger)
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def contains_dotted_key?(log)
|
20
|
-
log.keys.any? { |x| x.to_s.include?('.') }
|
21
|
-
end
|
22
|
-
|
23
|
-
def build_nested_object(key, val)
|
24
|
-
key.to_s
|
25
|
-
.split('.')
|
26
|
-
.reverse
|
27
|
-
.reduce(val) { |nested, key_part| Hash[key_part.to_sym, nested] }
|
28
|
-
end
|
29
|
-
end
|