twiglet 2.0.0.pre.alpha.1 → 2.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2bb718963cc1602d548f4f80bbf03a932feac4ceaa5b11bc3b6d19a1ee58e10
4
- data.tar.gz: 6ee69671edd4a487a905c45dec2833141fc6a4a7b8702bdffdc2dc0b81e6e9e4
3
+ metadata.gz: 3cb9302ac1ae427b94de457a976ace7c30d30b875676f43e91b4e206056cedf9
4
+ data.tar.gz: e436939ff209613f1a4f0c64c33f06a0cb3dcf36c9faa20b25bfc8c829946a26
5
5
  SHA512:
6
- metadata.gz: 3ccecc55b8320608406a0dfec87f617e12501dbdebbe62de7b74881afde44439ba789c59b80c1530c9e4325dc424a202c1ea71958adf32ce1bda020103b38a48
7
- data.tar.gz: 2762f0a75ac22ae69ef30de4afa022dc91e1504e578ce0fcf3198291e0f4963adbda2ab57e7e5b8a852ff2bfe6f058f3478d23c622a09a0eef98cc2821881c4b
6
+ metadata.gz: 06f15914e02513b777e473c6472186f1d189dae59df2e8e59a5f00efad3d7a45d4cf053b96e862c0b9337b70c2c672de12fe73d9e766ceaa047d483005f5000b
7
+ data.tar.gz: 35fbe15d0b18895fdb5443e2493d212c66989fea77eea204b2cb900535da78133e001c638981e856deacca3c905427222fa2a7073f895c1b26dbb270b63397c8
@@ -1,4 +1,3 @@
1
1
  # Add your project owners info here
2
2
  # More information: https://help.github.com/articles/about-codeowners/
3
3
  * @simplybusiness/application-tooling
4
- *
@@ -3,8 +3,6 @@ name: Ruby CI
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - '*' # matches every branch
7
- - '*/*' # matches every branch containing a single '/'
8
6
 
9
7
  jobs:
10
8
  build:
@@ -30,3 +28,6 @@ jobs:
30
28
  - name: Run all tests
31
29
  run: bundle exec rake test
32
30
  shell: bash
31
+ - name: Run example_app
32
+ run: bundle exec ruby example_app.rb
33
+ shell: bash
@@ -8,7 +8,7 @@ AllCops:
8
8
  Documentation:
9
9
  Enabled: false
10
10
  Metrics/BlockLength:
11
- Max: 200
11
+ Max: 250
12
12
  Metrics/AbcSize:
13
13
  Max: 20
14
14
  Metrics/MethodLength:
@@ -25,7 +25,7 @@ GEM
25
25
  parallel (1.19.1)
26
26
  parser (2.7.1.3)
27
27
  ast (~> 2.4.0)
28
- rack (2.2.2)
28
+ rack (2.2.3)
29
29
  rainbow (3.0.0)
30
30
  rake (13.0.1)
31
31
  rexml (3.2.4)
data/README.md CHANGED
@@ -40,6 +40,24 @@ This will write to STDOUT a JSON string:
40
40
 
41
41
  Obviously the timestamp will be different.
42
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
+
43
61
  Add log event specific information simply as attributes in a hash:
44
62
 
45
63
  ```ruby
@@ -61,6 +79,18 @@ This writes:
61
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"}}
62
80
  ```
63
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"}}
91
+ ```
92
+
93
+
64
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:
65
95
 
66
96
  ```ruby
@@ -126,7 +156,7 @@ First: Please read our project [Code of Conduct](../CODE_OF_CONDUCT.md).
126
156
  Second: run the tests and make sure your changes don't break anything:
127
157
 
128
158
  ```bash
129
- for file in test/*test.rb; do ruby $file; done
159
+ bundle exec rake test
130
160
  ```
131
161
 
132
162
  Then please feel free to submit a PR.
