journaled 4.2.0 → 4.3.0

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: bfdabb8b24bdfdf23c4f77ec819f03e89a4a0239ed997368eb1f07ac0bc2cd5b
4
- data.tar.gz: 97eeec1dfa22f7b683f23e322eb663e6fae4b47e0c2786690c4f44166b8cca51
3
+ metadata.gz: dc6d5adec8f9ee32ba34d1ba8dae8cb3da062e39dd1b67d5fc3e219be116d675
4
+ data.tar.gz: ae77057a26203e90fbb8770e1b41864edcff69058cd80b65011eb414ddf2365a
5
5
  SHA512:
6
- metadata.gz: 78c8b888f9c00a084e8d60c2f271d56a9a188f60287cb30edfbadc7a4f9fc1c3fc3210c763e757658a0040fb57f6f94f1a713fc1f95942db801e2e114dd50ec3
7
- data.tar.gz: f0e56609680ecfc3e1f3f3a9d2ba801515762311307f0a208757a70a82ff7c37ba6af040f8ee5a64ebbf7a08a910579c27d65e940680b2406383b8eca022f323
6
+ metadata.gz: 05a99d3d5d530bca784fb44fa290d0ff661430263fbde8fa882f8c98a9dda2eabe76c82df142b6477e68439066cba52d66d875c035a7af6e9d6a2938c8985b82
7
+ data.tar.gz: 7726aa5152e545cdba1a50ef475f8df0ce37fb97edc2d4fa146c74f73b41c7096434948afeecbe61bc042138cae9c3b94dda475adbf9c74d42892b1d6286e069
data/README.md CHANGED
@@ -179,52 +179,6 @@ journaling. Note that the less-frequently-used methods `toggle`,
179
179
  `increment*`, `decrement*`, and `update_counters` are not intercepted at
180
180
  this time.
181
181
 
182
- ### Tagged Events
183
-
184
- Events may be optionally marked as "tagged." This will add a `tags` field, intended for tracing and
185
- auditing purposes.
186
-
187
- ```ruby
188
- class MyEvent
189
- include Journaled::Event
190
-
191
- journal_attributes :attr_1, :attr_2, tagged: true
192
- end
193
- ```
194
-
195
- You may then use `Journaled.tag!` and `Journaled.tagged` inside of your
196
- `ApplicationController` and `ApplicationJob` classes (or anywhere else!) to tag
197
- all events with request and job metadata:
198
-
199
- ```ruby
200
- class ApplicationController < ActionController::Base
201
- before_action do
202
- Journaled.tag!(request_id: request.request_id, current_user_id: current_user&.id)
203
- end
204
- end
205
-
206
- class ApplicationJob < ActiveJob::Base
207
- around_perform do |job, perform|
208
- Journaled.tagged(job_id: job.id) { perform.call }
209
- end
210
- end
211
- ```
212
-
213
- This feature relies on `ActiveSupport::CurrentAttributes` under the hood, so these tags are local to
214
- the current thread, and will be cleared at the end of each request request/job.
215
-
216
- #### Testing
217
-
218
- If you use RSpec (and have required `journaled/rspec` in your
219
- `spec/rails_helper.rb`), you can regression-protect important journaling
220
- config with the `journal_changes_to` matcher:
221
-
222
- ```ruby
223
- it "journals exactly these things or there will be heck to pay" do
224
- expect(User).to journal_changes_to(:email, :first_name, :last_name, as: :identity_change)
225
- end
226
- ```
227
-
228
182
  ### Custom Journaling
229
183
 
230
184
  For every custom implementation of journaling in your application, define the JSON schema for the attributes in your event.
@@ -309,6 +263,40 @@ An event like the following will be journaled to kinesis:
309
263
  }
