twiglet 2.4.0 → 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9eac4966bd34b02389637e38100f843f842fc115bad74d9480dc5b87a1dcad2
4
- data.tar.gz: 3fb45d8d76a99246f4a215e42fa15dfa52b3f8f7cd8821edf8590653a114514c
3
+ metadata.gz: 501c866a8fafcb3a32d633ce10040ebecd946459a272c87fa925189fbc61caa4
4
+ data.tar.gz: 8bae82697cd8c606d82c7de19774de485ce0d87d524513de7ba50869218af4a8
5
5
  SHA512:
6
- metadata.gz: 276587b79e63ad48de407c26ad113e64c6fb2819c991622f3f6ea50abe583853991a7f32ceafa4fc2008dff2a4ff4216766c53d0910ff8538ad26430474573f5
7
- data.tar.gz: a963f5d218c7c8c327400245b122f83873201191c01828efa125418ea6e0b08d21c86a53ab791aedc9fb900a8642cb0e1130a835516388e922da6b00ccf83880
6
+ metadata.gz: a522314e822964fcda9508fa7c70456eecef93a89c51e5a43b8baa3d580de3dfc05a3872df8c81a9997825d011f058a6de38d33fe66967b7bbea1d6626c263c0
7
+ data.tar.gz: 83171f318329ac354dc0d5c823d192e1896b8d3f2cb6b6b927b13ec171174f366f89a4cb6f124752ac7bc2ca3d318e8ab8d87b87e0471ed8d911387c7ebafefe
@@ -0,0 +1,22 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ time: "07:00"
8
+ open-pull-requests-limit: 99
9
+ labels:
10
+ - dependencies
11
+ ignore:
12
+ - dependency-name: simplecov
13
+ versions:
14
+ - ">= 0.18"
15
+ - package-ecosystem: github-actions
16
+ directory: "/"
17
+ schedule:
18
+ interval: daily
19
+ time: "07:00"
20
+ open-pull-requests-limit: 99
21
+ labels:
22
+ - dependencies
@@ -16,7 +16,7 @@ jobs:
16
16
 
17
17
  strategy:
18
18
  matrix:
19
- ruby-version: [2.5, 2.6, 2.7]
19
+ ruby-version: [2.6, 2.7, 3.0]
20
20
 
21
21
  steps:
22
22
  - uses: actions/checkout@v2
@@ -2,7 +2,7 @@ inherit_gem:
2
2
  simplycop: .simplycop.yml
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 2.5
5
+ TargetRubyVersion: 2.6
6
6
  Rails:
7
7
  Enabled: false
8
8
  Documentation:
data/Gemfile CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  source 'https://rubygems.org'
4
- gem 'simplycop', git: 'https://github.com/simplybusiness/simplycop.git'
5
4
  group :development, :test do
6
5
  gem 'minitest'
7
6
  gem 'rake'
8
7
  gem 'simplecov', '0.17.1'
8
+ gem 'simplycop'
9
9
  end
data/README.md CHANGED
@@ -11,13 +11,13 @@ gem install twiglet
11
11
 
12
12
  ## How to use
13
13
 
14
- Create a new logger like so:
14
+ ### Instantiate the logger
15
15
 
16
16
  ```ruby
17
17
  require 'twiglet/logger'
18
18
  logger = Twiglet::Logger.new('service name')
19
19
  ```
20
-
20
+ #### Optional initialization parameters
21
21
  A hash can optionally be passed in as a keyword argument for `default_properties`. This hash must be in the Elastic Common Schema format and will be present in every log message created by this Twiglet logger object.
22
22
 
23
23
  You may also provide an optional `output` keyword argument which should be an object with a `puts` method - like `$stdout`.
@@ -28,7 +28,7 @@ Lastly, you may provide the optional keyword argument `level` to initialize the
28
28
 
29
29
  The defaults for both `output` and `now` should serve for most uses, though you may want to override them for testing as we have done [here](test/logger_test.rb).
30
30
 
31
- To use, simply invoke like most other loggers:
31
+ ### Invoke the Logger
32
32
 
33
33
  ```ruby
34
34
  logger.error({ event: { action: 'startup' }, message: "Emergency! There's an Emergency going on" })
@@ -56,6 +56,7 @@ This will write to STDOUT a JSON string:
56
56
 
57
57
  A message is always required unless a block is provided. The message can be an object or a string.
58
58
 
59
+ #### Error logging
59
60
  An optional error can also be provided, in which case the error message and backtrace will be logged in the relevant ECS compliant fields:
60
61
 
61
62
  ```ruby