@@ -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: {
@@ -0,0 +1,69 @@
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
+ .concat("\n")
67
+ end
68
+ end
69
+ end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
3
4
  require 'time'
4
5
  require 'json'
6
+ require_relative 'formatter'
5
7
  require_relative '../hash_extensions'
6
8
 
7
9
  module Twiglet
8
- class Logger
10
+ class Logger < ::Logger
9
11
  Hash.include HashExtensions
10
12
 
11
13
  def initialize(
@@ -19,36 +21,24 @@ module Twiglet
19
21
  @output = output
20
22
 
21
23
  raise 'Service name is mandatory' \
22
- unless @service_name.is_a?(String) && !@service_name.strip.empty?
24
+ unless service_name.is_a?(String) && !service_name.strip.empty?
23
25
 
24
- @default_properties = default_properties
26
+ formatter = Twiglet::Formatter.new(service_name, default_properties: default_properties, now: now)
27
+ super(output, formatter: formatter)
25
28
  end
26
29
 
27
- def debug(message)
28
- log(level: 'debug', message: message)
29
- end
30
-
31
- def info(message)
32
- log(level: 'info', message: message)
33
- end
34
-
35
- def warning(message)
36
- log(level: 'warning', message: message)
37
- end
38
-
39
- def error(message, error = nil)
30
+ def error(message = {}, error = nil, &block)
40
31
  if error
41
- message = message.merge({
42
- error_name: error.message,
43
- backtrace: error.backtrace
44
- })
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)
45
39
  end
46
40
 
47
- log(level: 'error', message: message)
48
- end
49
-
50
- def critical(message)
51
- log(level: 'critical', message: message)
41
+ super(message, &block)
52
42
  end
53
43
 
54
44
  def with(default_properties)
@@ -58,30 +48,13 @@ module Twiglet
58
48
  output: @output)
59
49
  end
60
50
 
61
- private
62
-
63
- def log(level:, message:)
64
- raise 'Message must be a Hash' unless message.is_a?(Hash)
65
-
66
- message = message.transform_keys(&:to_sym)
67
- message.key?(:message) || raise('Log object must have a \'message\' property')
51
+ alias_method :warning, :warn
52
+ alias_method :critical, :fatal
68
53
 
69
- message[:message].strip.empty? && raise('The \'message\' property of log object must not be empty')
70
-
71
- base_message = {
72
- service: {
73
- name: @service_name
74
- },
75
- "@timestamp": @now.call.iso8601(3),
76
- log: {
77
- level: level
78
- }
79
- }
54
+ private
80
55
 
81
- @output.puts base_message
82
- .deep_merge(@default_properties.to_nested)
83
- .deep_merge(message.to_nested)
84
- .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
85
58
  end
86
59
  end
87
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Twiglet
4
- VERSION = '2.0.0-alpha.1'
4
+ VERSION = '2.2.1'
5
5
  end
@@ -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
@@ -3,6 +3,7 @@
3
3
  require 'minitest/autorun'
4
4
  require_relative '../lib/twiglet/logger'
5
5
 
6
+ # rubocop:disable Metrics/BlockLength
6
7
  describe Twiglet::Logger do
7
8
  before do
8
9
  @now = -> { Time.utc(2020, 5, 11, 15, 1, 1) }
@@ -12,174 +13,296 @@ describe Twiglet::Logger do
12
13
  output: @buffer)
13
14
  end
14
15
 
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
+
15
26
  it 'should throw an error with an empty service name' do
16
27
  assert_raises RuntimeError do
17
28
  Twiglet::Logger.new(' ')
18
29
  end
19
30
  end
20
31
 
21
- it 'should throw an error with an empty message' do
22
- assert_raises RuntimeError do
23
- @logger.info('')
32
+ it 'conforms to the standard Ruby Logger API' do
33
+ [:debug, :debug?, :info, :info?, :warn, :warn?, :fatal, :fatal?, :error, :error?,
34
+ :level, :level=, :sev_threshold=].each do |call|
35
+ assert @logger.respond_to?(call), "Logger does not respond to #{call}"
24
36
  end
25
37
  end
26
38
 
