twiglet 1.1.0
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 +7 -0
- data/.github/CODEOWNERS +4 -0
- data/.github/workflows/ruby.yml +32 -0
- data/.gitignore +56 -0
- data/.rubocop.yml +15 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +60 -0
- data/LICENSE +21 -0
- data/README.md +126 -0
- data/example_app.rb +51 -0
- data/lib/elastic_common_schema.rb +29 -0
- data/lib/twiglet/logger.rb +82 -0
- data/lib/twiglet/version.rb +5 -0
- data/test/elastic_common_schema_test.rb +134 -0
- data/test/logger_test.rb +190 -0
- data/twiglet.gemspec +24 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8780baf991dc54df9731f5cb2f02cbf1472c4f026d4b5a0604fe332b96f84384
|
|
4
|
+
data.tar.gz: f059d2eafbb0ac5b5ba74c0f430508ca3966326ec832fb4b56df3efc3b436faf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1d133f5b631a3a192f1dd7d7f54fecb90c481d593a747ee1ce2b9772eb91c6209af78efb0ec1ebfebe06dabbc76946029edc6a70e6c6493f2351be933ffae7e4
|
|
7
|
+
data.tar.gz: 343cb5284055f7c5172f7451bbdeb73d1fd9f6c9f53c7341a44d997a63ffbc2f90aecece369e63e27b5955d45aa570f787d83e0c5d83cc52267e7329f9073c02
|
data/.github/CODEOWNERS
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Ruby CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- '*' # matches every branch
|
|
7
|
+
- '*/*' # matches every branch containing a single '/'
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
defaults:
|
|
13
|
+
run:
|
|
14
|
+
working-directory: ./
|
|
15
|
+
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
ruby-version: [2.6, 2.7]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v2
|
|
22
|
+
- name: Set up Ruby
|
|
23
|
+
uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: bundle install
|
|
28
|
+
- name: Rubocop Check
|
|
29
|
+
run: bundle exec rubocop
|
|
30
|
+
- name: Run all tests
|
|
31
|
+
run: bundle exec ruby test/*
|
|
32
|
+
shell: bash
|
data/.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
/.config
|
|
4
|
+
/coverage/
|
|
5
|
+
/InstalledFiles
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/spec/examples.txt
|
|
9
|
+
/test/tmp/
|
|
10
|
+
/test/version_tmp/
|
|
11
|
+
/tmp/
|
|
12
|
+
|
|
13
|
+
# Used by dotenv library to load environment variables.
|
|
14
|
+
# .env
|
|
15
|
+
|
|
16
|
+
# Ignore Byebug command history file.
|
|
17
|
+
.byebug_history
|
|
18
|
+
|
|
19
|
+
## Specific to RubyMotion:
|
|
20
|
+
.dat*
|
|
21
|
+
.repl_history
|
|
22
|
+
build/
|
|
23
|
+
*.bridgesupport
|
|
24
|
+
build-iPhoneOS/
|
|
25
|
+
build-iPhoneSimulator/
|
|
26
|
+
|
|
27
|
+
## Specific to RubyMotion (use of CocoaPods):
|
|
28
|
+
#
|
|
29
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
|
30
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
|
31
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
|
32
|
+
#
|
|
33
|
+
# vendor/Pods/
|
|
34
|
+
|
|
35
|
+
## Documentation cache and generated files:
|
|
36
|
+
/.yardoc/
|
|
37
|
+
/_yardoc/
|
|
38
|
+
/doc/
|
|
39
|
+
/rdoc/
|
|
40
|
+
|
|
41
|
+
## Environment normalization:
|
|
42
|
+
/.bundle/
|
|
43
|
+
/vendor/bundle
|
|
44
|
+
/lib/bundler/man/
|
|
45
|
+
|
|
46
|
+
# for a library or gem, you might want to ignore these files since the code is
|
|
47
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
48
|
+
# Gemfile.lock
|
|
49
|
+
# .ruby-version
|
|
50
|
+
# .ruby-gemset
|
|
51
|
+
|
|
52
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
53
|
+
.rvmrc
|
|
54
|
+
|
|
55
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
|
56
|
+
# .rubocop-https?--*
|
data/.rubocop.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
|
9
|
+
level of experience, education, socio-economic status, nationality, personal
|
|
10
|
+
appearance, race, religion, or sexual identity and orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
* Using welcoming and inclusive language
|
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
|
19
|
+
* Gracefully accepting constructive criticism
|
|
20
|
+
* Focusing on what is best for the community
|
|
21
|
+
* Showing empathy towards other community members
|
|
22
|
+
|
|
23
|
+
Examples of unacceptable behavior by participants include:
|
|
24
|
+
|
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
+
advances
|
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
+
* Public or private harassment
|
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
|
30
|
+
address, without explicit permission
|
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Our Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
+
response to any instances of unacceptable behavior.
|
|
39
|
+
|
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
+
threatening, offensive, or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
+
when an individual is representing the project or its community. Examples of
|
|
50
|
+
representing a project or community include using an official project e-mail
|
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
|
53
|
+
further defined and clarified by project maintainers.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
+
reported by contacting the project team at tech@simplybusiness.co.uk. All
|
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
|
63
|
+
|
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
+
members of the project's leadership.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
71
|
+
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
|
72
|
+
|
|
73
|
+
[homepage]: https://www.contributor-covenant.org
|
|
74
|
+
|
|
75
|
+
For answers to common questions about this code of conduct, see
|
|
76
|
+
https://www.contributor-covenant.org/faq
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
GIT
|
|
2
|
+
remote: https://github.com/simplybusiness/simplycop.git
|
|
3
|
+
revision: 02b417277e3ff9eeab34d6b7c30a95a17d876731
|
|
4
|
+
specs:
|
|
5
|
+
simplycop (0.5.4)
|
|
6
|
+
rubocop (~> 0.80.0)
|
|
7
|
+
rubocop-rails
|
|
8
|
+
rubocop-rspec
|
|
9
|
+
|
|
10
|
+
GEM
|
|
11
|
+
remote: https://rubygems.org/
|
|
12
|
+
specs:
|
|
13
|
+
activesupport (6.0.3.1)
|
|
14
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
15
|
+
i18n (>= 0.7, < 2)
|
|
16
|
+
minitest (~> 5.1)
|
|
17
|
+
tzinfo (~> 1.1)
|
|
18
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
|
19
|
+
ast (2.4.0)
|
|
20
|
+
concurrent-ruby (1.1.6)
|
|
21
|
+
i18n (1.8.2)
|
|
22
|
+
concurrent-ruby (~> 1.0)
|
|
23
|
+
jaro_winkler (1.5.4)
|
|
24
|
+
minitest (5.14.0)
|
|
25
|
+
parallel (1.19.1)
|
|
26
|
+
parser (2.7.1.3)
|
|
27
|
+
ast (~> 2.4.0)
|
|
28
|
+
rack (2.2.2)
|
|
29
|
+
rainbow (3.0.0)
|
|
30
|
+
rexml (3.2.4)
|
|
31
|
+
rubocop (0.80.1)
|
|
32
|
+
jaro_winkler (~> 1.5.1)
|
|
33
|
+
parallel (~> 1.10)
|
|
34
|
+
parser (>= 2.7.0.1)
|
|
35
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
36
|
+
rexml
|
|
37
|
+
ruby-progressbar (~> 1.7)
|
|
38
|
+
unicode-display_width (>= 1.4.0, < 1.7)
|
|
39
|
+
rubocop-rails (2.5.2)
|
|
40
|
+
activesupport
|
|
41
|
+
rack (>= 1.1)
|
|
42
|
+
rubocop (>= 0.72.0)
|
|
43
|
+
rubocop-rspec (1.39.0)
|
|
44
|
+
rubocop (>= 0.68.1)
|
|
45
|
+
ruby-progressbar (1.10.1)
|
|
46
|
+
thread_safe (0.3.6)
|
|
47
|
+
tzinfo (1.2.7)
|
|
48
|
+
thread_safe (~> 0.1)
|
|
49
|
+
unicode-display_width (1.6.1)
|
|
50
|
+
zeitwerk (2.3.0)
|
|
51
|
+
|
|
52
|
+
PLATFORMS
|
|
53
|
+
ruby
|
|
54
|
+
|
|
55
|
+
DEPENDENCIES
|
|
56
|
+
minitest
|
|
57
|
+
simplycop!
|
|
58
|
+
|
|
59
|
+
BUNDLED WITH
|
|
60
|
+
2.1.4
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Simply Business
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Twiglet: Ruby version
|
|
2
|
+
Like a log, only smaller.
|
|
3
|
+
|
|
4
|
+
This library provides a minimal JSON logging interface suitable for use in (micro)services. See the [README](../README.md) for design rationale and an explantion of the Elastic Common Schema that we are using for log attribute naming.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
gem install twiglet
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## How to use
|
|
13
|
+
|
|
14
|
+
Create a new logger like so:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require 'twiglet/logger'
|
|
18
|
+
logger = Twiglet::Logger.new(conf: { service: 'petshop' })
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The logger may be passed in the configuration object an optional `output` attribute which should be an object with a `puts` method - like `$stdout`. The configuration object may also have an optional `now` attribute, which should be a function returning a `Time` object. The defaults should serve for most uses, though you may want to override them for testing as we have done [here](test/logger_test.rb).
|
|
22
|
+
|
|
23
|
+
To use, simply invoke like most other loggers:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
logger.error({ event: { action: 'startup' }, message: "Emergency! There's an Emergency going on" })
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This will write to STDOUT a JSON string:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{"service":{"name":"petshop"},"@timestamp":"2020-05-14T10:54:59.164+01:00","log":{"level":"error"},"event":{"action":"startup"},"message":"Emergency! There's an Emergency going on"}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Obviously the timestamp will be different.
|
|
36
|
+
|
|
37
|
+
Add log event specific information simply as attributes in a hash:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
logger.info({
|
|
41
|
+
event: { action: 'HTTP request' },
|
|
42
|
+
message: 'GET /pets success',
|
|
43
|
+
trace: { id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb' },
|
|
44
|
+
http: {
|
|
45
|
+
request: { method: 'get' },
|
|
46
|
+
response: { status_code: 200 }
|
|
47
|
+
},
|
|
48
|
+
url: { path: '/pets' }
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This writes:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{"service":{"name":"petshop"},"@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
|
+
```
|
|
57
|
+
|
|
58
|
+
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
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
request_log = logger.with({ event: { action: 'HTTP request'}, trace: { id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb' }})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This can be used like any other Logger instance:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
request_logger.error({
|
|
68
|
+
message: 'Error 500 in /pets/buy',
|
|
69
|
+
http: {
|
|
70
|
+
request: { method: 'post', 'url.path': '/pet/buy' },
|
|
71
|
+
response: { status_code: 500 }
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
which will print:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{"service":{"name":"petshop"},"@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
|
+
```
|
|
81
|
+
|
|
82
|
+
## Use of dotted keys
|
|
83
|
+
|
|
84
|
+
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:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
logger.info({
|
|
88
|
+
'event.action': 'HTTP request',
|
|
89
|
+
message: 'GET /pets success',
|
|
90
|
+
'trace.id': '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
|
91
|
+
'http.request.method': 'get',
|
|
92
|
+
'http.response.status_code': 200,
|
|
93
|
+
'url.path': '/pets'
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
or mix between dotted keys and nested objects:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
logger.info({
|
|
101
|
+
'event.action': 'HTTP request',
|
|
102
|
+
message: 'GET /pets success',
|
|
103
|
+
trace: { id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb' },
|
|
104
|
+
'http.request.method': 'get',
|
|
105
|
+
'http.response.status_code': 200,
|
|
106
|
+
url: { path: '/pets' }
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Both cases would print out exact the same log item:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{"service":{"name":"petshop"},"@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
|
+
```
|
|
115
|
+
|
|
116
|
+
## How to contribute
|
|
117
|
+
|
|
118
|
+
First: Please read our project [Code of Conduct](../CODE_OF_CONDUCT.md).
|
|
119
|
+
|
|
120
|
+
Second: run the tests and make sure your changes don't break anything:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
for file in test/*test.rb; do ruby $file; done
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Then please feel free to submit a PR.
|
data/example_app.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/twiglet/logger'
|
|
4
|
+
|
|
5
|
+
PORT = 8080
|
|
6
|
+
|
|
7
|
+
logger = Logger.new(conf: { service: 'petshop' })
|
|
8
|
+
|
|
9
|
+
# Start our petshop
|
|
10
|
+
logger.info({
|
|
11
|
+
event: {
|
|
12
|
+
action: 'startup'
|
|
13
|
+
},
|
|
14
|
+
message: "Ready to go, listening on port #{PORT}",
|
|
15
|
+
server: {
|
|
16
|
+
port: PORT
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
# We get a request
|
|
21
|
+
request_logger = logger.with({
|
|
22
|
+
event: {
|
|
23
|
+
action: 'HTTP request'
|
|
24
|
+
},
|
|
25
|
+
trace: {
|
|
26
|
+
id: '126bb6fa-28a2-470f-b013-eefbf9182b2d'
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
# Oh noes!
|
|
31
|
+
db_err = StandardError.new('Connection timed-out')
|
|
32
|
+
|
|
33
|
+
request_logger.error({ message: 'DB connection failed.' }, db_err) if db_err
|
|
34
|
+
|
|
35
|
+
# We return an error to the requester
|
|
36
|
+
request_logger.info({
|
|
37
|
+
message: 'Internal Server Error',
|
|
38
|
+
http: {
|
|
39
|
+
request: {
|
|
40
|
+
method: 'get'
|
|
41
|
+
},
|
|
42
|
+
response: {
|
|
43
|
+
status_code: 500
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
# Logging with a non-empty message is an anti-pattern and is therefore forbidden
|
|
49
|
+
# Both of the following lines would throw an error
|
|
50
|
+
# request_logger.error({ message: "" })
|
|
51
|
+
# logger.debug({ message: " " })
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative '../elastic_common_schema'
|
|
6
|
+
|
|
7
|
+
module Twiglet
|
|
8
|
+
class Logger
|
|
9
|
+
include ElasticCommonSchema
|
|
10
|
+
|
|
11
|
+
def initialize(conf:, scoped_properties: {})
|
|
12
|
+
@service = conf[:service]
|
|
13
|
+
@now = conf[:now] || -> { Time.now.utc }
|
|
14
|
+
@output = conf[:output] || $stdout
|
|
15
|
+
|
|
16
|
+
raise 'configuration must have a service name' \
|
|
17
|
+
unless @service.is_a?(String) && !@service.strip.empty?
|
|
18
|
+
|
|
19
|
+
@scoped_properties = scoped_properties
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def debug(message)
|
|
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)
|
|
35
|
+
if error
|
|
36
|
+
message = message.merge({
|
|
37
|
+
error_name: error.message,
|
|
38
|
+
backtrace: error.backtrace
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
log(level: 'error', message: message)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def critical(message)
|
|
46
|
+
log(level: 'critical', message: message)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def with(scoped_properties)
|
|
50
|
+
Logger.new(conf: { service: @service,
|
|
51
|
+
now: @now,
|
|
52
|
+
output: @output },
|
|
53
|
+
scoped_properties: scoped_properties)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def log(level:, message:)
|
|
59
|
+
raise 'Message must be a Hash' unless message.is_a?(Hash)
|
|
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
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../lib/elastic_common_schema'
|
|
5
|
+
|
|
6
|
+
describe ElasticCommonSchema do
|
|
7
|
+
before do
|
|
8
|
+
@ecs = Object.new
|
|
9
|
+
@ecs.extend(ElasticCommonSchema)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'should retain an object without . in any keys' do
|
|
13
|
+
actual = {
|
|
14
|
+
message: 'Out of pets exception',
|
|
15
|
+
service: {
|
|
16
|
+
name: 'petshop'
|
|
17
|
+
},
|
|
18
|
+
log: {
|
|
19
|
+
level: 'error'
|
|
20
|
+
},
|
|
21
|
+
"@timestamp": '2020-05-09T15:13:20.736Z'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
expected = @ecs.to_nested(actual)
|
|
25
|
+
assert_equal actual, expected
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'should convert keys with . into nested objects' do
|
|
29
|
+
actual = {
|
|
30
|
+
"service.name": 'petshop',
|
|
31
|
+
"log.level": 'error'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
nested = @ecs.to_nested(actual)
|
|
35
|
+
|
|
36
|
+
assert_equal 'petshop', nested[:service][:name]
|
|
37
|
+
assert_equal 'error', nested[:log][:level]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'should group nested objects' do
|
|
41
|
+
actual = {
|
|
42
|
+
"service.name": 'petshop',
|
|
43
|
+
"service.id": 'ps001',
|
|
44
|
+
"service.version": '0.9.1',
|
|
45
|
+
"log.level": 'error'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
nested = @ecs.to_nested(actual)
|
|
49
|
+
|
|
50
|
+
assert_equal 'petshop', nested[:service][:name]
|
|
51
|
+
assert_equal 'ps001', nested[:service][:id]
|
|
52
|
+
assert_equal '0.9.1', nested[:service][:version]
|
|
53
|
+
assert_equal 'error', nested[:log][:level]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'should cope with more than two levels' do
|
|
57
|
+
actual = {
|
|
58
|
+
"http.request.method": 'get',
|
|
59
|
+
"http.request.body.bytes": 112,
|
|
60
|
+
"http.response.bytes": 1564,
|
|
61
|
+
"http.response.status_code": 200
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
nested = @ecs.to_nested(actual)
|
|
65
|
+
|
|
66
|
+
assert_equal 'get', nested[:http][:request][:method]
|
|
67
|
+
assert_equal 112, nested[:http][:request][:body][:bytes]
|
|
68
|
+
assert_equal 1564, nested[:http][:response][:bytes]
|
|
69
|
+
assert_equal 200, nested[:http][:response][:status_code]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it '#deep_merge() should work with two hashes without common keys' do
|
|
73
|
+
first = { id: 1, name: 'petshop' }
|
|
74
|
+
second = { level: 'debug', code: 5 }
|
|
75
|
+
|
|
76
|
+
actual = @ecs.deep_merge(first, second)
|
|
77
|
+
|
|
78
|
+
assert_equal 1, actual[:id]
|
|
79
|
+
assert_equal 'petshop', actual[:name]
|
|
80
|
+
assert_equal 'debug', actual[:level]
|
|
81
|
+
assert_equal 5, actual[:code]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it '#deep_merge() should use the second value for shared keys' do
|
|
85
|
+
first = { id: 1, name: 'petshop', level: 'debug' }
|
|
86
|
+
second = { name: 'petstore', level: 'error', code: 5 }
|
|
87
|
+
|
|
88
|
+
actual = @ecs.deep_merge(first, second)
|
|
89
|
+
|
|
90
|
+
assert_equal 1, actual[:id]
|
|
91
|
+
assert_equal 'petstore', actual[:name]
|
|
92
|
+
assert_equal 'error', actual[:level]
|
|
93
|
+
assert_equal 5, actual[:code]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it '#deep_merge() should merge two sub-keys' do
|
|
97
|
+
first = { service: { name: 'petshop' } }
|
|
98
|
+
second = { service: { id: 'ps001' } }
|
|
99
|
+
|
|
100
|
+
actual = @ecs.deep_merge(first, second)
|
|
101
|
+
assert_equal 'petshop', actual[:service][:name]
|
|
102
|
+
assert_equal 'ps001', actual[:service][:id]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it '#deep_merge() should merge sub-keys in more than 2 levels' do
|
|
106
|
+
first = { http: { request: { method: 'get', bytes: 124 } } }
|
|
107
|
+
second = { http: { response: { status_code: 200, bytes: 5001 } } }
|
|
108
|
+
|
|
109
|
+
actual = @ecs.deep_merge(first, second)
|
|
110
|
+
|
|
111
|
+
assert_equal 'get', actual[:http][:request][:method]
|
|
112
|
+
assert_equal 124, actual[:http][:request][:bytes]
|
|
113
|
+
assert_equal 200, actual[:http][:response][:status_code]
|
|
114
|
+
assert_equal 5001, actual[:http][:response][:bytes]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it '#deep_merge() should work when the first key is empty' do
|
|
118
|
+
first = {}
|
|
119
|
+
second = { id: 1 }
|
|
120
|
+
|
|
121
|
+
actual = @ecs.deep_merge(first, second)
|
|
122
|
+
|
|
123
|
+
assert_equal 1, actual[:id]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it '#deep_merge() should work when the second key is empty' do
|
|
127
|
+
first = { id: 1 }
|
|
128
|
+
second = {}
|
|
129
|
+
|
|
130
|
+
actual = @ecs.deep_merge(first, second)
|
|
131
|
+
|
|
132
|
+
assert_equal 1, actual[:id]
|
|
133
|
+
end
|
|
134
|
+
end
|
data/test/logger_test.rb
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require_relative '../lib/twiglet/logger'
|
|
5
|
+
|
|
6
|
+
describe Twiglet::Logger do
|
|
7
|
+
before do
|
|
8
|
+
@now = -> { Time.utc(2020, 5, 11, 15, 1, 1) }
|
|
9
|
+
@buffer = StringIO.new
|
|
10
|
+
@logger = Twiglet::Logger.new(conf: {
|
|
11
|
+
service: 'petshop',
|
|
12
|
+
now: @now,
|
|
13
|
+
output: @buffer
|
|
14
|
+
})
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'should throw an error with an empty service name' do
|
|
18
|
+
assert_raises RuntimeError do
|
|
19
|
+
Twiglet::Logger.new(conf: { service: ' ' })
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'should throw an error with an empty message' do
|
|
24
|
+
assert_raises RuntimeError do
|
|
25
|
+
@logger.info('')
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'should log mandatory attributes' do
|
|
30
|
+
@logger.error({ message: 'Out of pets exception' })
|
|
31
|
+
actual_log = read_json(@buffer)
|
|
32
|
+
|
|
33
|
+
expected_log = {
|
|
34
|
+
message: 'Out of pets exception',
|
|
35
|
+
"@timestamp": '2020-05-11T15:01:01.000Z',
|
|
36
|
+
service: {
|
|
37
|
+
name: 'petshop'
|
|
38
|
+
},
|
|
39
|
+
log: {
|
|
40
|
+
level: 'error'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
assert_equal expected_log, actual_log
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'should log the provided message' do
|
|
48
|
+
@logger.error({ event:
|
|
49
|
+
{ action: 'exception' },
|
|
50
|
+
message: 'Emergency! Emergency!' })
|
|
51
|
+
log = read_json(@buffer)
|
|
52
|
+
|
|
53
|
+
assert_equal 'exception', log[:event][:action]
|
|
54
|
+
assert_equal 'Emergency! Emergency!', log[:message]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'should log scoped properties defined at creation' do
|
|
58
|
+
extra_properties = {
|
|
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',
|
|
69
|
+
now: @now,
|
|
70
|
+
output: output
|
|
71
|
+
},
|
|
72
|
+
scoped_properties: extra_properties)
|
|
73
|
+
|
|
74
|
+
logger.error({ message: 'GET /cats' })
|
|
75
|
+
log = read_json output
|
|
76
|
+
|
|
77
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
|
78
|
+
assert_equal 'get', log[:request][:method]
|
|
79
|
+
assert_equal 200, log[:response][:status_code]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "should be able to add properties with '.with'" do
|
|
83
|
+
# Let's add some context to this customer journey
|
|
84
|
+
purchase_logger = @logger.with({
|
|
85
|
+
trace: { id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb' },
|
|
86
|
+
customer: { full_name: 'Freda Bloggs' },
|
|
87
|
+
event: { action: 'pet purchase' }
|
|
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
|
|
104
|
+
|
|
105
|
+
it "should log 'message' string property" do
|
|
106
|
+
message = {}
|
|
107
|
+
message['message'] = 'Guinea pigs arrived'
|
|
108
|
+
@logger.debug(message)
|
|
109
|
+
log = read_json(@buffer)
|
|
110
|
+
|
|
111
|
+
assert_equal 'Guinea pigs arrived', log[:message]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'should be able to convert dotted keys to nested objects' do
|
|
115
|
+
@logger.debug({
|
|
116
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
|
117
|
+
message: 'customer bought a dog',
|
|
118
|
+
"pet.name": 'Barker',
|
|
119
|
+
"pet.species": 'dog',
|
|
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
|
|
130
|
+
|
|
131
|
+
it 'should be able to mix dotted keys and nested objects' do
|
|
132
|
+
@logger.debug({
|
|
133
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
|
|
134
|
+
message: 'customer bought a dog',
|
|
135
|
+
pet: { name: 'Barker', breed: 'Bitsa' },
|
|
136
|
+
"pet.species": 'dog'
|
|
137
|
+
})
|
|
138
|
+
log = read_json(@buffer)
|
|
139
|
+
|
|
140
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
|
|
141
|
+
assert_equal 'customer bought a dog', log[:message]
|
|
142
|
+
assert_equal 'Barker', log[:pet][:name]
|
|
143
|
+
assert_equal 'dog', log[:pet][:species]
|
|
144
|
+
assert_equal 'Bitsa', log[:pet][:breed]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'should work with mixed string and symbol properties' do
|
|
148
|
+
log = {
|
|
149
|
+
"trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
|
|
150
|
+
}
|
|
151
|
+
event = {}
|
|
152
|
+
log['event'] = event
|
|
153
|
+
log['message'] = 'customer bought a dog'
|
|
154
|
+
pet = {}
|
|
155
|
+
pet['name'] = 'Barker'
|
|
156
|
+
pet['breed'] = 'Bitsa'
|
|
157
|
+
pet[:species] = 'dog'
|
|
158
|
+
log[:pet] = pet
|
|
159
|
+
|
|
160
|
+
@logger.debug(log)
|
|
161
|
+
actual_log = read_json(@buffer)
|
|
162
|
+
|
|
163
|
+
assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', actual_log[:trace][:id]
|
|
164
|
+
assert_equal 'customer bought a dog', actual_log[:message]
|
|
165
|
+
assert_equal 'Barker', actual_log[:pet][:name]
|
|
166
|
+
assert_equal 'dog', actual_log[:pet][:species]
|
|
167
|
+
assert_equal 'Bitsa', actual_log[:pet][:breed]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'should log an error with backtrace' do
|
|
171
|
+
begin
|
|
172
|
+
1 / 0
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
@logger.error({ message: 'Artificially raised exception' }, e)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
actual_log = read_json(@buffer)
|
|
178
|
+
|
|
179
|
+
assert_equal 'Artificially raised exception', actual_log[:message]
|
|
180
|
+
assert_equal 'divided by 0', actual_log[:error_name]
|
|
181
|
+
assert_match 'logger_test.rb', actual_log[:backtrace].first
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def read_json(buffer)
|
|
187
|
+
buffer.rewind
|
|
188
|
+
JSON.parse(buffer.read, symbolize_names: true)
|
|
189
|
+
end
|
|
190
|
+
end
|
data/twiglet.gemspec
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require File.expand_path('lib/twiglet/version', __dir__)
|
|
4
|
+
|
|
5
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |gem|
|
|
8
|
+
gem.name = 'twiglet'
|
|
9
|
+
gem.version = Twiglet::VERSION
|
|
10
|
+
gem.authors = ['Simply Business']
|
|
11
|
+
gem.email = ['tech@simplybusiness.co.uk']
|
|
12
|
+
gem.homepage = 'https://github.com/simplybusiness/twiglet'
|
|
13
|
+
|
|
14
|
+
gem.summary = 'Twiglet'
|
|
15
|
+
gem.description = 'Like a log, only smaller.'
|
|
16
|
+
|
|
17
|
+
gem.files = `git ls-files`.split("\n")
|
|
18
|
+
gem.test_files = `git ls-files -- {test}/*`.split("\n")
|
|
19
|
+
|
|
20
|
+
gem.require_paths = ['lib']
|
|
21
|
+
gem.required_ruby_version = '>= 2.6'
|
|
22
|
+
|
|
23
|
+
gem.license = 'Copyright SimplyBusiness'
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: twiglet
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Simply Business
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2020-06-01 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Like a log, only smaller.
|
|
14
|
+
email:
|
|
15
|
+
- tech@simplybusiness.co.uk
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- ".github/CODEOWNERS"
|
|
21
|
+
- ".github/workflows/ruby.yml"
|
|
22
|
+
- ".gitignore"
|
|
23
|
+
- ".rubocop.yml"
|
|
24
|
+
- CODE_OF_CONDUCT.md
|
|
25
|
+
- Gemfile
|
|
26
|
+
- Gemfile.lock
|
|
27
|
+
- LICENSE
|
|
28
|
+
- README.md
|
|
29
|
+
- example_app.rb
|
|
30
|
+
- lib/elastic_common_schema.rb
|
|
31
|
+
- lib/twiglet/logger.rb
|
|
32
|
+
- lib/twiglet/version.rb
|
|
33
|
+
- test/elastic_common_schema_test.rb
|
|
34
|
+
- test/logger_test.rb
|
|
35
|
+
- twiglet.gemspec
|
|
36
|
+
homepage: https://github.com/simplybusiness/twiglet
|
|
37
|
+
licenses:
|
|
38
|
+
- Copyright SimplyBusiness
|
|
39
|
+
metadata: {}
|
|
40
|
+
post_install_message:
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '2.6'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 3.0.3
|
|
56
|
+
signing_key:
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: Twiglet
|
|
59
|
+
test_files: []
|