loga 2.8.1 → 2.9.0

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build-and-test.yml +4 -4
  3. data/.github/workflows/publish-gem.yml +1 -1
  4. data/.rubocop.yml +6 -11
  5. data/.rubocop_todo.yml +67 -8
  6. data/Appraisals +73 -24
  7. data/CHANGELOG.md +8 -0
  8. data/Gemfile +10 -8
  9. data/README.md +1 -1
  10. data/gemfiles/rails60.gemfile +10 -8
  11. data/gemfiles/rails61.gemfile +10 -8
  12. data/gemfiles/rails70.gemfile +11 -8
  13. data/gemfiles/rails71.gemfile +11 -8
  14. data/gemfiles/rails72.gemfile +38 -0
  15. data/gemfiles/rails80.gemfile +38 -0
  16. data/gemfiles/sidekiq51.gemfile +11 -8
  17. data/gemfiles/sidekiq60.gemfile +11 -8
  18. data/gemfiles/sidekiq61.gemfile +11 -8
  19. data/gemfiles/sidekiq62.gemfile +11 -8
  20. data/gemfiles/sidekiq63.gemfile +11 -8
  21. data/gemfiles/sidekiq64.gemfile +11 -8
  22. data/gemfiles/sidekiq65.gemfile +11 -8
  23. data/gemfiles/sidekiq70.gemfile +11 -8
  24. data/gemfiles/sidekiq71.gemfile +11 -8
  25. data/gemfiles/{sidekiq7.gemfile → sidekiq72.gemfile} +12 -9
  26. data/gemfiles/sidekiq73.gemfile +36 -0
  27. data/gemfiles/sidekiq80.gemfile +36 -0
  28. data/gemfiles/sinatra14.gemfile +11 -8
  29. data/gemfiles/sinatra2.gemfile +11 -8
  30. data/gemfiles/sinatra3.gemfile +11 -8
  31. data/gemfiles/sinatra4.gemfile +11 -8
  32. data/gemfiles/unit.gemfile +10 -8
  33. data/lib/loga/parameter_filter.rb +0 -2
  34. data/lib/loga/rack/request.rb +2 -2
  35. data/lib/loga/rack/request_id.rb +2 -2
  36. data/lib/loga/railtie.rb +1 -1
  37. data/lib/loga/sidekiq.rb +11 -0
  38. data/lib/loga/sidekiq8/job_logger.rb +13 -0
  39. data/lib/loga/tagged_logging.rb +2 -2
  40. data/lib/loga/version.rb +1 -1
  41. data/spec/fixtures/rails71.rb +1 -1
  42. data/spec/fixtures/rails72.rb +80 -0
  43. data/spec/fixtures/rails80.rb +80 -0
  44. data/spec/integration/sidekiq8_spec.rb +193 -0
  45. data/spec/integration/sinatra_spec.rb +10 -3
  46. data/spec/loga/sidekiq_latest/job_logger_spec.rb +138 -0
  47. data/spec/loga/sidekiq_spec.rb +15 -3
  48. data/spec/spec_helper.rb +8 -2
  49. data/spec/support/helpers.rb +8 -0
  50. data/spec/support/request_spec.rb +8 -1
  51. data/spec/unit/loga/formatters/gelf_formatter_spec.rb +7 -1
  52. data/spec/unit/loga/formatters/simple_formatter_spec.rb +20 -5
  53. metadata +17 -9
  54. data/spec/loga/sidekiq7/job_logger_spec.rb +0 -127
@@ -4,24 +4,26 @@ source "https://rubygems.org"
4
4
 
5
5
  group :development do
6
6
  gem "appraisal"
7
+ gem "benchmark"
8
+ gem "bigdecimal"
7
9
  gem "bundler", ">= 1.6"
8
10
  gem "byebug"
9
- gem "guard"
11
+ gem "fakeredis"
10
12
  gem "guard-rspec"
11
13
  gem "guard-rubocop"