27
- it 'should log mandatory attributes' do
28
- @logger.error({message: 'Out of pets exception'})
29
- actual_log = read_json(@buffer)
30
-
31
- expected_log = {
32
- message: 'Out of pets exception',
33
- "@timestamp": '2020-05-11T15:01:01.000Z',
34
- service: {
35
- name: 'petshop'
36
- },
37
- log: {
38
- level: 'error'
39
+ describe 'JSON logging' do
40
+ it 'should throw an error with an empty message' do
41
+ assert_raises RuntimeError do
42
+ @logger.info({message: ''})
43
+ end
44
+ end
45
+
46
+ it 'should log mandatory attributes' do
47
+ @logger.error({message: 'Out of pets exception'})
48
+ actual_log = read_json(@buffer)
49
+
50
+ expected_log = {
51
+ message: 'Out of pets exception',
52
+ "@timestamp": '2020-05-11T15:01:01.000Z',
53
+ service: {
54
+ name: 'petshop'
55
+ },
56
+ log: {
57
+ level: 'error'
58
+ }
39
59
  }
40
- }
41
60
 
42
- assert_equal expected_log, actual_log
43
- end
61
+ assert_equal expected_log, actual_log
62
+ end
44
63
 
45
- it 'should log the provided message' do
46
- @logger.error({event:
47
- {action: 'exception'},
48
- message: 'Emergency! Emergency!'})
49
- log = read_json(@buffer)
64
+ it 'should log the provided message' do
65
+ @logger.error({event:
66
+ {action: 'exception'},
67
+ message: 'Emergency! Emergency!'})
68
+ log = read_json(@buffer)
50
69
 
51
- assert_equal 'exception', log[:event][:action]
52
- assert_equal 'Emergency! Emergency!', log[:message]
53
- end
70
+ assert_equal 'exception', log[:event][:action]
71
+ assert_equal 'Emergency! Emergency!', log[:message]
72
+ end
54
73
 
55
- it 'should log scoped properties defined at creation' do
56
- extra_properties = {
57
- trace: {
58
- id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
59
- },
60
- service: {
61
- type: 'shop'
62
- },
63
- request: {method: 'get'},
64
- response: {status_code: 200}
65
- }
66
-
67
- output = StringIO.new
68
- logger = Twiglet::Logger.new('petshop',
69
- now: @now,
70
- output: output,
71
- default_properties: extra_properties)
72
-
73
- logger.error({message: 'GET /cats'})
74
- log = read_json output
75
-
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]
79
- assert_equal 'get', log[:request][:method]
80
- assert_equal 200, log[:response][:status_code]
81
- end
74
+ it 'should log scoped properties defined at creation' do
75
+ extra_properties = {
76
+ trace: {
77
+ id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
78
+ },
79
+ service: {
80
+ type: 'shop'
81
+ },
82
+ request: {method: 'get'},
83
+ response: {status_code: 200}
84
+ }
82
85
 
