twiglet 2.1.0 → 2.2.3

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: 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