14
+ gem "guard"
15
+ gem "net-imap"
16
+ gem "net-pop"
17
+ gem "net-smtp"
18
+ gem "ostruct"
12
19
  gem "pry"
20
+ gem "psych"
13
21
  gem "rack-test"
14
22
  gem "rake"
15
- gem "fakeredis"
16
23
  gem "rspec", "~> 3.7"
17
- gem "rubocop"
18
24
  gem "rubocop-rspec"
25
+ gem "rubocop"
19
26
  gem "timecop"
20
- gem "psych"
21
- gem "net-smtp"
22
- gem "net-pop"
23
- gem "net-imap"
24
- gem "bigdecimal"
25
27
  end
26
28
 
27
29
  group :test do
@@ -20,8 +20,6 @@ module Loga
20
20
 
21
21
  class CompiledFilter
22
22
  def self.compile(filters)
23
- ->(params) { params.dup } if filters.empty?
24
-
25
23
  regexps = []
26
24
  strings = []
27
25
 
@@ -34,12 +34,12 @@ module Loga
34
34
  env['loga.request.original_path']
35
35
  end
36
36
 
37
- # rubocop:disable Metrics/LineLength
37
+ # rubocop:disable Layout/LineLength
38
38
  def filtered_full_path
39
39
  @filtered_full_path ||=
40
40
  query_string.empty? ? original_path : "#{original_path}?#{filtered_query_string}"
41
41
  end
42
- # rubocop:enable Metrics/LineLength
42
+ # rubocop:enable Layout/LineLength
43
43
 
44
44
  def filtered_parameters
45
45
  @filtered_parameters ||= filtered_query_hash.merge(filtered_form_hash)
@@ -3,7 +3,7 @@ require 'securerandom'
3
3
  require 'active_support/core_ext/string/access'
4
4
  require 'active_support/core_ext/object/blank'
5
5
 
6
- # rubocop:disable Lint/AssignmentInCondition, Metrics/LineLength, Style/GuardClause
6
+ # rubocop:disable Lint/AssignmentInCondition, Layout/LineLength, Style/GuardClause
7
7
  module Loga
8
8
  module Rack
9
9
  # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
@@ -41,4 +41,4 @@ module Loga
41
41
  end
42
42
  end
43
43
  end
44
- # rubocop:enable Lint/AssignmentInCondition, Metrics/LineLength, Style/GuardClause
44
+ # rubocop:enable Lint/AssignmentInCondition, Layout/LineLength, Style/GuardClause
data/lib/loga/railtie.rb CHANGED
@@ -125,7 +125,7 @@ module Loga
125
125
  def silence_rails_rack_logger
126
126
  case Rails::VERSION::MAJOR
127
127
  when 3 then require 'loga/ext/rails/rack/logger3.rb'
128
- when 4..7 then require 'loga/ext/rails/rack/logger.rb'
128
+ when 4..8 then require 'loga/ext/rails/rack/logger.rb'
129
129
  else
130
130
  raise Loga::ConfigurationError,
131
131
  "Rails #{Rails::VERSION::MAJOR} is unsupported"
data/lib/loga/sidekiq.rb CHANGED
@@ -12,6 +12,8 @@ module Loga
12
12
  configure_for_sidekiq6
13
13
  elsif Gem::Version.new(::Sidekiq::VERSION) < Gem::Version.new('8.0')
14
14
  configure_for_sidekiq7
15
+ elsif Gem::Version.new(::Sidekiq::VERSION) < Gem::Version.new('9.0')
16
+ configure_for_sidekiq8
15
17
  end
16
18
  end
17
19
 
@@ -43,5 +45,14 @@ module Loga
43
45
  config.logger = Loga.configuration.logger
44
46
  end
45
47
  end
48
+
49
+ def self.configure_for_sidekiq8
50
+ require 'loga/sidekiq8/job_logger'
51
+
52
+ ::Sidekiq.configure_server do |config|
53
+ config[:job_logger] = Loga::Sidekiq8::JobLogger
54
+ config.logger = Loga.configuration.logger
55
+ end
56
+ end
46
57
  end