83
- it "should be able to add properties with '.with'" do
84
- # Let's add some context to this customer journey
85
- purchase_logger = @logger.with({
86
- trace: {id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'},
87
- customer: {full_name: 'Freda Bloggs'},
88
- event: {action: 'pet purchase'}
89
- })
90
-
91
- # do stuff
92
- purchase_logger.info({
93
- message: 'customer bought a dog',
94
- pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
95
- })
96
-
97
- log = read_json @buffer
98
-
99
- assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
100
- assert_equal 'Freda Bloggs', log[:customer][:full_name]
101
- assert_equal 'pet purchase', log[:event][:action]
102
- assert_equal 'customer bought a dog', log[:message]
103
- assert_equal 'Barker', log[:pet][:name]
104
- end
86
+ output = StringIO.new
87
+ logger = Twiglet::Logger.new('petshop',
88
+ now: @now,
89
+ output: output,
90
+ default_properties: extra_properties)
105
91
 
106
- it "should log 'message' string property" do
107
- message = {}
108
- message['message'] = 'Guinea pigs arrived'
109
- @logger.debug(message)
110
- log = read_json(@buffer)
92
+ logger.error({message: 'GET /cats'})
93
+ log = read_json output
111
94
 
112
- assert_equal 'Guinea pigs arrived', log[:message]
113
- end
95
+ assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
96
+ assert_equal 'petshop', log[:service][:name]
97
+ assert_equal 'shop', log[:service][:type]
98
+ assert_equal 'get', log[:request][:method]
99
+ assert_equal 200, log[:response][:status_code]
100
+ end
114
101
 
115
- it 'should be able to convert dotted keys to nested objects' do
116
- @logger.debug({
117
- "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
118
- message: 'customer bought a dog',
119
- "pet.name": 'Barker',
120
- "pet.species": 'dog',
121
- "pet.breed": 'Bitsa'
122
- })
123
- log = read_json(@buffer)
124
-
125
- assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
126
- assert_equal 'customer bought a dog', log[:message]
127
- assert_equal 'Barker', log[:pet][:name]
128
- assert_equal 'dog', log[:pet][:species]
129
- assert_equal 'Bitsa', log[:pet][:breed]
130
- end
102
+ it "should be able to add properties with '.with'" do
103
+ # Let's add some context to this customer journey
104
+ purchase_logger = @logger.with({
105
+ trace: {id: '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'},
106
+ customer: {full_name: 'Freda Bloggs'},
107
+ event: {action: 'pet purchase'}
108
+ })
131
109
 
132
- it 'should be able to mix dotted keys and nested objects' do
133
- @logger.debug({
134
- "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
135
- message: 'customer bought a dog',
136
- pet: {name: 'Barker', breed: 'Bitsa'},
137
- "pet.species": 'dog'
138
- })
139
- log = read_json(@buffer)
140
-
141
- assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
142
- assert_equal 'customer bought a dog', log[:message]
143
- assert_equal 'Barker', log[:pet][:name]
144
- assert_equal 'dog', log[:pet][:species]
145
- assert_equal 'Bitsa', log[:pet][:breed]
146
- end
110
+ # do stuff
111
+ purchase_logger.info({
112
+ message: 'customer bought a dog',
113
+ pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
114
+ })
147
115
 
148
- it 'should work with mixed string and symbol properties' do
149
- log = {
150
- "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
151
- }
152
- event = {}
153
- log['event'] = event
154
- log['message'] = 'customer bought a dog'
155
- pet = {}
156
- pet['name'] = 'Barker'
157
- pet['breed'] = 'Bitsa'
158
- pet[:species] = 'dog'
159
- log[:pet] = pet
160
-
161
- @logger.debug(log)
162
- actual_log = read_json(@buffer)
163
-
164
- assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', actual_log[:trace][:id]
165
- assert_equal 'customer bought a dog', actual_log[:message]
166
- assert_equal 'Barker', actual_log[:pet][:name]
167
- assert_equal 'dog', actual_log[:pet][:species]
168
- assert_equal 'Bitsa', actual_log[:pet][:breed]
169
- end
116
+ log = read_json @buffer
170
117
 
171
- it 'should log an error with backtrace' do
172
- begin
173
- 1 / 0
174
- rescue StandardError => e
118
+ assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
119
+ assert_equal 'Freda Bloggs', log[:customer][:full_name]
120
+ assert_equal 'pet purchase', log[:event][:action]
121
+ assert_equal 'customer bought a dog', log[:message]
122
+ assert_equal 'Barker', log[:pet][:name]
123
+ end
124
+
125
+ it "should log 'message' string property" do
126
+ message = {}
127
+ message['message'] = 'Guinea pigs arrived'
128
+ @logger.debug(message)
129
+ log = read_json(@buffer)
130
+
131
+ assert_equal 'Guinea pigs arrived', log[:message]
132
+ end
133
+
134
+ it "should log multiple messages properly" do
135
+ @logger.debug({message: 'hi'})
136
+ @logger.info({message: 'there'})
137
+
138
+ expected_output =
139
+ '{"@timestamp":"2020-05-11T15:01:01.000Z","service":{"name":"petshop"},"log":{"level":"debug"},"message":"hi"}'\
140
+ "\n"\
141
+ '{"@timestamp":"2020-05-11T15:01:01.000Z","service":{"name":"petshop"},"log":{"level":"info"},"message":"there"}'\
142
+ "\n"\
143
+
144
+ assert_equal expected_output, @buffer.string
145
+ end
146
+
147
+ it 'should be able to convert dotted keys to nested objects' do
148
+ @logger.debug({
149
+ "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
150
+ message: 'customer bought a dog',
151
+ "pet.name": 'Barker',
152
+ "pet.species": 'dog',
153
+ "pet.breed": 'Bitsa'
154
+ })
155
+ log = read_json(@buffer)
156
+
157
+ assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
158
+ assert_equal 'customer bought a dog', log[:message]
159
+ assert_equal 'Barker', log[:pet][:name]
160
+ assert_equal 'dog', log[:pet][:species]
161
+ assert_equal 'Bitsa', log[:pet][:breed]
162
+ end
163
+
164
+ it 'should be able to mix dotted keys and nested objects' do
165
+ @logger.debug({
166
+ "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
167
+ message: 'customer bought a dog',
168
+ pet: {name: 'Barker', breed: 'Bitsa'},
169
+ "pet.species": 'dog'
170
+ })
171
+ log = read_json(@buffer)
172
+
173
+ assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
174
+ assert_equal 'customer bought a dog', log[:message]
175
+ assert_equal 'Barker', log[:pet][:name]
176
+ assert_equal 'dog', log[:pet][:species]
177
+ assert_equal 'Bitsa', log[:pet][:breed]
178
+ end
179
+
180
+ it 'should work with mixed string and symbol properties' do
181
+ log = {
182
+ "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
183
+ }
184
+ event = {}
185
+ log['event'] = event
186
+ log['message'] = 'customer bought a dog'
187
+ pet = {}
188
+ pet['name'] = 'Barker'
189
+ pet['breed'] = 'Bitsa'
190
+ pet[:species] = 'dog'
191
+ log[:pet] = pet
192
+
193
+ @logger.debug(log)
194
+ actual_log = read_json(@buffer)
195
+
196
+ assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', actual_log[:trace][:id]
197
+ assert_equal 'customer bought a dog', actual_log[:message]
198
+ assert_equal 'Barker', actual_log[:pet][:name]
199
+ assert_equal 'dog', actual_log[:pet][:species]
200
+ assert_equal 'Bitsa', actual_log[:pet][:breed]
201
+ end
202
+
203
+ it 'should log an error with backtrace' do
204
+ begin
205
+ 1 / 0
206
+ rescue StandardError => e
207
+ @logger.error({message: 'Artificially raised exception'}, e)
208
+ end
209
+
210
+ actual_log = read_json(@buffer)
211
+
212
+ assert_equal 'Artificially raised exception', actual_log[:message]
213
+ assert_equal 'divided by 0', actual_log[:error][:message]
214
+ assert_match 'logger_test.rb', actual_log[:error][:stack_trace].lines.first
215
+ end
216
+
217
+ it 'should log an error without backtrace' do
218
+ e = StandardError.new('Connection timed-out')
175
219
  @logger.error({message: 'Artificially raised exception'}, e)
220
+
221
+ actual_log = read_json(@buffer)
222
+
223
+ assert_equal 'Artificially raised exception', actual_log[:message]
224
+ assert_equal 'Connection timed-out', actual_log[:error][:message]
225
+ refute actual_log[:error].key?(:stack_trace)
226
+ end
227
+
228
+ LEVELS.each do |attrs|
229
+ it "should correctly log level when calling #{attrs[:method]}" do
230
+ @logger.public_send(attrs[:method], {message: 'a log message'})
231
+ actual_log = read_json(@buffer)
232
+
233
+ assert_equal attrs[:level], actual_log[:log][:level]
234
+ assert_equal 'a log message', actual_log[:message]
235
+ end
236
+ end
237
+ end
238
+
239
+ describe 'text logging' do
240
+ it 'should throw an error with an empty message' do
241
+ assert_raises RuntimeError do
242
+ @logger.info('')
243
+ end
176
244
  end
177
245
 
178
- actual_log = read_json(@buffer)
246
+ it 'should log mandatory attributes' do
247
+ @logger.error('Out of pets exception')
248
+ actual_log = read_json(@buffer)
179
249
 
180
- assert_equal 'Artificially raised exception', actual_log[:message]
181
- assert_equal 'divided by 0', actual_log[:error_name]
182
- assert_match 'logger_test.rb', actual_log[:backtrace].first
250
+ expected_log = {
251
+ message: 'Out of pets exception',
252
+ "@timestamp": '2020-05-11T15:01:01.000Z',
253
+ service: {
254
+ name: 'petshop'
255
+ },
256
+ log: {
257
+ level: 'error'
258
+ }
259
+ }
260
+
261
+ assert_equal expected_log, actual_log
262
+ end
263
+
264
+ it 'should log the provided message' do
265
+ @logger.error('Emergency! Emergency!')
266
+ log = read_json(@buffer)
267
+
268
+ assert_equal 'Emergency! Emergency!', log[:message]
269
+ end
270
+
271
+ LEVELS.each do |attrs|
272
+ it "should correctly log level when calling #{attrs[:method]}" do
273
+ @logger.public_send(attrs[:method], 'a log message')
274
+ actual_log = read_json(@buffer)
275
+
276
+ assert_equal attrs[:level], actual_log[:log][:level]
277
+ assert_equal 'a log message', actual_log[:message]
278
+ end
279
+ end
280
+ end
281
+
282
+ describe 'logging with a block' do
283
+ LEVELS.each do |attrs|
284
+ it "should correctly log the block when calling #{attrs[:method]}" do
285
+ block = proc { 'a block log message' }
286
+ @logger.public_send(attrs[:method], &block)
287
+ actual_log = read_json(@buffer)
288
+
289
+ assert_equal attrs[:level], actual_log[:log][:level]
290
+ assert_equal 'a block log message', actual_log[:message]
291
+ end
292
+ end
293
+ end
294
+
295
+ describe 'logger level' do
296
+ [
297
+ { expression: :info, level: 1 },
298
+ { expression: 'warn', level: 2 },
299
+ { expression: Logger::DEBUG, level: 0 }
300
+ ].each do |args|
301
+ it "sets the severity threshold to level #{args[:level]}" do
302
+ @logger.level = args[:expression]
303
+ assert_equal args[:level], @logger.level
304
+ end
305
+ end
183
306
  end
184
307
 
185
308
  private
@@ -189,3 +312,4 @@ describe Twiglet::Logger do
189
312
  JSON.parse(buffer.read, symbolize_names: true)
190
313
  end
191
314
  end
315
+ # rubocop:enable Metrics/BlockLength
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.0.0.pre.alpha.1
4
+ version: 2.2.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-03 00:00:00.000000000 Z
11
+ date: 2020-06-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Like a log, only smaller.
14
14
  email:
@@ -31,8 +31,10 @@ files:
31
31
  - Rakefile
32
32
  - example_app.rb
33
33
  - lib/hash_extensions.rb
34
+ - lib/twiglet/formatter.rb
34
35
  - lib/twiglet/logger.rb
35
36
  - lib/twiglet/version.rb
37
+ - test/formatter_test.rb
36
38
  - test/hash_extensions_test.rb
37
39
  - test/logger_test.rb
38
40
  - twiglet.gemspec
@@ -51,9 +53,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
51
53
  version: '2.6'
52
54
  required_rubygems_version: !ruby/object:Gem::Requirement
53
55
  requirements:
54
- - - ">"
56
+ - - ">="
55
57
  - !ruby/object:Gem::Version
56
- version: 1.3.1
58
+ version: '0'
57
59
  requirements: []
58
60
  rubygems_version: 3.1.2
59
61
  signing_key: