Skip to content

Commit

Permalink
Merge branch 'release/2024-09-11'
Browse files Browse the repository at this point in the history
  • Loading branch information
rahoulb committed Sep 11, 2024
2 parents a1b33d6 + 52e3e14 commit 1fe20c6
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 76 deletions.
39 changes: 39 additions & 0 deletions app/models/audit_trail/event.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

module AuditTrail
class Event < ApplicationRecord
belongs_to :context, class_name: "AuditTrail::Event", optional: true
Expand All @@ -8,5 +10,42 @@ class Event < ApplicationRecord
validates :partition, presence: true
serialize :data, type: Hash, coder: YAML, default: {}
enum :status, ready: 0, in_progress: 10, completed: 100, failed: -1

def result
result_as_value || result_as_model
end

def result= value
value.is_a?(ActiveRecord::Base) ? record_result_as_model(value) : record_result_as_value(value)
end

def exception= value
data[EXCEPTION_CLASS_NAME] = value.class.name
data[EXCEPTION_MESSAGE] = value.message
end

def exception_class = data[EXCEPTION_CLASS_NAME]

def exception_message = data[EXCEPTION_MESSAGE]

private

def result_as_value = data[RESULT]

def result_as_model
links.find_by(name: RESULT)&.model
end

def record_result_as_value(value)
value.nil? ? data.delete(RESULT) : data[RESULT] = value
end

def record_result_as_model(model)
links.create! name: RESULT, partition: partition, model: model
end

RESULT = "audit_trail/event/result"
EXCEPTION_CLASS_NAME = "audit_trail/event/exception_class"
EXCEPTION_MESSAGE = "audit_trail/event/exception_message"
end
end
19 changes: 19 additions & 0 deletions lib/audit_trail/context_stack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module AuditTrail
class ContextStack
def initialize
@stack = []
end

def push event
@stack << event
end

def pop
@stack.pop
end

def current
@stack.last
end
end
end
27 changes: 24 additions & 3 deletions lib/audit_trail/recording.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
require_relative "context_stack"

module AuditTrail
def self.record event_name, partition: "event", user: nil, context: nil, **params
def self.record event_name, partition: nil, user: nil, context: nil, **params, &block
context ||= context_stack.current
partition ||= context&.partition || "event"
user ||= context&.user

models = params.select { |key, value| value.is_a? ActiveRecord::Base }
data = params.select { |key, value| !value.is_a? ActiveRecord::Base }
Event.create!(name: event_name, context: context, partition: partition, user: user, data: data, status: "completed").tap do |event|
models.each { |key, model| event.links.create! name: key, model: model, partition: partition }

Event.create!(name: event_name, context: context, partition: partition, user: user, data: data, status: "in_progress").tap do |event|
begin
context_stack.push event

models.each { |key, model| event.links.create! name: key, model: model, partition: partition }

event.update result: block&.call, status: "completed"
rescue => ex
event.update status: "failed", exception: ex
ensure
context_stack.pop
end
end
end

def self.context_stack
Thread.current[:audit_trail_context] ||= ContextStack.new
end
end
152 changes: 152 additions & 0 deletions spec/audit_trail/recording_multiple_events_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
require "rails_helper"

RSpec.describe "Recording audit trail events within the context of other events" do
context "#context" do
it "records an event within the context of another event" do
AuditTrail.record "some_event" do
AuditTrail.record "another_event"
end

@event = AuditTrail::Event.last
expect(@event.name).to eq "another_event"
expect(@event.context.name).to eq "some_event"
end

it "records a hierarchy of events" do
AuditTrail.record "event" do
AuditTrail.record "child" do
AuditTrail.record "grandchild"
end
end

@event = AuditTrail::Event.last
expect(@event.name).to eq "grandchild"
expect(@event.context.name).to eq "child"
expect(@event.context.context.name).to eq "event"
end

it "tracks the current context" do
AuditTrail.record "some_event" do
@event = AuditTrail::Event.find_by! name: "some_event"
expect(AuditTrail.current_context).to eq @event

AuditTrail.record "another_event" do
@another_event = AuditTrail::Event.find_by! name: "another_event"
expect(AuditTrail.current_context).to eq @another_event
end
end
end

it "removes the current context when an event completes" do
AuditTrail.record "some_event" do
@event = AuditTrail::Event.find_by! name: "some_event"

AuditTrail.record "another_event" do
raise "FAILURE"
end

expect(AuditTrail.context_stack.current).to eq @event

raise "ANOTHER FAILURE"
end

expect(AuditTrail.context_stack.current).to be_nil
end

it "removes the current context when an event fails" do
AuditTrail.record "some_event" do
@event = AuditTrail::Event.find_by! name: "some_event"
AuditTrail.record "another_event" do
@another_event = AuditTrail::Event.find_by! name: "another_event"
expect(AuditTrail.context_stack.current).to eq @another_event
end
expect(AuditTrail.context_stack.current).to eq @event
end
expect(AuditTrail.context_stack.current).to be_nil
end

end

context "#status" do
it "marks the event as in progress" do
AuditTrail.record "some_event" do
@event = AuditTrail::Event.last
expect(@event).to be_in_progress
end
end

it "marks the event as completed" do
AuditTrail.record "some_event" do
# whatever
end
@event = AuditTrail::Event.last
expect(@event).to be_completed
end

it "marks the event as failed" do
AuditTrail.record "some_event" do
raise "BOOM"
end
@event = AuditTrail::Event.last
expect(@event).to be_failed
end
end

context "#result" do
it "records the result of the event as a simple type" do
AuditTrail.record "some_event" do
:the_result
end

@event = AuditTrail::Event.last
expect(@event.result).to eq :the_result
end

it "records the result of the event as a linked model" do
@some_user = User.create! name: "Some person"

AuditTrail.record "some_event" do
@some_user
end

@event = AuditTrail::Event.last
expect(@event.result).to eq @some_user
end
end

context "#exception" do
it "records the exception that caused the failure" do
AuditTrail.record "some_event" do
raise "BOOM"
end

@event = AuditTrail::Event.last
expect(@event.exception_class).to eq "RuntimeError"
expect(@event.exception_message).to eq "BOOM"
end
end

context "inheritance" do
it "inherits the partition key from the current context" do
AuditTrail.record "some_event", partition: "ABC" do
AuditTrail.record "another_event"
end

@event = AuditTrail::Event.last
expect(@event.name).to eq "another_event"
expect(@event.partition).to eq "ABC"
end

it "inherits the user from the current context" do
@user = User.create! name: "Some person"

AuditTrail.record "some_event", user: @user do
AuditTrail.record "another_event"
end

@event = AuditTrail::Event.last
expect(@event.name).to eq "another_event"
expect(@event.user).to eq @user
end
end
end
72 changes: 72 additions & 0 deletions spec/audit_trail/recording_single_events_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require "rails_helper"

RSpec.describe "Recording single audit trail events" do
it "records the event name" do
AuditTrail.record "some_event"

@event = AuditTrail::Event.last
expect(@event.name).to eq "some_event"
end

it "records a partition key" do
AuditTrail.record "some_event", partition: "partition-123"

@event = AuditTrail::Event.last
expect(@event.partition).to eq "partition-123"
end

it "records the user" do
@user = User.create! name: "Some person"

AuditTrail.record "some_event", user: @user

@event = AuditTrail::Event.last
expect(@event.user).to eq @user
end

it "records parameters with the event" do
AuditTrail.record "some_event", string: "Hello", number: 123

@event = AuditTrail::Event.last
expect(@event.data).to eq({string: "Hello", number: 123})
end

it "records models with the event" do
@user = User.create! name: "Some person"
@post = Post.create! user: @user, title: "Hello world", contents: "Welcome to my blog!"

AuditTrail.record "post_added", post: @post, title: "Hello world"

@event = AuditTrail::Event.last
expect(@event.data).to eq({title: "Hello world"})
expect(@event.links.find_by(model: @post)).to_not be_nil
end

it "records the partition key along with models and the event" do
@user = User.create! name: "Some person"
@post = Post.create! user: @user, title: "Hello world", contents: "Welcome to my blog!"

AuditTrail.record "post_added", post: @post, title: "Hello world", partition: "partition-123"

@event = AuditTrail::Event.last
expect(@event.data).to eq({title: "Hello world"})
expect(@event.links.find_by(model: @post).partition).to eq "partition-123"
end

it "records the parent event as the context for this event" do
@context = AuditTrail::Event.create! name: "parent", status: "completed"

AuditTrail.record "child", context: @context

@event = AuditTrail::Event.last
expect(@event.context).to eq @context
expect(@context.children).to include @event
end

it "records that the event has completed" do
AuditTrail.record "some_event"

@event = AuditTrail::Event.last
expect(@event).to be_completed
end
end
Loading

0 comments on commit 1fe20c6

Please sign in to comment.