@@ -72,7 +73,8 @@ These will both result in the same JSON string written to STDOUT:
72
73
  {"ecs":{"version":"1.5.0"},"@timestamp":"2020-08-21T15:44:37.890Z","service":{"name":"service name"},"log":{"level":"error"},"message":"DB connection failed.","error":{"message":"Connection timed-out"}}
73
74
  ```
74
75
 
75
- Add log event specific information simply as attributes in a hash:
76
+ #### Custom fields
77
+ Log custom event-specific information simply as attributes in a hash:
76
78
 
77
79
  ```ruby
78
80
  logger.info({
@@ -129,6 +131,15 @@ which will print:
129
131
  {"service":{"name":"service name"},"@timestamp":"2020-05-14T10:58:30.780+01:00","log":{"level":"error"},"event":{"action":"HTTP request"},"trace":{"id":"126bb6fa-28a2-470f-b013-eefbf9182b2d"},"message":"Error 500 in /pets/buy","http":{"request":{"method":"post","url.path":"/pet/buy"},"response":{"status_code":500}}}
130
132
  ```
131
133
 
134
+ ### Log formatting
135
+ Some third party applications will allow you to optionally specify a [log formatter](https://ruby-doc.org/stdlib-2.4.0/libdoc/logger/rdoc/Logger/Formatter.html).
136
+ Supplying a Twiglet log formatter will format those third party logs so that they are ECS compliant and have the same default parameters as your application's internal logs.
137
+
138
+ To access the formatter:
139
+ ```ruby
140
+ logger.formatter
141
+ ```
142
+
132
143
  ## Use of dotted keys (DEPRECATED)
133
144
 
134
145
  Writing nested json objects could be confusing. This library has a built-in feature to convert dotted keys into nested objects, so if you log like this:
@@ -8,27 +8,27 @@ logger = Twiglet::Logger.new('petshop')
8
8
 
9
9
  # Start our petshop
10
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
- })
11
+ event: {
12
+ action: 'startup'
13
+ },
14
+ message: "Ready to go, listening on port #{PORT}",
15
+ server: {
16
+ port: PORT
17
+ }
18
+ })
19
19
 
20
20
  # Use text logging
21
21
  logger.info("Ready to go, listening on port #{PORT}")
22
22
  #
23
23
  # We get a request
24
24
  request_logger = logger.with({
25
- event: {
26
- action: 'HTTP request'
27
- },
28
- trace: {
29
- id: '126bb6fa-28a2-470f-b013-eefbf9182b2d'
30
- }
31
- })
25
+ event: {
26
+ action: 'HTTP request'
27
+ },
28
+ trace: {
29
+ id: '126bb6fa-28a2-470f-b013-eefbf9182b2d'
30
+ }
31
+ })
32
32
 
33
33
  # Oh noes!
34
34
  db_err = StandardError.new('Connection timed-out')
@@ -37,16 +37,16 @@ request_logger.error({ message: 'DB connection failed.' }, db_err) if db_err
37
37
 
38
38
  # We return an error to the requester
39
39
  request_logger.info({
40
- message: 'Internal Server Error',
41
- http: {
42
- request: {
43
- method: 'get'
44
- },
45
- response: {
46
- status_code: 500
47
- }
48
- }
49
- })
40
+ message: 'Internal Server Error',
41
+ http: {
42
+ request: {
43
+ method: 'get'
44
+ },
45
+ response: {
46
+ status_code: 500
47
+ }
48
+ }
49
+ })
50
50
 
51
51
  # Logging with an empty message is an anti-pattern and is therefore forbidden
52
52
  # Both of the following lines would throw an error
@@ -11,7 +11,7 @@ class RequestLogger
11
11
  [status, headers, body]
12
12
  rescue StandardError => e
13
13
  log_error(env, 500, e)
14
- [500, {}, body]
14
+ raise e
15
15
  end
16
16
 
17
17
  private
@@ -26,24 +26,41 @@ class RequestLogger
26
26
  @logger.error(fields, error)
27
27
  end
28
28
 
29
+ # https://www.elastic.co/guide/en/ecs/1.5/ecs-field-reference.html
29
30
  def get_fields(env, status)
30
31
  message = "#{env['REQUEST_METHOD']}: #{env['PATH_INFO']}"
31
32
 
32
33
  {
33
- http: {
34
- request: {
35
- method: env['REQUEST_METHOD'],
36
- server: env['SERVER_NAME'],
37
- https_enabled: env['HTTPS'],
38
- path: env['PATH_INFO'],
39
- query: env['QUERY_STRING'] # Don't log PII query params
40
- },
41
- response: {
42
- status: status,
43
- body: { bytes: env['CONTENT_LENGTH'] }
44
- }
34
+ http: http_fields(env, status),
35
+ url: url_fields(env),
36
+ client: {
37
+ ip: env['HTTP_TRUE_CLIENT_IP'] || env['REMOTE_ADDR']
38
+ },
39
+ user_agent: {
40
+ original: env['HTTP_USER_AGENT']
45
41
  },
46
42
  message: message
47
43
  }
48
44
  end
45
+
46
+ def http_fields(env, status)
47
+ {
48
+ request: {
49
+ method: env['REQUEST_METHOD'],
50
+ mime_type: env['HTTP_ACCEPT']
51
+ },
52
+ response: {
53
+ status: status
54
+ },
55
+ version: env['HTTP_VERSION']
56
+ }
57
+ end
58
+
59
+ def url_fields(env)
60
+ {
61
+ path: env['PATH_INFO'],
62
+ query: env['QUERY_STRING'],
63
+ domain: env['SERVER_NAME']
64
+ }
65
+ end
49
66
  end
@@ -1,4 +1,5 @@
1
1
  require 'minitest/autorun'
2
+ require_relative '../../lib/twiglet/logger'
2
3
  require_relative './request_logger'
3
4
  require 'rack'
4
5
 
@@ -14,23 +15,43 @@ describe RequestLogger do
14
15
  end
15
16
 
16
17
  it 'logs the request data' do
17
- request.get("/some/path?some_var=1")
18
+ request.get("/some/path?some_var=1", 'HTTP_ACCEPT' => 'application/json',
19
+ 'REMOTE_ADDR' => '0.0.0.0',
20
+ 'HTTP_VERSION' => 'HTTP/1.1',
21
+ 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh)')
18
22
  log = JSON.parse(output.string)
19
- http_body = {
20
- "request" => {
21
- "https_enabled" => "off",
22
- "method" => "GET",
23
+
24
+ expected_log = {
25
+ "log" => { "level" => "info" },
26
+ "http" => {
27
+ "request" => {
28
+ "method" => "GET",
29
+ "mime_type" => 'application/json'
30
+ },
31
+ "response" => {
32
+ "status" => 200
33
+ },
34
+ "version" => 'HTTP/1.1'
35
+ },
36
+ "url" => {
23
37
  "path" => "/some/path",
24
38
  "query" => "some_var=1",
25
- "server" => "example.org"
39
+ "domain" => "example.org"
40
+ },
41
+ "client" => {
42
+ 'ip' => '0.0.0.0'
26
43
  },
27
- "response" => {
28
- "status" => 200,
29
- "body" => { "bytes" => "0" }
30
- }
44
+ "user_agent" => {
45
+ "original" => 'Mozilla/5.0 (Macintosh)'
46
+ },
47
+ "message" => "GET: /some/path"
31
48
  }
32
- assert_equal http_body, log["http"]
33
- assert_equal "GET: /some/path", log["message"]
49
+
50
+ assert_equal(log['log'], expected_log['log'])
51
+ assert_equal(log['http'], expected_log['http'])
52
+ assert_equal(log['url'], expected_log['url'])
53
+ assert_equal(log['user_agent'], expected_log['user_agent'])
54
+ assert_equal(log['message'], expected_log['message'])
34
55
  end
35
56
 
36
57
  it 'does not log PII' do
@@ -42,10 +63,12 @@ describe RequestLogger do
42
63
  end
43
64
 
44
65
  it 'logs an error message when a request is bad' do
45
- bad_request.get("/some/path")
66
+ -> { bad_request.get("/some/path") }.must_raise StandardError
46
67
  log = JSON.parse(output.string)
47
- assert_equal 'error', log['log']['level']
48
- assert_equal 'some exception', log['error']['message']
68
+ assert_equal log['log']['level'], 'error'
69
+ assert_equal log['error']['message'], 'some exception'
70
+ assert_equal log['error']['type'], 'StandardError'
71
+ assert_includes log['error']['stack_trace'], 'request_logger_test.rb'
49
72
  end
50
73
  end
51
74
 
@@ -22,8 +22,8 @@ module HashExtensions
22
22
 
23
23
  def build_nested_object(key, val)
24
24
  key.to_s
25
- .split('.')
26
- .reverse
27
- .reduce(val) { |nested, key_part| Hash[key_part.to_sym, nested] }
25
+ .split('.')
26
+ .reverse
27
+ .reduce(val) { |nested, key_part| Hash[key_part.to_sym, nested] }
28
28
  end
29
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Twiglet
4
- VERSION = '2.4.0'
4
+ VERSION = '3.0.4'
5
5
  end
@@ -3,6 +3,16 @@
3
3
  require 'minitest/autorun'
4
4
  require_relative '../lib/twiglet/logger'
5
5
 
6
+ LEVELS = [
7
+ { method: :debug, level: 'debug' },
8
+ { method: :info, level: 'info' },
9
+ { method: :warning, level: 'warn' },
10
+ { method: :warn, level: 'warn' },
11
+ { method: :critical, level: 'fatal' },
12
+ { method: :fatal, level: 'fatal' },
13
+ { method: :error, level: 'error' }
14
+ ].freeze
15
+
6
16
  # rubocop:disable Metrics/BlockLength
7
17
  describe Twiglet::Logger do
8
18
  before do
@@ -13,16 +23,6 @@ describe Twiglet::Logger do
13
23
  output: @buffer)
14
24
  end
15
25
 
16
- LEVELS = [
17
- { method: :debug, level: 'debug' },
18
- { method: :info, level: 'info' },
19
- { method: :warning, level: 'warn' },
20
- { method: :warn, level: 'warn' },
21
- { method: :critical, level: 'fatal' },
22
- { method: :fatal, level: 'fatal' },
23
- { method: :error, level: 'error' }
24
- ].freeze
25
-
26
26
  it 'should throw an error with an empty service name' do
27
27
  assert_raises RuntimeError do
28
28
  Twiglet::Logger.new(' ')
@@ -105,16 +105,16 @@ describe Twiglet::Logger do
105
105
  it "should be able to add properties with '.with'" do
106
106
  # Let's add some context to this customer journey
107
107
  purchase_logger = @logger.with({
108
- trace: {id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'},
109
- customer: {full_name: 'Freda Bloggs'},
110
- event: {action: 'pet purchase'}
111
- })
108
+ trace: {id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'},
109
+ customer: {full_name: 'Freda Bloggs'},
110
+ event: {action: 'pet purchase'}
111
+ })
112
112
 
113
113
  # do stuff
114
114
  purchase_logger.info({
115
- message: 'customer bought a dog',
116
- pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
117
- })
115
+ message: 'customer bought a dog',
116
+ pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
117
+ })
118
118
 
119
119
  log = read_json @buffer
120
120
 
@@ -285,12 +285,12 @@ describe Twiglet::Logger do
285
285
  describe 'dotted keys' do
286
286
  it 'should be able to convert dotted keys to nested objects' do
287
287
  @logger.debug({
288
- "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
289
- message: 'customer bought a dog',
290
- "pet.name": 'Barker',
291
- "pet.species": 'dog',
292
- "pet.breed": 'Bitsa'
293
- })
288
+ "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
289
+ message: 'customer bought a dog',
290
+ "pet.name": 'Barker',
291
+ "pet.species": 'dog',
292
+ "pet.breed": 'Bitsa'
293
+ })
294
294
  log = read_json(@buffer)
295
295
 
296
296
  assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
@@ -302,11 +302,11 @@ describe Twiglet::Logger do
302
302
 
303
303
  it 'should be able to mix dotted keys and nested objects' do
304
304
  @logger.debug({
305
- "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
306
- message: 'customer bought a dog',
307
- pet: {name: 'Barker', breed: 'Bitsa'},
308
- "pet.species": 'dog'
309
- })
305
+ "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
306
+ message: 'customer bought a dog',
307
+ pet: {name: 'Barker', breed: 'Bitsa'},
308
+ "pet.species": 'dog'
309
+ })
310
310
  log = read_json(@buffer)
311
311
 
312
312
  assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
@@ -18,7 +18,7 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = `git ls-files -- {test}/*`.split("\n")
19
19
 
20
20
  gem.require_paths = ['lib']
21
- gem.required_ruby_version = '>= 2.5'
21
+ gem.required_ruby_version = '>= 2.6'
22
22
 
23
23
  gem.license = 'Copyright SimplyBusiness'
24
24
  end
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: 2.4.0
4
+ version: 3.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simply Business
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-04 00:00:00.000000000 Z
11
+ date: 2021-01-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Like a log, only smaller.
14
14
  email:
@@ -18,6 +18,7 @@ extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
20
  - ".github/CODEOWNERS"
21
+ - ".github/dependabot.yml"
21
22
  - ".github/workflows/gem-publish.yml"
22
23
  - ".github/workflows/ruby.yml"
23
24
  - ".github/workflows/version-forget-me-not.yml"
@@ -59,14 +60,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
59
60
  requirements:
60
61
  - - ">="
61
62
  - !ruby/object:Gem::Version
62
- version: '2.5'
63
+ version: '2.6'
63
64
  required_rubygems_version: !ruby/object:Gem::Requirement
64
65
  requirements:
65
66
  - - ">="
66
67
  - !ruby/object:Gem::Version
67
68
  version: '0'
68
69
  requirements: []
69
- rubygems_version: 3.0.3
70
+ rubygems_version: 3.1.4
70
71
  signing_key:
71
72
  specification_version: 4
72
73
  summary: Twiglet