twiglet 2.1.1 → 2.3.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: 4f97187804f27d00e7b8231880b43c50769b70a1598dcb3eedb42d2afad4fe0c
4
- data.tar.gz: 18f126aef6c1b43bde8b4e343d2c7595e4fc3f1c07608e02214b3bf7ab2e36e4
3
+ metadata.gz: 1d0d7afeaf36dbb4b751acc81270a07f53c407419a71b2f98506641bb945eacc
4
+ data.tar.gz: '08c3562addfa95848f2e12faf90e541f7f0f5dd3a33d9dda2948197b57a36965'
5
5
  SHA512:
6
- metadata.gz: 2bf3104cbb1e9f4051f58d6969cf7a761f8aac44a50dfee5677ed52b8ddc0679692357b513b0c995f40fe29f9327f339c8491db326bff261e167e1e8bf6d61b1
7
- data.tar.gz: d1ee7cd4c20da132b3cfa8ca88ef117cc93bbb77773671274792cf67746f6b1947ff8c426099a2417ccbc291ff95853adaf1ded2be007f1deb85ebce265567ef
6
+ metadata.gz: 3513377b684ac2f788c31734fb229027710e2e1ceba8cc535ce49faa38076ddc90b0204851483c23a05261271b2c14c6d7ce5622be364d6097c69e073fe32c73
7
+ data.tar.gz: d65dda69ac427e922edd34213fbe0672266e272eb6135b39afba6bd3738464958f054cc8bf0d1382b4c2ef45771abd7c419ac1153a1133d010ec6923ad0e94ce
@@ -0,0 +1,26 @@
1
+ name: Publish Ruby Gem
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+
7
+ jobs:
8
+ build:
9
+ name: Build and Publish
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v2
13
+ - name: Set up Ruby 2.6
14
+ uses: actions/setup-ruby@v1
15
+ with:
16
+ version: 2.6.x
17
+ - name: Publish to RubyGems
18
+ run: |
19
+ mkdir -p $HOME/.gem
20
+ touch $HOME/.gem/credentials
21
+ chmod 0600 $HOME/.gem/credentials
22
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
23
+ gem build *.gemspec
24
+ gem push *.gem
25
+ env:
26
+ GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
@@ -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,44 +1,35 @@
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
34
  error_fields = {
44
35
  'error': {
@@ -46,69 +37,24 @@ module Twiglet
46
37
  }
47
38
  }
48
39
  add_stack_trace(error_fields, error)
49
- message = message.merge(error_fields)
40
+ message.is_a?(Hash) ? message.merge!(error_fields) : error_fields.merge!(message: message)
50
41
  end
51
42
 
52
- log(level: 'error', message: message)
53
- end
54
-
55
- def critical(message)
56
- log(level: 'critical', message: message)
43
+ super(message, &block)
57
44
  end
58
45
 
59
- alias_method :fatal, :critical
60
-
61
46
  def with(default_properties)
62
47
  Logger.new(@service_name,
63
48
  default_properties: default_properties,
64
49
  now: @now,
65
- output: @output)
66
- end
67
-
68
- private
69
-
70
- def log(level:, message:)
71
- case message
72
- when String
73
- log_text(level, message: message)
74
- when Hash
75
- log_object(level, message: message)
76
- else
77
- raise('Message must be String or Hash')
78
- end
79
- end
80
-
81
- def log_text(level, message:)
82
- raise('The \'message\' property of log object must not be empty') if message.strip.empty?
83
-
84
- message = { message: message }
85
- log_message(level, message: message)
50
+ output: @output,
51
+ level: @level)
86
52
  end
87
53
 
88
- def log_object(level, message:)
89
- message = message.transform_keys(&:to_sym)
90
- message.key?(:message) || raise('Log object must have a \'message\' property')
91
- message[:message].strip.empty? && raise('The \'message\' property of log object must not be empty')
54
+ alias_method :warning, :warn
55
+ alias_method :critical, :fatal
92
56
 
93
- log_message(level, message: message)
94
- end
95
-
96
- def log_message(level, message:)
97
- base_message = {
98
- "@timestamp": @now.call.iso8601(3),
99
- service: {
100
- name: @service_name
101
- },
102
- log: {
103
- level: level
104
- }
105
- }
106
-
107
- @output.puts base_message
108
- .deep_merge(@default_properties.to_nested)
109
- .deep_merge(message.to_nested)
110
- .to_json
111
- end
57
+ private
112
58
 
113
59
  def add_stack_trace(hash_to_add_to, error)
114
60
  hash_to_add_to[:error][:stack_trace] = error.backtrace.join("\n") if error.backtrace
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Twiglet
4
- VERSION = '2.1.1'
4
+ VERSION = '2.3.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',
@@ -258,6 +281,36 @@ describe Twiglet::Logger do
258
281
  end
259
282
  end
260
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
+
261
314
  private
262
315
 
263
316
  def read_json(buffer)
@@ -265,3 +318,4 @@ describe Twiglet::Logger do
265
318
  JSON.parse(buffer.read, symbolize_names: true)
266
319
  end
267
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.1
4
+ version: 2.3.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-11 00:00:00.000000000 Z
11
+ date: 2020-07-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Like a log, only smaller.
14
14
  email:
@@ -18,21 +18,23 @@ extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
20
  - ".github/CODEOWNERS"
21
+ - ".github/workflows/gem-publish.yml"
21
22
  - ".github/workflows/ruby.yml"
22
23
  - ".gitignore"
23
24
  - ".rubocop.yml"
24
25
  - ".ruby-version"
25
26
  - CODE_OF_CONDUCT.md
26
27
  - Gemfile
27
- - Gemfile.lock
28
28
  - LICENSE
29
29
  - RATIONALE.md
30
30
  - README.md
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
@@ -55,7 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
55
57
  - !ruby/object:Gem::Version
56
58
  version: '0'
57
59
  requirements: []
58
- rubygems_version: 3.1.2
60
+ rubygems_version: 3.0.3
59
61
  signing_key:
60
62
  specification_version: 4
61
63
  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