twiglet 1.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +0 -1
- data/.github/workflows/ruby.yml +1 -1
- data/.rubocop.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 +44 -8
- data/Rakefile +7 -0
- data/example_app.rb +5 -2
- data/lib/hash_extensions.rb +29 -0
- data/lib/twiglet/formatter.rb +68 -0
- data/lib/twiglet/logger.rb +36 -58
- data/lib/twiglet/version.rb +1 -1
- data/test/formatter_test.rb +31 -0
- data/test/{elastic_common_schema_test.rb → hash_extensions_test.rb} +13 -14
- data/test/logger_test.rb +253 -143
- data/twiglet.gemspec +1 -1
- metadata +11 -6
- 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: 356d8c7fec6c41823029cc36bef1cc49bacae6157342b251e9b57f49dcb8151f
|
4
|
+
data.tar.gz: bd58a67b48daa9bf6108dc78098f46ad0204f98ca600c29bacd713f6cb54ea2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4bc443482b93b1bba853fbe67605d9b0673830726cf1ef89e941860bc7f832d1efb5f82d5890197ae279b9edf97bf19edf8dc2d8bf61d56d082d6e641f94a245
|
7
|
+
data.tar.gz: 8d5e178fca01957147dca807aab4117958b18514d568440e72f53cfc8931aad62035a71633646776368f4409d9be48ab8598c66aad4347edd59f79a0bfc281ae
|
data/.github/CODEOWNERS
CHANGED
data/.github/workflows/ruby.yml
CHANGED
data/.rubocop.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,11 +35,29 @@ 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.
|
36
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
|
+
|
37
61
|
Add log event specific information simply as attributes in a hash:
|
38
62
|
|
39
63
|
```ruby
|
@@ -52,9 +76,21 @@ logger.info({
|
|
52
76
|
This writes:
|
53
77
|
|
54
78
|
```json
|
55
|
-
{"service":{"name":"
|
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"}}
|
80
|
+
```
|
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"}}
|
56
91
|
```
|
57
92
|
|
93
|
+
|
58
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:
|
59
95
|
|
60
96
|
```ruby
|
@@ -76,7 +112,7 @@ request_logger.error({
|
|
76
112
|
which will print:
|
77
113
|
|
78
114
|
```json
|
79
|
-
{"service":{"name":"
|
115
|
+
{"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
116
|
```
|
81
117
|
|
82
118
|
## Use of dotted keys
|
@@ -110,7 +146,7 @@ logger.info({
|
|
110
146
|
Both cases would print out exact the same log item:
|
111
147
|
|
112
148
|
```json
|
113
|
-
{"service":{"name":"
|
149
|
+
{"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
150
|
```
|
115
151
|
|
116
152
|
## How to contribute
|
@@ -120,7 +156,7 @@ First: Please read our project [Code of Conduct](../CODE_OF_CONDUCT.md).
|
|
120
156
|
Second: run the tests and make sure your changes don't break anything:
|
121
157
|
|
122
158
|
```bash
|
123
|
-
|
159
|
+
bundle exec rake test
|
124
160
|
```
|
125
161
|
|
126
162
|
Then please feel free to submit a PR.
|
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({
|
@@ -17,6 +17,9 @@ logger.info({
|
|
17
17
|
}
|
18
18
|
})
|
19
19
|
|
20
|
+
# Use text logging
|
21
|
+
logger.info("Ready to go, listening on port #{PORT}")
|
22
|
+
#
|
20
23
|
# We get a request
|
21
24
|
request_logger = logger.with({
|
22
25
|
event: {
|
@@ -45,7 +48,7 @@ request_logger.info({
|
|
45
48
|
}
|
46
49
|
})
|
47
50
|
|
48
|
-
# Logging with
|
51
|
+
# Logging with an empty message is an anti-pattern and is therefore forbidden
|
49
52
|
# Both of the following lines would throw an error
|
50
53
|
# request_logger.error({ message: "" })
|
51
54
|
# 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
|
@@ -0,0 +1,68 @@
|
|
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
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/twiglet/logger.rb
CHANGED
@@ -1,82 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'logger'
|
3
4
|
require 'time'
|
4
5
|
require 'json'
|
5
|
-
|
6
|
+
require 'twiglet/formatter'
|
7
|
+
require_relative '../hash_extensions'
|
6
8
|
|
7
9
|
module Twiglet
|
8
|
-
class Logger
|
9
|
-
include
|
10
|
+
class Logger < ::Logger
|
11
|
+
Hash.include HashExtensions
|
10
12
|
|
11
|
-
def initialize(
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
def initialize(
|
14
|
+
service_name,
|
15
|
+
default_properties: {},
|
16
|
+
now: -> { Time.now.utc },
|
17
|
+
output: $stdout
|
18
|
+
)
|
19
|
+
@service_name = service_name
|
20
|
+
@now = now
|
21
|
+
@output = output
|
15
22
|
|
16
|
-
raise '
|
17
|
-
unless
|
23
|
+
raise 'Service name is mandatory' \
|
24
|
+
unless service_name.is_a?(String) && !service_name.strip.empty?
|
18
25
|
|
19
|
-
|
26
|
+
formatter = Twiglet::Formatter.new(service_name, default_properties: default_properties, now: now)
|
27
|
+
super(output, formatter: formatter)
|
20
28
|
end
|
21
29
|
|
22
|
-
def
|
23
|
-
log(level: 'debug', message: message)
|
24
|
-
end
|
25
|
-
|
26
|
-
def info(message)
|
27
|
-
log(level: 'info', message: message)
|
28
|
-
end
|
29
|
-
|
30
|
-
def warning(message)
|
31
|
-
log(level: 'warning', message: message)
|
32
|
-
end
|
33
|
-
|
34
|
-
def error(message, error = nil)
|
30
|
+
def error(message = {}, error = nil, &block)
|
35
31
|
if error
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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)
|
40
39
|
end
|
41
40
|
|
42
|
-
|
41
|
+
super(message, &block)
|
43
42
|
end
|
44
43
|
|
45
|
-
def
|
46
|
-
|
44
|
+
def with(default_properties)
|
45
|
+
Logger.new(@service_name,
|
46
|
+
default_properties: default_properties,
|
47
|
+
now: @now,
|
48
|
+
output: @output)
|
47
49
|
end
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
-
now: @now,
|
52
|
-
output: @output },
|
53
|
-
scoped_properties: scoped_properties)
|
54
|
-
end
|
51
|
+
alias_method :warning, :warn
|
52
|
+
alias_method :critical, :fatal
|
55
53
|
|
56
54
|
private
|
57
55
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
61
|
-
message = message.transform_keys(&:to_sym)
|
62
|
-
message.key?(:message) || raise('Log object must have a \'message\' property')
|
63
|
-
|
64
|
-
message[:message].strip.empty? && raise('The \'message\' property of log object must not be empty')
|
65
|
-
|
66
|
-
total_message = {
|
67
|
-
service: {
|
68
|
-
name: @service
|
69
|
-
},
|
70
|
-
"@timestamp": @now.call.iso8601(3),
|
71
|
-
log: {
|
72
|
-
level: level
|
73
|
-
}
|
74
|
-
}
|
75
|
-
total_message = total_message.merge(@scoped_properties)
|
76
|
-
.merge!(message)
|
77
|
-
.then { |log_entry| to_nested(log_entry) }
|
78
|
-
|
79
|
-
@output.puts total_message.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
|
80
58
|
end
|
81
59
|
end
|
82
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
|
@@ -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,178 +7,288 @@ 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
|
|
15
|
+
LEVELS = [
|
16
|
+
{ method: :debug, level: 'debug' },
|
17
|
+
{ method: :info, level: 'info' },
|
18
|
+
{ method: :warning, level: 'warn' },
|
19
|
+
{ method: :warn, level: 'warn' },
|
20
|
+
{ method: :critical, level: 'fatal' },
|
21
|
+
{ method: :fatal, level: 'fatal' },
|
22
|
+
{ method: :error, level: 'error' }
|
23
|
+
].freeze
|
24
|
+
|
17
25
|
it 'should throw an error with an empty service name' do
|
18
26
|
assert_raises RuntimeError do
|
19
|
-
Twiglet::Logger.new(
|
27
|
+
Twiglet::Logger.new(' ')
|
20
28
|
end
|
21
29
|
end
|
22
30
|
|
23
|
-
it '
|
24
|
-
|
25
|
-
|
31
|
+
it 'conforms to the standard Ruby Logger API' do
|
32
|
+
[:debug, :debug?, :info, :info?, :warn, :warn?, :fatal, :fatal?, :error, :error?,
|
33
|
+
:level, :level=, :sev_threshold=].each do |call|
|
34
|
+
assert @logger.respond_to?(call), "Logger does not respond to #{call}"
|
26
35
|
end
|
27
36
|
end
|
28
37
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
describe 'JSON logging' do
|
39
|
+
it 'should throw an error with an empty message' do
|
40
|
+
assert_raises RuntimeError do
|
41
|
+
@logger.info({message: ''})
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should log mandatory attributes' do
|
46
|
+
@logger.error({message: 'Out of pets exception'})
|
47
|
+
actual_log = read_json(@buffer)
|
48
|
+
|
49
|
+
expected_log = {
|
50
|
+
message: 'Out of pets exception',
|
51
|
+
"@timestamp": '2020-05-11T15:01:01.000Z',
|
52
|
+
service: {
|
53
|
+
name: 'petshop'
|
54
|
+
},
|
55
|
+
log: {
|
56
|
+
level: 'error'
|
57
|
+
}
|
41
58
|
}
|
42
|
-
}
|
43
59
|
|
44
|
-
|
45
|
-
|
60
|
+
assert_equal expected_log, actual_log
|
61
|
+
end
|
46
62
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
63
|
+
it 'should log the provided message' do
|
64
|
+
@logger.error({event:
|
65
|
+
{action: 'exception'},
|
66
|
+
message: 'Emergency! Emergency!'})
|
67
|
+
log = read_json(@buffer)
|
52
68
|
|
53
|
-
|
54
|
-
|
55
|
-
|
69
|
+
assert_equal 'exception', log[:event][:action]
|
70
|
+
assert_equal 'Emergency! Emergency!', log[:message]
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should log scoped properties defined at creation' do
|
74
|
+
extra_properties = {
|
75
|
+
trace: {
|
76
|
+
id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
77
|
+
},
|
78
|
+
service: {
|
79
|
+
type: 'shop'
|
80
|
+
},
|
81
|
+
request: {method: 'get'},
|
82
|
+
response: {status_code: 200}
|
83
|
+
}
|
56
84
|
|
57
|
-
|
58
|
-
|
59
|
-
trace: {
|
60
|
-
id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
61
|
-
},
|
62
|
-
request: { method: 'get' },
|
63
|
-
response: { status_code: 200 }
|
64
|
-
}
|
65
|
-
|
66
|
-
output = StringIO.new
|
67
|
-
logger = Twiglet::Logger.new(conf: {
|
68
|
-
service: 'petshop',
|
85
|
+
output = StringIO.new
|
86
|
+
logger = Twiglet::Logger.new('petshop',
|
69
87
|
now: @now,
|
70
|
-
output: output
|
71
|
-
|
72
|
-
scoped_properties: extra_properties)
|
88
|
+
output: output,
|
89
|
+
default_properties: extra_properties)
|
73
90
|
|
74
|
-
|
75
|
-
|
91
|
+
logger.error({message: 'GET /cats'})
|
92
|
+
log = read_json output
|
76
93
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
94
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
95
|
+
assert_equal 'petshop', log[:service][:name]
|
96
|
+
assert_equal 'shop', log[:service][:type]
|
97
|
+
assert_equal 'get', log[:request][:method]
|
98
|
+
assert_equal 200, log[:response][:status_code]
|
99
|
+
end
|
81
100
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
# do stuff
|
91
|
-
purchase_logger.info({
|
92
|
-
message: 'customer bought a dog',
|
93
|
-
pet: { name: 'Barker', species: 'dog', breed: 'Bitsa' }
|
94
|
-
})
|
95
|
-
|
96
|
-
log = read_json @buffer
|
97
|
-
|
98
|
-
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
99
|
-
assert_equal 'Freda Bloggs', log[:customer][:full_name]
|
100
|
-
assert_equal 'pet purchase', log[:event][:action]
|
101
|
-
assert_equal 'customer bought a dog', log[:message]
|
102
|
-
assert_equal 'Barker', log[:pet][:name]
|
103
|
-
end
|
101
|
+
it "should be able to add properties with '.with'" do
|
102
|
+
# Let's add some context to this customer journey
|
103
|
+
purchase_logger = @logger.with({
|
104
|
+
trace: {id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'},
|
105
|
+
customer: {full_name: 'Freda Bloggs'},
|
106
|
+
event: {action: 'pet purchase'}
|
107
|
+
})
|
104
108
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
109
|
+
# do stuff
|
110
|
+
purchase_logger.info({
|
111
|
+
message: 'customer bought a dog',
|
112
|
+
pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
|
113
|
+
})
|
110
114
|
|
111
|
-
|
112
|
-
end
|
115
|
+
log = read_json @buffer
|
113
116
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
"pet.breed": 'Bitsa'
|
121
|
-
})
|
122
|
-
log = read_json(@buffer)
|
123
|
-
|
124
|
-
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
125
|
-
assert_equal 'customer bought a dog', log[:message]
|
126
|
-
assert_equal 'Barker', log[:pet][:name]
|
127
|
-
assert_equal 'dog', log[:pet][:species]
|
128
|
-
assert_equal 'Bitsa', log[:pet][:breed]
|
129
|
-
end
|
117
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
118
|
+
assert_equal 'Freda Bloggs', log[:customer][:full_name]
|
119
|
+
assert_equal 'pet purchase', log[:event][:action]
|
120
|
+
assert_equal 'customer bought a dog', log[:message]
|
121
|
+
assert_equal 'Barker', log[:pet][:name]
|
122
|
+
end
|
130
123
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
124
|
+
it "should log 'message' string property" do
|
125
|
+
message = {}
|
126
|
+
message['message'] = 'Guinea pigs arrived'
|
127
|
+
@logger.debug(message)
|
128
|
+
log = read_json(@buffer)
|
129
|
+
|
130
|
+
assert_equal 'Guinea pigs arrived', log[:message]
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should be able to convert dotted keys to nested objects' do
|
134
|
+
@logger.debug({
|
135
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
136
|
+
message: 'customer bought a dog',
|
137
|
+
"pet.name": 'Barker',
|
138
|
+
"pet.species": 'dog',
|
139
|
+
"pet.breed": 'Bitsa'
|
140
|
+
})
|
141
|
+
log = read_json(@buffer)
|
142
|
+
|
143
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
144
|
+
assert_equal 'customer bought a dog', log[:message]
|
145
|
+
assert_equal 'Barker', log[:pet][:name]
|
146
|
+
assert_equal 'dog', log[:pet][:species]
|
147
|
+
assert_equal 'Bitsa', log[:pet][:breed]
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should be able to mix dotted keys and nested objects' do
|
151
|
+
@logger.debug({
|
152
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
153
|
+
message: 'customer bought a dog',
|
154
|
+
pet: {name: 'Barker', breed: 'Bitsa'},
|
155
|
+
"pet.species": 'dog'
|
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 work with mixed string and symbol properties' do
|
167
|
+
log = {
|
168
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
169
|
+
}
|
170
|
+
event = {}
|
171
|
+
log['event'] = event
|
172
|
+
log['message'] = 'customer bought a dog'
|
173
|
+
pet = {}
|
174
|
+
pet['name'] = 'Barker'
|
175
|
+
pet['breed'] = 'Bitsa'
|
176
|
+
pet[:species] = 'dog'
|
177
|
+
log[:pet] = pet
|
178
|
+
|
179
|
+
@logger.debug(log)
|
180
|
+
actual_log = read_json(@buffer)
|
146
181
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
182
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', actual_log[:trace][:id]
|
183
|
+
assert_equal 'customer bought a dog', actual_log[:message]
|
184
|
+
assert_equal 'Barker', actual_log[:pet][:name]
|
185
|
+
assert_equal 'dog', actual_log[:pet][:species]
|
186
|
+
assert_equal 'Bitsa', actual_log[:pet][:breed]
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'should log an error with backtrace' do
|
190
|
+
begin
|
191
|
+
1 / 0
|
192
|
+
rescue StandardError => e
|
193
|
+
@logger.error({message: 'Artificially raised exception'}, e)
|
194
|
+
end
|
195
|
+
|
196
|
+
actual_log = read_json(@buffer)
|
197
|
+
|
198
|
+
assert_equal 'Artificially raised exception', actual_log[:message]
|
199
|
+
assert_equal 'divided by 0', actual_log[:error][:message]
|
200
|
+
assert_match 'logger_test.rb', actual_log[:error][:stack_trace].lines.first
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'should log an error without backtrace' do
|
204
|
+
e = StandardError.new('Connection timed-out')
|
205
|
+
@logger.error({message: 'Artificially raised exception'}, e)
|
206
|
+
|
207
|
+
actual_log = read_json(@buffer)
|
208
|
+
|
209
|
+
assert_equal 'Artificially raised exception', actual_log[:message]
|
210
|
+
assert_equal 'Connection timed-out', actual_log[:error][:message]
|
211
|
+
refute actual_log[:error].key?(:stack_trace)
|
212
|
+
end
|
213
|
+
|
214
|
+
LEVELS.each do |attrs|
|
215
|
+
it "should correctly log level when calling #{attrs[:method]}" do
|
216
|
+
@logger.public_send(attrs[:method], {message: 'a log message'})
|
217
|
+
actual_log = read_json(@buffer)
|
218
|
+
|
219
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
220
|
+
assert_equal 'a log message', actual_log[:message]
|
221
|
+
end
|
222
|
+
end
|
168
223
|
end
|
169
224
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
225
|
+
describe 'text logging' do
|
226
|
+
it 'should throw an error with an empty message' do
|
227
|
+
assert_raises RuntimeError do
|
228
|
+
@logger.info('')
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'should log mandatory attributes' do
|
233
|
+
@logger.error('Out of pets exception')
|
234
|
+
actual_log = read_json(@buffer)
|
235
|
+
|
236
|
+
expected_log = {
|
237
|
+
message: 'Out of pets exception',
|
238
|
+
"@timestamp": '2020-05-11T15:01:01.000Z',
|
239
|
+
service: {
|
240
|
+
name: 'petshop'
|
241
|
+
},
|
242
|
+
log: {
|
243
|
+
level: 'error'
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
assert_equal expected_log, actual_log
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'should log the provided message' do
|
251
|
+
@logger.error('Emergency! Emergency!')
|
252
|
+
log = read_json(@buffer)
|
253
|
+
|
254
|
+
assert_equal 'Emergency! Emergency!', log[:message]
|
255
|
+
end
|
256
|
+
|
257
|
+
LEVELS.each do |attrs|
|
258
|
+
it "should correctly log level when calling #{attrs[:method]}" do
|
259
|
+
@logger.public_send(attrs[:method], 'a log message')
|
260
|
+
actual_log = read_json(@buffer)
|
261
|
+
|
262
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
263
|
+
assert_equal 'a log message', actual_log[:message]
|
264
|
+
end
|
175
265
|
end
|
266
|
+
end
|
176
267
|
|
177
|
-
|
268
|
+
describe 'logging with a block' do
|
269
|
+
LEVELS.each do |attrs|
|
270
|
+
it "should correctly log the block when calling #{attrs[:method]}" do
|
271
|
+
block = proc { 'a block log message' }
|
272
|
+
@logger.public_send(attrs[:method], &block)
|
273
|
+
actual_log = read_json(@buffer)
|
178
274
|
|
179
|
-
|
180
|
-
|
181
|
-
|
275
|
+
assert_equal attrs[:level], actual_log[:log][:level]
|
276
|
+
assert_equal 'a block log message', actual_log[:message]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
describe 'logger level' do
|
282
|
+
[
|
283
|
+
{ expression: :info, level: 1 },
|
284
|
+
{ expression: 'warn', level: 2 },
|
285
|
+
{ expression: Logger::DEBUG, level: 0 }
|
286
|
+
].each do |args|
|
287
|
+
it "sets the severity threshold to level #{args[:level]}" do
|
288
|
+
@logger.level = args[:expression]
|
289
|
+
assert_equal args[:level], @logger.level
|
290
|
+
end
|
291
|
+
end
|
182
292
|
end
|
183
293
|
|
184
294
|
private
|
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.2.0
|
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-22 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Like a log, only smaller.
|
14
14
|
email:
|
@@ -21,19 +21,24 @@ 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
|
34
|
+
- lib/twiglet/formatter.rb
|
31
35
|
- lib/twiglet/logger.rb
|
32
36
|
- lib/twiglet/version.rb
|
33
|
-
- test/
|
37
|
+
- test/formatter_test.rb
|
38
|
+
- test/hash_extensions_test.rb
|
34
39
|
- test/logger_test.rb
|
35
40
|
- twiglet.gemspec
|
36
|
-
homepage: https://github.com/simplybusiness/twiglet
|
41
|
+
homepage: https://github.com/simplybusiness/twiglet-ruby
|
37
42
|
licenses:
|
38
43
|
- Copyright SimplyBusiness
|
39
44
|
metadata: {}
|
@@ -52,7 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
57
|
- !ruby/object:Gem::Version
|
53
58
|
version: '0'
|
54
59
|
requirements: []
|
55
|
-
rubygems_version: 3.0.
|
60
|
+
rubygems_version: 3.0.8
|
56
61
|
signing_key:
|
57
62
|
specification_version: 4
|
58
63
|
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
|