47
58
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # If a future Sidekiq 8.x release introduces breaking changes that require a
4
+ # divergent implementation, replace this constant assignment with a dedicated
5
+ # class (you can copy the Sidekiq7 implementation as a starting point).
6
+
7
+ require 'loga/sidekiq7/job_logger'
8
+
9
+ module Loga
10
+ module Sidekiq8
11
+ JobLogger = Class.new(Loga::Sidekiq7::JobLogger)
12
+ end
13
+ end
@@ -8,7 +8,7 @@ require 'active_support/core_ext/object/blank'
8
8
  require 'logger'
9
9
 
10
10
  module Loga
11
- # rubocop:disable Metrics/LineLength
11
+ # rubocop:disable Layout/LineLength
12
12
  # Wraps any standard Logger object to provide tagging capabilities.
13
13
  #
14
14
  # logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
@@ -19,7 +19,7 @@ module Loga
19
19
  # This is used by the default Rails.logger as configured by Railties to make
20
20
  # it easy to stamp log lines with subdomains, request ids, and anything else
21
21
  # to aid debugging of multi-user production applications.
22
- # rubocop:enable Metrics/LineLength
22
+ # rubocop:enable Layout/LineLength
23
23
  module TaggedLogging
24
24
  module Formatter # :nodoc:
25
25
  def tagged(*tags)
data/lib/loga/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Loga
2
- VERSION = '2.8.1'.freeze
2
+ VERSION = '2.9.0'.freeze
3
3
  end
@@ -9,7 +9,7 @@ class Dummy < Rails::Application
9
9
  config.eager_load = true
10
10
  config.filter_parameters += [:password]
11
11
  config.secret_key_base = '2624599ca9ab3cf3823626240138a128118a87683bf03ab8f155844c33b3cd8cbbfa3ef5e29db6f5bd182f8bd4776209d9577cfb46ac51bfd232b00ab0136b24'
12
- config.session_store :cookie_store, key: '_rails70_session'
12
+ config.session_store :cookie_store, key: '_rails71_session'
13
13
 
14
14
  config.log_tags = [:uuid, 'TEST_TAG']
