twiglet 2.1.1 → 2.3.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: 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