twiglet 2.0.0 → 2.2.2

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: 8f7e6901994e7715bab639be94853dbbcce697e16da26f2f8340404ebe4c0211
4
- data.tar.gz: 1758572f62c689a68642cfb8887dc2ea380a3602332cf2321b06d51e80a7066d
3
+ metadata.gz: 9bd7e1ea9d6464abb4dcb96e8d2679ce6c82ebc289c55cb5989d8f8f657f065f
4
+ data.tar.gz: 2b2d8e963cf250c0b746dd9d56c40368e31f67b1deaf6cd63c9dee9a731ce804
5
5
  SHA512:
6
- metadata.gz: 808fb4ddbdaff2287331e022ec4a593469335a40476fa59cb0546facd126113c6b564f7538c3366984bb40ed178a131861b0a97e9ea571640caa68db0354a906
7
- data.tar.gz: 83913e66900495ee39ba12d8f38e16e83fbc3621c4caf7bdaa128c6b44568f82face4498f1b2081e798ff6265b79a7758dd97eff08bd6224e8b0b7d077e8cb81
6
+ metadata.gz: 8277fd64f609bb4edbf6c95e0f97dc56784a1ea63415c1b261dd9649a69fd6cfee54abbb0c91dfb2232f78f0a14b9c539592d4ca58d57d566350f2a0dbceb648
7
+ data.tar.gz: e9daae207235db6582162e7d2682cf69ea59e5dc09796b7d804bed6f31775ff07ceaff4b38518dd7c6d6a1ec40759c20dadcbebd388417187bcccb66c09fe793
@@ -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
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  *.gem
2
+ Gemfile.lock
2
3
  *.rbc
3
4
  /.config
4
5
  /coverage/
@@ -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:
data/README.md CHANGED
@@ -40,6 +40,17 @@ 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
+
43
54
  Errors can be logged as well, and this will log the error message and backtrace in the relevant ECS compliant fields:
44
55
 
45
56
  ```ruby
@@ -68,6 +79,18 @@ This writes:
68
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"}}
69
80
  ```
70
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
+
71
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:
72
95
 