15
15
  config.loga = {
@@ -0,0 +1,80 @@
1
+ require 'action_controller/railtie'
2
+ require 'action_mailer/railtie'
3
+
4
+ Bundler.require(*Rails.groups)
5
+
6
+ STREAM = StringIO.new unless defined?(STREAM)
7
+
8
+ class Dummy < Rails::Application
9
+ config.eager_load = true
10
+ config.filter_parameters += [:password]
11
+ config.secret_key_base = '2624599ca9ab3cf3823626240138a128118a87683bf03ab8f155844c33b3cd8cbbfa3ef5e29db6f5bd182f8bd4776209d9577cfb46ac51bfd232b00ab0136b24'
12
+ config.session_store :cookie_store, key: '_rails72_session'
13
+
14
+ config.log_tags = [:uuid, 'TEST_TAG']
15
+ config.loga = {
16
+ device: STREAM,
17
+ host: 'bird.example.com',
18
+ service_name: 'hello_world_app',
19
+ service_version: '1.0',
20
+ }
21
+ config.action_mailer.delivery_method = :test
22
+ end
23
+
24
+ class ApplicationController < ActionController::Base
25
+ include Rails.application.routes.url_helpers
26
+ protect_from_forgery with: :null_session
27
+
28
+ def ok
29
+ render plain: 'Hello Rails'
30
+ end
31
+
32
+ def error
33
+ nil.name
34
+ end
35
+
36
+ def show
37
+ render json: params
38
+ end
39
+
40
+ def create
41
+ render json: params
42
+ end
43
+
44
+ def new
45
+ redirect_to :ok
46
+ end
47
+
48
+ def update
49
+ @id = params[:id]
50
+ render '/user'
51
+ end
52
+ end
53
+
54
+ class FakeMailer < ActionMailer::Base
55
+ default from: 'notifications@example.com'
56
+
57
+ def self.send_email
58
+ basic_mail.deliver_now
59
+ end
60
+
61
+ def basic_mail
62
+ mail(
63
+ to: 'user@example.com',
64
+ subject: 'Welcome to My Awesome Site',
65
+ body: 'Banana muffin',
66
+ content_type: 'text/html',
67
+ )
68
+ end
69
+ end
70
+
71
+ Dummy.routes.append do
72
+ get 'ok' => 'application#ok'
73
+ get 'error' => 'application#error'
74
+ get 'show' => 'application#show'
75
+ post 'users' => 'application#create'
76
+ get 'new' => 'application#new'
77
+ put 'users/:id' => 'application#update'
78
+ end
79
+
80
+ Dummy.initialize!
@@ -0,0 +1,80 @@
1
+ require 'action_controller/railtie'
2
+ require 'action_mailer/railtie'
3
+
4
+ Bundler.require(*Rails.groups)
5
+
6
+ STREAM = StringIO.new unless defined?(STREAM)
7
+
8
+ class Dummy < Rails::Application
9
+ config.eager_load = true
10
+ config.filter_parameters += [:password]
11
+ config.secret_key_base = '2624599ca9ab3cf3823626240138a128118a87683bf03ab8f155844c33b3cd8cbbfa3ef5e29db6f5bd182f8bd4776209d9577cfb46ac51bfd232b00ab0136b24'
12
+ config.session_store :cookie_store, key: '_rails80_session'
13
+
14
+ config.log_tags = [:uuid, 'TEST_TAG']
15
+ config.loga = {
16
+ device: STREAM,
17
+ host: 'bird.example.com',
18
+ service_name: 'hello_world_app',
19
+ service_version: '1.0',
20
+ }
21
+ config.action_mailer.delivery_method = :test
22
+ end
23
+
24
+ class ApplicationController < ActionController::Base
25
+ include Rails.application.routes.url_helpers
26
+ protect_from_forgery with: :null_session
27
+
28
+ def ok
29
+ render plain: 'Hello Rails'
30
+ end
31
+
32
+ def error
33
+ nil.name
34
+ end
35
+
36
+ def show
37
+ render json: params
38
+ end
39
+
40
+ def create
41
+ render json: params
42
+ end
43
+
44
+ def new
45
+ redirect_to :ok
46
+ end
47
+
48
+ def update
49
+ @id = params[:id]
50
+ render '/user'
51
+ end
52
+ end
53
+
54
+ class FakeMailer < ActionMailer::Base
55
+ default from: 'notifications@example.com'
56
+
57
+ def self.send_email
58
+ basic_mail.deliver_now
59
+ end
60
+
61
+ def basic_mail
62
+ mail(
63
+ to: 'user@example.com',
64
+ subject: 'Welcome to My Awesome Site',
65
+ body: 'Banana muffin',
66
+ content_type: 'text/html',
67
+ )
68
+ end
69
+ end
70
+
71
+ Dummy.routes.append do
72
+ get 'ok' => 'application#ok'
73
+ get 'error' => 'application#error'
74
+ get 'show' => 'application#show'
75
+ post 'users' => 'application#create'
76
+ get 'new' => 'application#new'
77
+ put 'users/:id' => 'application#update'
78
+ end
79
+
80
+ Dummy.initialize!
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Sidekiq client logger' do
6
+ let(:target) { StringIO.new }
7
+ let(:config) { Sidekiq.instance_variable_get :@config }
8
+
9
+ before do
10
+ Sidekiq.configure_server do |config|
11
+ config.redis = { pool_name: :default }
12
+ end
13
+
14
+ Loga.reset
15
+
16
+ Loga.configure(
17
+ service_name: 'hello_world_app',
18
+ service_version: '1.0',
19
+ device: target,
20
+ format: :gelf,
21
+ )
22
+ end
23
+
24
+ it 'has the proper job logger' do
25
+ expect(config[:job_logger]).to eq Loga::Sidekiq8::JobLogger
26
+ end
27
+
28
+ it 'has the proper logger for Sidekiq.logger' do
29
+ expect(Sidekiq.logger).to eq Loga.logger
30
+ end
31
+
32
+ context 'with processor' do
33
+ require 'sidekiq/processor'
34
+
35
+ let(:mutex) { Mutex.new }
36
+ let(:cond) { ConditionVariable.new }
37
+ let(:processor) do
38
+ Sidekiq::Processor.new(config.default_capsule) { |pr, ex| result(pr, ex) }
39
+ end
40
+
41
+ before do
42
+ @exception = nil
43
+ end
44
+
45
+ context 'with a successful job' do
46
+ before do
47
+ MySidekiqWorker.perform_async('Bob')
48
+
49
+ await { processor.start }
50
+
51
+ processor.terminate(true)
52
+ end
53
+
54
+ it 'logs the job processing event' do
55
+ test_log_from_worker(read_json_log(line: -2))
56
+ end
57
+
58
+ it 'logs the "done" event' do
59
+ test_job_end_log(read_json_log(line: -1))
60
+ end
61
+ end
62
+
63
+ context 'with an error' do
64
+ before do
65
+ MySidekiqWorker.perform_async('Boom')
66
+
67
+ await { processor.start }
68
+
69
+ processor.terminate(true)
70
+ end
71
+
72
+ it 'logs the "error" event' do
73
+ test_job_fail_log(read_json_log(line: 0))
74
+ end
75
+
76
+ it 're-throws the error' do
77
+ # rubocop:disable RSpec/InstanceVariable
78
+ expect(@exception.message).to eq('Boom')
79
+ # rubocop:enable RSpec/InstanceVariable
80
+ end
81
+ end
82
+
83
+ def result(_processor, exception)
84
+ @exception = exception
85
+ mutex.synchronize { cond.signal }
86
+ end
87
+
88
+ def await(timeout: 0.1)
89
+ mutex.synchronize do
90
+ yield
91
+ cond.wait(mutex, timeout)
92
+ end
93
+ end
94
+
95
+ def common_log_fields
96
+ {
97
+ '_class' => 'MySidekiqWorker',
98
+ '_service.name' => 'hello_world_app',
99
+ '_service.version' => '1.0',
100
+ '_tags' => '',
101
+ 'version' => '1.1',
102
+ }.freeze
103
+ end
104
+
105
+ def job_logger_common_fields
106
+ common_log_fields.merge(
107
+ '_queue' => 'default',
108
+ '_retry' => true,
109
+ '_type' => 'sidekiq',
110
+ )
111
+ end
112
+
113
+ def test_log_from_worker(json_line)
114
+ aggregate_failures do
115
+ expect(json_line).to include(
116
+ common_log_fields.merge(
117
+ 'level' => 6,
118
+ 'short_message' => 'Hello from MySidekiqWorker',
119
+ ),
120
+ )
121
+
122
+ %w[_jid timestamp host].each do |key|
123
+ expect(json_line).to have_key(key)
124
+ end
125
+
126
+ expect(json_line).not_to include('_duration')
127
+ end
128
+ end
129
+
130
+ def test_job_end_log(json_line)
131
+ aggregate_failures do
132
+ expect(json_line).to include(
133
+ job_logger_common_fields.merge(
134
+ '_params' => ['Bob'],
135
+ 'level' => 6,
136
+ ),
137
+ )
138
+
139
+ %w[_created_at _enqueued_at _jid _duration timestamp host].each do |key|
140
+ expect(json_line).to have_key(key)
141
+ end
142
+
143
+ expect(json_line['_duration']).to be < 500
144
+ expect(json_line['short_message'])
145
+ .to match(/MySidekiqWorker with jid: '\w+' done/)
146
+ end
147
+ end
148
+
149
+ def test_job_fail_log(json_line)
150
+ aggregate_failures do
151
+ expect(json_line).to include(
152
+ job_logger_common_fields.merge(
153
+ '_params' => ['Boom'],
154
+ 'level' => 4,
155
+ ),
156
+ )
157
+
158
+ %w[_created_at _enqueued_at _jid _duration timestamp host].each do |key|
159
+ expect(json_line).to have_key(key)
160
+ end
161
+
162
+ expect(json_line['_duration']).to be < 500
163
+ expect(json_line['short_message'])
164
+ .to match(/MySidekiqWorker with jid: '\w+' fail/)
165
+ end
166
+ end
167
+ end
168
+
169
+ def dump_log
170
+ offset = target.pos
171
+
172
+ target.rewind
173
+ target.each_line { puts _1 }
174
+
175
+ target.pos = offset
176
+ end
177
+
178
+ def read_json_log(line:)
179
+ target.rewind
180
+
181
+ JSON.parse(target.readlines[line])
182
+ end
183
+ end
184
+
185
+ class MySidekiqWorker
186
+ include Sidekiq::Job
187
+
188
+ def perform(name)
189
+ raise name if name == 'Boom'
190
+
191
+ logger.info('Hello from MySidekiqWorker')
192
+ end
193
+ end
@@ -61,7 +61,7 @@ RSpec.describe 'Structured logging with Sinatra', :timecop, :with_hostname do
61
61
  end
62
62
  end
63
63
 
64
- # rubocop:disable Metrics/LineLength
64
+ # rubocop:disable Layout/LineLength
65
65
  context 'when RACK_ENV is development', if: ENV['RACK_ENV'].eql?('development') do
66
66
  let(:format) { :simple }
67
67
  let(:last_log_entry) do
@@ -122,9 +122,16 @@ RSpec.describe 'Structured logging with Sinatra', :timecop, :with_hostname do
122
122
  it 'logs the request with the exception' do
123
123
  get '/error', {}, 'HTTP_X_REQUEST_ID' => '700a6a01'
124
124
 
125
- expect(last_log_entry).to start_with("E, #{time_pid_tags} GET /error 500 in 0ms type=request #{data_as_text} exception=undefined method `name' for nil")
125
+ exception_line =
126
+ if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('3.4.0dev')
127
+ "E, #{time_pid_tags} GET /error 500 in 0ms type=request #{data_as_text} exception=undefined method 'name' for nil"
128
+ else
129
+ "E, #{time_pid_tags} GET /error 500 in 0ms type=request #{data_as_text} exception=undefined method `name' for nil"
130
+ end
131
+
132
+ expect(last_log_entry).to start_with(exception_line)
126
133
  end
127
134
  end
128
135
  end
129
- # rubocop:enable Metrics/LineLength
136
+ # rubocop:enable Layout/LineLength
130
137
  end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'loga/sidekiq7/job_logger'
5
+ require 'loga/sidekiq8/job_logger'
6
+
7
+ logger_klass =
8
+ if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('8.0.0')
9
+ Loga::Sidekiq8::JobLogger
10
+ elsif Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('7.0.0')
11
+ Loga::Sidekiq7::JobLogger
12
+ else
13
+ raise 'No suitable Sidekiq JobLogger implementation found'
14
+ end
15
+
16
+ RSpec.describe logger_klass, 'unified (latest) Sidekiq JobLogger spec' do
17
+ # Sidekiq < 7.3: initializer expects a raw logger
18
+ # Sidekiq >= 7.3: initializer expects a config object responding to #logger and #[] (Sidekiq 8 keeps this)
19
+ subject(:job_logger) do
20
+ if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('7.3.0')
21
+ described_class.new(sidekiq_config)
22
+ else
23
+ described_class.new(logger)
24
+ end
25
+ end
26
+
27
+ let(:target) { StringIO.new }
28
+
29
+ let(:json_line) do
30
+ target.rewind
31
+ raw = target.read
32
+ line = raw.split("\n").last
33
+ JSON.parse(line) if line
34
+ end
35
+
36
+ let(:logger) { Loga.logger }
37
+ let(:sidekiq_config) { instance_double(Sidekiq::Config, logger: logger, :[] => nil) }
38
+
39
+ before do
40
+ Loga.reset
41
+ Loga.configure(
42
+ service_name: 'hello_world_app',
43
+ service_version: '1.0',
44
+ device: target,
45
+ format: :gelf,
46
+ )
47
+ end
48
+
49
+ it 'inherits from ::Sidekiq::JobLogger' do
50
+ expect(job_logger).to be_a(Sidekiq::JobLogger)
51
+ end
52
+
53
+ describe '#call' do
54
+ let(:item_data) do
55
+ {
56
+ 'class' => 'HardWorker',
57
+ 'args' => ['asd'],
58
+ 'retry' => true,
59
+ 'queue' => 'default',
60
+ 'jid' => '591f6f66ee0d218fb451dfb6',
61
+ 'created_at' => 1_528_799_582.904939,
62
+ 'enqueued_at' => 1_528_799_582.9049861,
63
+ }
64
+ end
65
+
66
+ context 'when the job passes successfully' do
67
+ it 'logs the expected structured event' do
68
+ job_logger.call(item_data, 'default') do
69
+ # simulate job body
70
+ end
71
+
72
+ expected = {
73
+ 'version' => '1.1',
74
+ 'level' => 6, # INFO -> 6 (syslog mapping)
75
+ '_type' => 'sidekiq',
76
+ '_created_at' => item_data['created_at'],
77
+ '_enqueued_at' => item_data['enqueued_at'],
78
+ '_jid' => item_data['jid'],
79
+ '_retry' => true,
80
+ '_queue' => 'default',
81
+ '_service.name' => 'hello_world_app',
82
+ '_service.version' => '1.0',
83
+ '_class' => 'HardWorker',
84
+ '_params' => ['asd'],
85
+ '_tags' => '',
86
+ }
87
+
88
+ aggregate_failures do
89
+ expect(json_line).to include(expected)
90
+ expect(json_line['_duration']).to be_a(Float).or be_a(Integer)
91
+ expect(json_line['timestamp']).to be_a(Float)
92
+ expect(json_line['host']).to be_a(String)
93
+ expect(json_line['short_message'])
94
+ .to match(/HardWorker with jid: '591f6f66ee0d218fb451dfb6' (done|executed)/)
95
+ end
96
+ end
97
+ end
98
+
99
+ context 'when the job fails' do
100
+ it 'logs the expected structured event with exception info' do
101
+ failing = lambda do
102
+ job_logger.call(item_data, 'default') do
103
+ raise StandardError, 'Boom'
104
+ end
105
+ end
106
+
107
+ expect(&failing).to raise_error(StandardError, 'Boom')
108
+
109
+ expected = {
110
+ 'version' => '1.1',
111
+ # WARN -> syslog 4 (sidekiq job failure logged at warn)
112
+ 'level' => 4,
113
+ '_type' => 'sidekiq',
114
+ '_created_at' => item_data['created_at'],
115
+ '_enqueued_at' => item_data['enqueued_at'],
116
+ '_jid' => item_data['jid'],
117
+ '_retry' => true,
118
+ '_queue' => 'default',
119
+ '_service.name' => 'hello_world_app',
120
+ '_service.version' => '1.0',
121
+ '_class' => 'HardWorker',
122
+ '_params' => ['asd'],
123
+ '_tags' => '',
124
+ '_exception' => 'Boom',
125
+ }
126
+
127
+ aggregate_failures do
128
+ expect(json_line).to include(expected)
129
+ expect(json_line['_duration']).to be_a(Float).or be_a(Integer)
130
+ expect(json_line['timestamp']).to be_a(Float)
131
+ expect(json_line['host']).to be_a(String)
132
+ expect(json_line['short_message'])
133
+ .to match(/HardWorker with jid: '591f6f66ee0d218fb451dfb6' fail/)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end