310
264
  ```
311
265
 
266
+ ### Tagged Events
267
+
268
+ Events may be optionally marked as "tagged." This will add a `tags` field, intended for tracing and
269
+ auditing purposes.
270
+
271
+ ```ruby
272
+ class MyEvent
273
+ include Journaled::Event
274
+
275
+ journal_attributes :attr_1, :attr_2, tagged: true
276
+ end
277
+ ```
278
+
279
+ You may then use `Journaled.tag!` and `Journaled.tagged` inside of your
280
+ `ApplicationController` and `ApplicationJob` classes (or anywhere else!) to tag
281
+ all events with request and job metadata:
282
+
283
+ ```ruby
284
+ class ApplicationController < ActionController::Base
285
+ before_action do
286
+ Journaled.tag!(request_id: request.request_id, current_user_id: current_user&.id)
287
+ end
288
+ end
289
+
290
+ class ApplicationJob < ActiveJob::Base
291
+ around_perform do |job, perform|
292
+ Journaled.tagged(job_id: job.id) { perform.call }
293
+ end
294
+ end
295
+ ```
296
+
297
+ This feature relies on `ActiveSupport::CurrentAttributes` under the hood, so these tags are local to
298
+ the current thread, and will be cleared at the end of each request request/job.
299
+
312
300
  ### Helper methods for custom events
313
301
 
314
302
  Journaled provides a couple helper methods that may be useful in your
@@ -352,6 +340,102 @@ Returns one of the following in order of preference:
352
340
  In order for this to be most useful, you must configure your controller
353
341
  as described in [Change Journaling](#change-journaling) above.
354
342
 
343
+ ### Testing
344
+
345
+ If you use RSpec, you can test for journaling behaviors with the
346
+ `journal_event(s)_including` and `journal_changes_to` matchers. First, make
347
+ sure to require `journaled/rspec` in your spec setup (e.g.
348
+ `spec/rails_helper.rb`):
349
+
350
+ ```ruby
351
+ require 'journaled/rspec'
352
+ ```
353
+
354
+ #### Checking for specific events
355
+
356
+ The `journal_event_including` and `journal_events_including` matchers allow you
357
+ to check for one or more matching event being journaled:
358
+
359
+ ```ruby
360
+ expect { my_code }
361
+ .to journal_event_including(name: 'foo')
362
+ expect { my_code }
363
+ .to journal_events_including({ name: 'foo', value: 1 }, { name: 'foo', value: 2 })
364
+ ```
365
+
366
+ This will only perform matches on the specified fields (and will not match one
367
+ way or the other against unspecified fields). These matchers will also ignore
368
+ any extraneous events that are not positively matched (as they may be unrelated
369
+ to behavior under test).
370
+
371
+ When writing tests, pairing every positive assertion with a negative assertion
372
+ is a good practice, and so negative matching is also supported (via both
373
+ `.not_to` and `.to not_`):
374
+
375
+ ```ruby
376
+ expect { my_code }
377
+ .not_to journal_events_including({ name: 'foo' }, { name: 'bar' })
378
+ expect { my_code }
379
+ .to raise_error(SomeError)
380
+ .and not_journal_event_including(name: 'foo') # the `not_` variant can chain off of `.and`
381
+ ```
382
+
383
+ Several chainable modifiers are also available:
384
+
385
+ ```ruby
386
+ expect { my_code }.to journal_event_including(name: 'foo')
387
+ .with_schema_name('my_event_schema')
388
+ .with_partition_key(user.id)
389
+ .with_stream_name('my_stream_name')
390
+ .with_enqueue_opts(run_at: future_time)
391
+ .with_priority(999)
392
+ ```
393
+
394
+ All of this can be chained together to test for multiple sets of events with
395
+ multiple sets of options:
396
+
397
+ ```ruby
398
+ expect { subject.journal! }
399
+ .to journal_events_including({ name: 'event1', value: 300 }, { name: 'event2', value: 200 })
400
+ .with_priority(10)
401
+ .and journal_event_including(name: 'event3', value: 100)
402
+ .with_priority(20)
403
+ .and not_journal_event_including(name: 'other_event')
404
+ ```
405
+
406
+ #### Checking for `Journaled::Changes` declarations
407
+
408
+ The `journal_changes_to` matcher checks against the list of attributes specified
409
+ on the model. It does not actually test that an event is emitted within a given
410
+ codepath, and is instead intended to guard against accidental regressions that
411
+ may impact external consumers of these events:
412
+
413
+ ```ruby
414
+ it "journals exactly these things or there will be heck to pay" do
415
+ expect(User).to journal_changes_to(:email, :first_name, :last_name, as: :identity_change)
416
+ end
417
+ ```
418
+
419
+ ### Instrumentation
420
+
421
+ When an event is enqueued, an `ActiveSupport::Notification` titled
422
+ `journaled.event.enqueue` is emitted. Its payload will include the `:event` and
423
+ its background job `:priority`.
424
+
425
+ This can be forwarded along to your preferred monitoring solution via a Rails
426
+ initializer:
427
+
428
+ ```ruby
429
+ ActiveSupport::Notifications.subscribe('journaled.event.enqueue') do |*args|
430
+ payload = ActiveSupport::Notifications::Event.new(*args).payload
431
+ journaled_event = payload[:event]
432
+
433
+ tags = { priority: payload[:priority], event_type: journaled_event.journaled_attributes[:event_type] }
434
+
435
+ Statsd.increment('journaled.event.enqueue', tags: tags.map { |k,v| "#{k.to_s[0..64]}:#{v.to_s[0..255]}" })
436
+ end
437
+ ```
438
+
355
439
  ## Upgrades
356
440
 
357
441
  Since this gem relies on background jobs (which can remain in the queue across
@@ -63,7 +63,7 @@ module Journaled::Event
63
63
  end
64
64
 
65
65
  included do
66
- cattr_accessor(:journaled_enqueue_opts, instance_writer: false) { {} }
66
+ class_attribute :journaled_enqueue_opts, default: {}
67
67
 
68
68
  journal_attributes :id, :event_type, :created_at
69
69
  end
@@ -26,9 +26,9 @@ class Journaled::Writer
26
26
 
27
27
  def journal!
28
28
  validate!
29
- Journaled::DeliveryJob
30
- .set(journaled_enqueue_opts.reverse_merge(priority: Journaled.job_priority))
31
- .perform_later(**delivery_perform_args)
29
+ ActiveSupport::Notifications.instrument('journaled.event.enqueue', event: journaled_event, priority: job_opts[:priority]) do
30
+ Journaled::DeliveryJob.set(job_opts).perform_later(**delivery_perform_args)
31
+ end
32
32
  end
33
33
 
34
34
  private
@@ -43,6 +43,10 @@ class Journaled::Writer
43
43
  schema_validator(journaled_schema_name).validate! serialized_event
44
44
  end
45
45
 
46
+ def job_opts
47
+ journaled_enqueue_opts.reverse_merge(priority: Journaled.job_priority)
48
+ end
49
+
46
50
  def delivery_perform_args
47
51
  {
48
52
  serialized_event: serialized_event,
@@ -16,3 +16,89 @@ RSpec::Matchers.define :journal_changes_to do |*attribute_names, as:|
16
16
  "expected #{model_class} not to journal changes to #{attribute_names.map(&:inspect).join(', ')} as #{as.inspect}"
17
17
  end
18
18
  end
19
+
20
+ RSpec::Matchers.define_negated_matcher :not_journal_changes_to, :journal_changes_to
21
+
22
+ RSpec::Matchers.define :journal_events_including do |*expected_events|
23
+ raise "Please specify at least one expected event. RSpec argument matchers are supported." if expected_events.empty?
24
+
25
+ attr_accessor :expected, :actual, :matches, :nonmatches
26
+
27
+ chain :with_schema_name, :expected_schema_name
28
+ chain :with_partition_key, :expected_partition_key
29
+ chain :with_stream_name, :expected_stream_name
30
+ chain :with_enqueue_opts, :expected_enqueue_opts
31
+ chain :with_priority, :expected_priority
32
+
33
+ def supports_block_expectations?
34
+ true
35
+ end
36
+
37
+ def hash_including_recursive(hash)
38
+ hash_including(
39
+ hash.transform_values { |v| v.is_a?(Hash) ? hash_including_recursive(v) : v },
40
+ )
41
+ end
42
+
43
+ match do |block|
44
+ expected_events = [expected_events.first].flatten(1) unless expected_events.length > 1
45
+
46
+ self.expected = expected_events.map { |e| { journaled_attributes: e } }
47
+ expected.each { |e| e.merge!(journaled_schema_name: expected_schema_name) } if expected_schema_name
48
+ expected.each { |e| e.merge!(journaled_partition_key: expected_partition_key) } if expected_partition_key
49
+ expected.each { |e| e.merge!(journaled_stream_name: expected_stream_name) } if expected_stream_name
50
+ expected.each { |e| e.merge!(journaled_enqueue_opts: expected_enqueue_opts) } if expected_enqueue_opts
51
+ expected.each { |e| e.merge!(priority: expected_priority) } if expected_priority
52
+ self.actual = []
53
+
54
+ callback = ->(_name, _started, _finished, _unique_id, payload) do
55
+ event = payload[:event]
56
+ a = { journaled_attributes: event.journaled_attributes }
57
+ a[:journaled_schema_name] = event.journaled_schema_name if expected_schema_name
58
+ a[:journaled_partition_key] = event.journaled_partition_key if expected_partition_key
59
+ a[:journaled_stream_name] = event.journaled_stream_name if expected_stream_name
60
+ a[:journaled_enqueue_opts] = event.journaled_enqueue_opts if expected_enqueue_opts
61
+ a[:priority] = payload[:priority] if expected_priority
62
+ actual << a
63
+ end
64
+
65
+ ActiveSupport::Notifications.subscribed(callback, 'journaled.event.enqueue', &block)
66
+
67
+ self.matches = actual.select do |a|
68
+ expected.any? { |e| values_match?(hash_including_recursive(e), a) }
69
+ end
70
+
71
+ self.nonmatches = actual - matches
72
+
73
+ exact_matches = matches.dup
74
+ matches.count == expected.count && expected.all? do |e|
75
+ match, index = exact_matches.each_with_index.find { |a, _| values_match?(hash_including_recursive(e), a) }
76
+ exact_matches.delete_at(index) if match
77
+ end && exact_matches.empty?
78
+ end
79
+
80
+ failure_message do
81
+ <<~MSG
82
+ Expected the code block to journal exactly one matching event per expected event.
83
+
84
+ Expected Events (#{expected.count}):
85
+ ===============================================================================
86
+ #{expected.map(&:to_json).join("\n ")}
87
+ ===============================================================================
88
+
89
+ Matching Events (#{matches.count}):
90
+ ===============================================================================
91
+ #{matches.map(&:to_json).join("\n ")}
92
+ ===============================================================================
93
+
94
+ Non-Matching Events (#{nonmatches.count}):
95
+ ===============================================================================
96
+ #{nonmatches.map(&:to_json).join("\n ")}
97
+ ===============================================================================
98
+ MSG
99
+ end
100
+ end
101
+
102
+ RSpec::Matchers.alias_matcher :journal_event_including, :journal_events_including
103
+ RSpec::Matchers.define_negated_matcher :not_journal_events_including, :journal_events_including
104
+ RSpec::Matchers.define_negated_matcher :not_journal_event_including, :journal_event_including
@@ -1,3 +1,3 @@
1
1
  module Journaled
2
- VERSION = "4.2.0".freeze
2
+ VERSION = "4.3.0".freeze
3
3
  end
@@ -44,7 +44,7 @@ RSpec.describe Journaled::Changes do
44
44
 
45
45
  subject { klass.new }
46
46
 
47
- let(:change_writer) { double(Journaled::ChangeWriter, create: true, update: true, delete: true) }
47
+ let(:change_writer) { instance_double(Journaled::ChangeWriter, create: true, update: true, delete: true) }
48
48
 
49
49
  before do
50
50
  allow(Journaled::ChangeWriter).to receive(:new) do |opts|
@@ -1,11 +1,12 @@
1
1
  require 'rails_helper'
2
2
 
3
3
  RSpec.describe Journaled::Writer do
4
+ let(:event_class) { Class.new { include Journaled::Event } }
4
5
  subject { described_class.new journaled_event: journaled_event }
5
6
 
6
7
  describe '#initialize' do
7
8
  context 'when the Journaled Event does not implement all the necessary methods' do
8
- let(:journaled_event) { double }
9
+ let(:journaled_event) { instance_double(event_class) }
9
10
 
10
11
  it 'raises on initialization' do
11
12
  expect { subject }.to raise_error RuntimeError, /An enqueued event must respond to/
@@ -14,7 +15,8 @@ RSpec.describe Journaled::Writer do
14
15
 
15
16
  context 'when the Journaled Event returns non-present values for some of the required methods' do
16
17
  let(:journaled_event) do
17
- double(
18
+ instance_double(
19
+ event_class,
18
20
  journaled_schema_name: nil,
19
21
  journaled_attributes: {},
20
22
  journaled_partition_key: '',
@@ -30,7 +32,8 @@ RSpec.describe Journaled::Writer do
30
32
 
31
33
  context 'when the Journaled Event complies with the API' do
32
34
  let(:journaled_event) do
33
- double(
35
+ instance_double(
36
+ event_class,
34
37
  journaled_schema_name: :fake_schema_name,
35
38
  journaled_attributes: { foo: :bar },
36
39
  journaled_partition_key: 'fake_partition_key',
@@ -71,8 +74,9 @@ RSpec.describe Journaled::Writer do
71
74
 
72
75
  let(:journaled_enqueue_opts) { {} }
73
76
  let(:journaled_event) do
74
- double(
75
- journaled_schema_name: :fake_schema_name,
77
+ instance_double(
78
+ event_class,
79
+ journaled_schema_name: 'fake_schema_name',
76
80
  journaled_attributes: journaled_event_attributes,
77
81
  journaled_partition_key: 'fake_partition_key',
78
82
  journaled_stream_name: 'my_app_events',
@@ -85,7 +89,9 @@ RSpec.describe Journaled::Writer do
85
89
  let(:journaled_event_attributes) { { foo: 1 } }
86
90
 
87
91
  it 'raises an error and does not enqueue anything' do
88
- expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
92
+ expect { subject.journal! }
93
+ .to raise_error(JSON::Schema::ValidationError)
94
+ .and not_journal_event_including(anything)
89
95
  expect(enqueued_jobs.count).to eq 0
90
96
  end
91
97
  end
@@ -95,7 +101,9 @@ RSpec.describe Journaled::Writer do
95
101
  let(:journaled_event_attributes) { { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: 1 } }
96
102
 
97
103
  it 'raises an error and does not enqueue anything' do
98
- expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
104
+ expect { subject.journal! }
105
+ .to raise_error(JSON::Schema::ValidationError)
106
+ .and not_journal_event_including(anything)
99
107
  expect(enqueued_jobs.count).to eq 0
100
108
  end
101
109
  end
@@ -104,7 +112,13 @@ RSpec.describe Journaled::Writer do
104
112
  let(:journaled_event_attributes) { { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: :bar } }
105
113
 
106
114
  it 'creates a delivery with the app name passed through' do
107
- expect { subject.journal! }.to change { enqueued_jobs.count }.from(0).to(1)
115
+ expect { subject.journal! }
116
+ .to change { enqueued_jobs.count }.from(0).to(1)
117
+ .and journal_event_including(journaled_event_attributes)
118
+ .with_schema_name('fake_schema_name')
119
+ .with_partition_key('fake_partition_key')
120
+ .with_stream_name('my_app_events')
121
+ .with_enqueue_opts({})
108
122
  expect(enqueued_jobs.first[:args].first).to include('stream_name' => 'my_app_events')
109
123
  end
110
124
 
@@ -124,6 +138,13 @@ RSpec.describe Journaled::Writer do
124
138
  enqueued_jobs.count { |j| j['job_class'] == 'Journaled::DeliveryJob' && j['priority'] == 999 }
125
139
  end
126
140
  }.from(0).to(1)
141
+ .and journal_event_including(journaled_event_attributes)
142
+ .with_schema_name('fake_schema_name')
143
+ .with_partition_key('fake_partition_key')
144
+ .with_stream_name('my_app_events')
145
+ .with_priority(999)
146
+ .and not_journal_event_including(anything)
147
+ .with_enqueue_opts(priority: 999) # with_enqueue_opts looks at event itself
127
148
  end
128
149
  end
129
150
 
@@ -138,6 +159,12 @@ RSpec.describe Journaled::Writer do
138
159
  enqueued_jobs.count { |j| j['job_class'] == 'Journaled::DeliveryJob' && j['priority'] == 13 }
139
160
  end
140
161
  }.from(0).to(1)
162
+ .and journal_event_including(journaled_event_attributes)
163
+ .with_schema_name('fake_schema_name')
164
+ .with_partition_key('fake_partition_key')
165
+ .with_stream_name('my_app_events')
166
+ .with_priority(13)
167
+ .with_enqueue_opts(priority: 13)
141
168
  end
142
169
  end
143
170
  end
@@ -154,7 +181,9 @@ RSpec.describe Journaled::Writer do
154
181
  end
155
182
 
156
183
  it 'raises an error and does not enqueue anything' do
157
- expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
184
+ expect { subject.journal! }
185
+ .to not_journal_events_including(anything)
186
+ .and raise_error JSON::Schema::ValidationError
158
187
  expect(enqueued_jobs.count).to eq 0
159
188
  end
160
189
  end
@@ -165,7 +194,16 @@ RSpec.describe Journaled::Writer do
165
194
  end
166
195
 
167
196
  it 'creates a delivery with the app name passed through' do
168
- expect { subject.journal! }.to change { enqueued_jobs.count }.from(0).to(1)
197
+ expect { subject.journal! }
198
+ .to change { enqueued_jobs.count }.from(0).to(1)
199
+ .and journal_event_including(journaled_event_attributes)
200
+ .with_schema_name('fake_schema_name')
201
+ .with_partition_key('fake_partition_key')
202
+ .with_stream_name('my_app_events')
203
+ .with_enqueue_opts({})
204
+ .with_priority(Journaled.job_priority)
205
+ .and not_journal_event_including(anything)
206
+ .with_enqueue_opts(priority: Journaled.job_priority)
169
207
  expect(enqueued_jobs.first[:args].first).to include('stream_name' => 'my_app_events')
170
208
  end
171
209
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: journaled
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Lipson
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2022-03-17 00:00:00.000000000 Z
14
+ date: 2022-04-07 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activejob