73
96
  ```ruby
@@ -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,40 +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
- alias_method :warn, :warning
40
-
41
- def error(message, error = nil)
30
+ def error(message = {}, error = nil, &block)
42
31
  if error
43
- message = message.merge({
44
- 'error': {
45
- 'message': error.message,
46
- 'stack_trace': error.backtrace.join("\n")
47
- }
48
- })
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)
49
39
  end
50
40
 
51
- log(level: 'error', message: message)
52
- end
53
-
54
- def critical(message)
55
- log(level: 'critical', message: message)
41
+ super(message, &block)
56
42
  end
57
43
 
58
44
  def with(default_properties)
@@ -62,30 +48,13 @@ module Twiglet
62
48
  output: @output)
63
49
  end
64
50
 
65
- private
66
-
67
- def log(level:, message:)
68
- raise 'Message must be a Hash' unless message.is_a?(Hash)
51
+ alias_method :warning, :warn
52
+ alias_method :critical, :fatal
69
53
 
70
- message = message.transform_keys(&:to_sym)
71
- message.key?(:message) || raise('Log object must have a \'message\' property')
72
-
73
- message[:message].strip.empty? && raise('The \'message\' property of log object must not be empty')
74
-
75
- base_message = {
76
- service: {
77
- name: @service_name
78
- },
79
- "@timestamp": @now.call.iso8601(3),
80
- log: {
81
- level: level
82
- }
83
- }
54
+ private
84
55
 
85
- @output.puts base_message
86
- .deep_merge(@default_properties.to_nested)
87
- .deep_merge(message.to_nested)
88
- .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
89
58
  end
90
59
  end
91
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Twiglet
4
- VERSION = '2.0.0'
4
+ VERSION = '2.2.2'
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,191 +13,297 @@ 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
+ })
109
+
110
+ # do stuff
111
+ purchase_logger.info({
112
+ message: 'customer bought a dog',
113
+ pet: {name: 'Barker', species: 'dog', breed: 'Bitsa'}
114
+ })
115
+
116
+ log = read_json @buffer
117
+
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
131
124
 
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
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)
147
130
 
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
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",'\
140
+ '"service":{"name":"petshop"},"log":{"level":"debug"},"message":"hi"}'\
141
+ "\n"\
142
+ '{"@timestamp":"2020-05-11T15:01:01.000Z",'\
143
+ '"service":{"name":"petshop"},"log":{"level":"info"},"message":"there"}'\
144
+ "\n"\
145
+
146
+ assert_equal expected_output, @buffer.string
147
+ end
148
+
149
+ it 'should be able to convert dotted keys to nested objects' do
150
+ @logger.debug({
151
+ "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
152
+ message: 'customer bought a dog',
153
+ "pet.name": 'Barker',
154
+ "pet.species": 'dog',
155
+ "pet.breed": 'Bitsa'
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 be able to mix dotted keys and nested objects' do
167
+ @logger.debug({
168
+ "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
169
+ message: 'customer bought a dog',
170
+ pet: {name: 'Barker', breed: 'Bitsa'},
171
+ "pet.species": 'dog'
172
+ })
173
+ log = read_json(@buffer)
174
+
175
+ assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', log[:trace][:id]
176
+ assert_equal 'customer bought a dog', log[:message]
177
+ assert_equal 'Barker', log[:pet][:name]
178
+ assert_equal 'dog', log[:pet][:species]
179
+ assert_equal 'Bitsa', log[:pet][:breed]
180
+ end
181
+
182
+ it 'should work with mixed string and symbol properties' do
183
+ log = {
184
+ "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb'
185
+ }
186
+ event = {}
187
+ log['event'] = event
188
+ log['message'] = 'customer bought a dog'
189
+ pet = {}
190
+ pet['name'] = 'Barker'
191
+ pet['breed'] = 'Bitsa'
192
+ pet[:species] = 'dog'
193
+ log[:pet] = pet
194
+
195
+ @logger.debug(log)
196
+ actual_log = read_json(@buffer)
197
+
198
+ assert_equal '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb', actual_log[:trace][:id]
199
+ assert_equal 'customer bought a dog', actual_log[:message]
200
+ assert_equal 'Barker', actual_log[:pet][:name]
201
+ assert_equal 'dog', actual_log[:pet][:species]
202
+ assert_equal 'Bitsa', actual_log[:pet][:breed]
203
+ end
204
+
205
+ it 'should log an error with backtrace' do
206
+ begin
207
+ 1 / 0
208
+ rescue StandardError => e
209
+ @logger.error({message: 'Artificially raised exception'}, e)
210
+ end
211
+
212
+ actual_log = read_json(@buffer)
213
+
214
+ assert_equal 'Artificially raised exception', actual_log[:message]
215
+ assert_equal 'divided by 0', actual_log[:error][:message]
216
+ assert_match 'logger_test.rb', actual_log[:error][:stack_trace].lines.first
217
+ end
170
218
 
171
- it 'should log an error with backtrace' do
172
- begin
173
- 1 / 0
174
- rescue StandardError => e
219
+ it 'should log an error without backtrace' do
220
+ e = StandardError.new('Connection timed-out')
175
221
  @logger.error({message: 'Artificially raised exception'}, e)
222
+
223
+ actual_log = read_json(@buffer)
224
+
225
+ assert_equal 'Artificially raised exception', actual_log[:message]
226
+ assert_equal 'Connection timed-out', actual_log[:error][:message]
227
+ refute actual_log[:error].key?(:stack_trace)
176
228
  end
177
229
 
178
- actual_log = read_json(@buffer)
230
+ LEVELS.each do |attrs|
231
+ it "should correctly log level when calling #{attrs[:method]}" do
232
+ @logger.public_send(attrs[:method], {message: 'a log message'})
233
+ actual_log = read_json(@buffer)
179
234
 
180
- assert_equal 'Artificially raised exception', actual_log[:message]
181
- assert_equal 'divided by 0', actual_log[:error][:message]
182
- assert_match 'logger_test.rb', actual_log[:error][:stack_trace].lines.first
235
+ assert_equal attrs[:level], actual_log[:log][:level]
236
+ assert_equal 'a log message', actual_log[:message]
237
+ end
238
+ end
183
239
  end
184
240
 
185
- levels = [
186
- { method: :debug, level: 'debug' },
187
- { method: :info, level: 'info' },
188
- { method: :warning, level: 'warning' },
189
- { method: :warn, level: 'warning' },
190
- { method: :error, level: 'error' }
191
- ]
241
+ describe 'text logging' do
242
+ it 'should throw an error with an empty message' do
243
+ assert_raises RuntimeError do
244
+ @logger.info('')
245
+ end
246
+ end
192
247
 
193
- levels.each do |attrs|
194
- it "should correctly log level when calling #{attrs[:method]}" do
195
- @logger.public_send(attrs[:method], {message: 'a log message'})
248
+ it 'should log mandatory attributes' do
249
+ @logger.error('Out of pets exception')
196
250
  actual_log = read_json(@buffer)
197
251
 
198
- assert_equal attrs[:level], actual_log[:log][:level]
199
- assert_equal 'a log message', actual_log[:message]
252
+ expected_log = {
253
+ message: 'Out of pets exception',
254
+ "@timestamp": '2020-05-11T15:01:01.000Z',
255
+ service: {
256
+ name: 'petshop'
257
+ },
258
+ log: {
259
+ level: 'error'
260
+ }
261
+ }
262
+
263
+ assert_equal expected_log, actual_log
264
+ end
265
+
266
+ it 'should log the provided message' do
267
+ @logger.error('Emergency! Emergency!')
268
+ log = read_json(@buffer)
269
+
270
+ assert_equal 'Emergency! Emergency!', log[:message]
271
+ end
272
+
273
+ LEVELS.each do |attrs|
274
+ it "should correctly log level when calling #{attrs[:method]}" do
275
+ @logger.public_send(attrs[:method], 'a log message')
276
+ actual_log = read_json(@buffer)
277
+
278
+ assert_equal attrs[:level], actual_log[:log][:level]
279
+ assert_equal 'a log message', actual_log[:message]
280
+ end
281
+ end
282
+ end
283
+
284
+ describe 'logging with a block' do
285
+ LEVELS.each do |attrs|
286
+ it "should correctly log the block when calling #{attrs[:method]}" do
287
+ block = proc { 'a block log message' }
288
+ @logger.public_send(attrs[:method], &block)
289
+ actual_log = read_json(@buffer)
290
+
291
+ assert_equal attrs[:level], actual_log[:log][:level]
292
+ assert_equal 'a block log message', actual_log[:message]
293
+ end
294
+ end
295
+ end
296
+
297
+ describe 'logger level' do
298
+ [
299
+ { expression: :info, level: 1 },
300
+ { expression: 'warn', level: 2 },
301
+ { expression: Logger::DEBUG, level: 0 }
302
+ ].each do |args|
303
+ it "sets the severity threshold to level #{args[:level]}" do
304
+ @logger.level = args[:expression]
305
+ assert_equal args[:level], @logger.level
306
+ end
200
307
  end
201
308
  end
202
309
 
@@ -207,3 +314,4 @@ describe Twiglet::Logger do
207
314
  JSON.parse(buffer.read, symbolize_names: true)
208
315
  end
209
316
  end
317
+ # 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
4
+ version: 2.2.2
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-05 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:
@@ -24,15 +24,16 @@ files:
24
24
  - ".ruby-version"
25
25
  - CODE_OF_CONDUCT.md
26
26
  - Gemfile
27
- - Gemfile.lock
28
27
  - LICENSE
29
28
  - RATIONALE.md
30
29
  - README.md
31
30
  - Rakefile
32
31
  - example_app.rb
33
32
  - lib/hash_extensions.rb
33
+ - lib/twiglet/formatter.rb
34
34
  - lib/twiglet/logger.rb
35
35
  - lib/twiglet/version.rb
36
+ - test/formatter_test.rb
36
37
  - test/hash_extensions_test.rb
37
38
  - test/logger_test.rb
38
39
  - twiglet.gemspec
@@ -1,62 +0,0 @@
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
- rake (13.0.1)
31
- rexml (3.2.4)
32
- rubocop (0.80.1)
33
- jaro_winkler (~> 1.5.1)
34
- parallel (~> 1.10)
35
- parser (>= 2.7.0.1)
36
- rainbow (>= 2.2.2, < 4.0)
37
- rexml
38
- ruby-progressbar (~> 1.7)
39
- unicode-display_width (>= 1.4.0, < 1.7)
40
- rubocop-rails (2.5.2)
41
- activesupport
42
- rack (>= 1.1)
43
- rubocop (>= 0.72.0)
44
- rubocop-rspec (1.39.0)
45
- rubocop (>= 0.68.1)
46
- ruby-progressbar (1.10.1)
47
- thread_safe (0.3.6)
48
- tzinfo (1.2.7)
49
- thread_safe (~> 0.1)
50
- unicode-display_width (1.6.1)
51
- zeitwerk (2.3.0)
52
-
53
- PLATFORMS
54
- ruby
55
-
56
- DEPENDENCIES
57
- minitest
58
- rake
59
- simplycop!
60
-
61
- BUNDLED WITH
62
- 2.1.4