twiglet 2.1.0 → 2.2.3

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: 9a94a60f7f04b8a19fd27f483e9f8e29992a237fa03491ad417a385032b98401
4
- data.tar.gz: 976a134b1c69aa19956b5979affa35fc0141cb5164a8fa67a0aa91da1f490b2a
3
+ metadata.gz: dd959654140f88727254fcf4d660dd017e8a00e12f3a68eabf6359246f7aeb97
4
+ data.tar.gz: ab14bddf0d5b9138634000a0fad1e7d4fd5d13d56e2e604aef87a74e2c0c7d78
5
5
  SHA512:
6
- metadata.gz: 93c304efe8eeccbe2f6ed826499a288eee8d07897c79e1c0aca1f16e3908ea136a1731db7318228e5fa819b3cdafe6b196926a0fd1f9d387f94f7a315b0e868b
7
- data.tar.gz: 74b4d3ec698510d3a236a159bf92fe1b0d07d6caf60fce91e46d24d89680ea5c93ffaee042606c5560d72709148756912d011405c113f51213f959c449d9e0ba
6
+ metadata.gz: 1effc27ff4f08c25d1b92789e7b7937da24d652c922bf0bc404a4ca106d5c670897b0e24bbee17ee0fd1d1e584457bba06034e5fb0432edf6d517cd6dc3f801b
7
+ data.tar.gz: 5e6a270d224fb73e077670036b2540a23787ac4d9cde08c8aa0912c2c94e2ec392e49b8d39e134d13c4a76b3e6294729b5241ac651504e9099e69509b8ddf044
@@ -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/
data/README.md CHANGED
@@ -22,7 +22,9 @@ A hash can optionally be passed in as a keyword argument for `default_properties
22
22
 
23
23
  You may also provide an optional `output` keyword argument which should be an object with a `puts` method - like `$stdout`.
24
24
 
25
- Lastly, you can provide another optional keyword argument called `now`, which should be a function returning a `Time` string in ISO8601 format.
25
+ In addition, you can provide another optional keyword argument called `now`, which should be a function returning a `Time` string in ISO8601 format.
26
+
27
+ Lastly, you may provide the optional keyword argument `level` to initialize the logger with a severity threshold. Alternatively, the threshold can be updated at runtime by calling the `level` instance method.
26
28
 
27
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).
28
30
 
@@ -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,112 +1,63 @@
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(
12
14
  service_name,
13
15
  default_properties: {},
14
16
  now: -> { Time.now.utc },
15
- output: $stdout
17
+ output: $stdout,
18
+ level: Logger::DEBUG
16
19
  )
17
20
  @service_name = service_name
18
21
  @now = now
19
22
  @output = output
23
+ @level = level
20
24
 
21
25
  raise 'Service name is mandatory' \
22
- unless @service_name.is_a?(String) && !@service_name.strip.empty?
26
+ unless service_name.is_a?(String) && !service_name.strip.empty?
23
27
 
24
- @default_properties = default_properties
28
+ formatter = Twiglet::Formatter.new(service_name, default_properties: default_properties, now: now)
29
+ super(output, formatter: formatter, level: level)
25
30
  end
26
31
 
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)
32
+ def error(message = {}, error = nil, &block)
42
33
  if error
43
- message = message.merge({
44
- 'error': {
45
- 'message': error.message,
46
- 'stack_trace': error.backtrace.join("\n")
47
- }
48
- })
34
+ error_fields = {
35
+ 'error': {
36
+ 'message': error.message
37
+ }
38
+ }
39
+ add_stack_trace(error_fields, error)
40
+ message.is_a?(Hash) ? message.merge!(error_fields) : error_fields.merge!(message: message)
49
41
  end
50
42
 
51
- log(level: 'error', message: message)
52
- end
53
-
54
- def critical(message)
55
- log(level: 'critical', message: message)
43
+ super(message, &block)
56
44
  end
57
45
 
58
- alias_method :fatal, :critical
59
-
60
46
  def with(default_properties)
61
47
  Logger.new(@service_name,
62
48
  default_properties: default_properties,
63
49
  now: @now,
64
- output: @output)
50
+ output: @output,
51
+ level: @level)
65
52
  end
66
53
 
67
- private
54
+ alias_method :warning, :warn
55
+ alias_method :critical, :fatal
68
56
 
69
- def log(level:, message:)
70
- case message
71
- when String
72
- log_text(level, message: message)
73
- when Hash
74
- log_object(level, message: message)
75
- else
76
- raise('Message must be String or Hash')
77
- end
78
- end
79
-
80
- def log_text(level, message:)
81
- raise('The \'message\' property of log object must not be empty') if message.strip.empty?
82
-
83
- message = { message: message }
84
- log_message(level, message: message)
85
- end
86
-
87
- def log_object(level, message:)
88
- message = message.transform_keys(&:to_sym)
89
- message.key?(:message) || raise('Log object must have a \'message\' property')
90
- message[:message].strip.empty? && raise('The \'message\' property of log object must not be empty')
91
-
92
- log_message(level, message: message)
93
- end
94
-
95
- def log_message(level, message:)
96
- base_message = {
97
- "@timestamp": @now.call.iso8601(3),
98
- service: {
99
- name: @service_name
100
- },
101
- log: {
102
- level: level
103
- }
104
- }
57
+ private
105
58
 
106
- @output.puts base_message
107
- .deep_merge(@default_properties.to_nested)
108
- .deep_merge(message.to_nested)
109
- .to_json
59
+ def add_stack_trace(hash_to_add_to, error)
60
+ hash_to_add_to[:error][:stack_trace] = error.backtrace.join("\n") if error.backtrace
110
61
  end
111
62
  end
112
63
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Twiglet
4
- VERSION = '2.1.0'
4
+ VERSION = '2.2.3'
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) }
@@ -15,10 +16,10 @@ describe Twiglet::Logger do
15
16
  LEVELS = [
16
17
  { method: :debug, level: 'debug' },
17
18
  { method: :info, level: 'info' },
18
- { method: :warning, level: 'warning' },
19
- { method: :warn, level: 'warning' },
20
- { method: :critical, level: 'critical' },
21
- { method: :fatal, level: 'critical' },
19
+ { method: :warning, level: 'warn' },
20
+ { method: :warn, level: 'warn' },
21
+ { method: :critical, level: 'fatal' },
22
+ { method: :fatal, level: 'fatal' },
22
23
  { method: :error, level: 'error' }
23
24
  ].freeze
24
25
 
@@ -28,6 +29,13 @@ describe Twiglet::Logger do
28
29
  end
29
30
  end
30
31
 
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}"
36
+ end
37
+ end
38
+
31
39
  describe 'JSON logging' do
32
40
  it 'should throw an error with an empty message' do
33
41
  assert_raises RuntimeError do
@@ -123,6 +131,21 @@ describe Twiglet::Logger do
123
131
  assert_equal 'Guinea pigs arrived', log[:message]
124
132
  end
125
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
+
126
149
  it 'should be able to convert dotted keys to nested objects' do
127
150
  @logger.debug({
128
151
  "trace.id": '1c8a5fb2-fecd-44d8-92a4-449eb2ce4dcb',
@@ -193,6 +216,17 @@ describe Twiglet::Logger do
193
216
  assert_match 'logger_test.rb', actual_log[:error][:stack_trace].lines.first
194
217
  end
195
218
 
219
+ it 'should log an error without backtrace' do
220
+ e = StandardError.new('Connection timed-out')
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)
228
+ end
229
+
196
230
  LEVELS.each do |attrs|
197
231
  it "should correctly log level when calling #{attrs[:method]}" do
198
232
  @logger.public_send(attrs[:method], {message: 'a log message'})
@@ -247,6 +281,36 @@ describe Twiglet::Logger do
247
281
  end
248
282
  end
249
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
307
+ end
308
+
309
+ it 'initializes the logger with the provided level' do
310
+ assert_equal Logger::WARN, Twiglet::Logger.new('petshop', level: :warn).level
311
+ end
312
+ end
313
+
250
314
  private
251
315
 
252
316
  def read_json(buffer)
@@ -254,3 +318,4 @@ describe Twiglet::Logger do
254
318
  JSON.parse(buffer.read, symbolize_names: true)
255
319
  end
256
320
  end
321
+ # 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.1.0
4
+ version: 2.2.3
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-10 00:00:00.000000000 Z
11
+ date: 2020-06-29 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
@@ -55,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
56
  - !ruby/object:Gem::Version
56
57
  version: '0'
57
58
  requirements: []
58
- rubygems_version: 3.0.3
59
+ rubygems_version: 3.0.8
59
60
  signing_key:
60
61
  specification_version: 4
61
62
  summary: Twiglet
@@ -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