Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Opera Changelog

### 0.3.4 - February 19, 2025

- Add support for `benchmark` label
- Add support for instrumentation

### 0.3.3 - January 15, 2025

- Fix issue with handling exceptions in nested operations
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
opera (0.3.3)
opera (0.3.4)

GEM
remote: https://rubygems.org/
Expand Down
46 changes: 38 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ Simply initialise the configuration and chose what method you want to use to rep
Opera::Operation::Config.configure do |config|
config.transaction_class = ActiveRecord::Base
config.transaction_method = :transaction
config.transaction_options = { requires_new: true }
config.transaction_options = { requires_new: true, level: :step } # or level: :operation - default
config.instrumentation_class = Datadog::Tracing
config.instrumentation_method = :trace
config.instrumentation_options = { service: :operation }
config.mode = :development # Can be set to production too
config.reporter = defined?(Rollbar) ? Rollbar : Rails.logger
end
Expand All @@ -52,7 +55,6 @@ Once opera gem is in your project you can start to build Operations

```ruby
class A < Opera::Operation::Base

configure do |config|
config.transaction_class = Profile
config.reporter = Rails.logger
Expand Down Expand Up @@ -96,18 +98,44 @@ Start developing your business logic, services and interactions as Opera::Operat
When using Opera::Operation inside an engine add the following
configuration to your spec_helper.rb or rails_helper.rb:

```
```ruby
Opera::Operation::Config.configure do |config|
config.transaction_class = ActiveRecord::Base
end
```

Without this extra configuration you will receive:
```
```ruby
NoMethodError:
undefined method `transaction' for nil:NilClass
```

### Instrumentation

When you want to easily instrument your operations you can add this to the opera config:

```ruby
Rails.application.configure do
config.x.instrumentation_class = Datadog::Tracing
config.x.instrumentation_method = :trace
config.x.instrumentation_options = { service: :opera }
end
```

You can also instrument individual operations by adding this to the operation config:

```ruby
class A < Opera::Operation::Base
configure do |config|
config.instrumentation_class = Datadog::Tracing
config.instrumentation_method = :trace
config.instrumentation_options = { service: :opera, level: :step }
end

# steps
end
```

### Debugging

When you want to easily debug exceptions you can add this
Expand Down Expand Up @@ -674,10 +702,12 @@ class Profile::Create < Opera::Operation::Base

validate :profile_schema

step :create
step :update
benchmark :fast_section do
step :create
step :update
end

benchmark do
benchmark :slow_section do
step :send_email
step :output
end
Expand Down Expand Up @@ -717,7 +747,7 @@ Profile::Create.call(params: {
}, dependencies: {
current_account: Account.find(1)
})
#<Opera::Operation::Result:0x007ff414a01238 @errors={}, @exceptions={}, @information={:real=>1.800013706088066e-05, :total=>0.0}, @executions=[:profile_schema, :create, :update, :send_email, :output], @output={:model=>#<Profile id: 30, user_id: nil, linkedin_uid: nil, picture: nil, headline: nil, summary: nil, first_name: "foo", last_name: "bar", created_at: "2020-08-19 10:46:00", updated_at: "2020-08-18 10:46:00", agree_to_terms_and_conditions: nil, registration_status: "", account_id: 1, start_date: nil, supervisor_id: nil, picture_processing: false, statistics: {}, data: {}, notification_timestamps: {}, suggestions: {}, notification_settings: {}, contact_information: []>}>
#<Opera::Operation::Result:0x007ff414a01238 @errors={}, @exceptions={}, @information={fast_section: {:real=>0.300013706088066e-05, :total=>0.0}, slow_section: {:real=>1.800013706088066e-05, :total=>0.0}}, @executions=[:profile_schema, :create, :update, :send_email, :output], @output={:model=>#<Profile id: 30, user_id: nil, linkedin_uid: nil, picture: nil, headline: nil, summary: nil, first_name: "foo", last_name: "bar", created_at: "2020-08-19 10:46:00", updated_at: "2020-08-18 10:46:00", agree_to_terms_and_conditions: nil, registration_status: "", account_id: 1, start_date: nil, supervisor_id: nil, picture_processing: false, statistics: {}, data: {}, notification_timestamps: {}, suggestions: {}, notification_settings: {}, contact_information: []>}>
```

### Success
Expand Down
1 change: 1 addition & 0 deletions lib/opera/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'opera/operation/builder'
require 'opera/operation/base'
require 'opera/operation/executor'
require 'opera/operation/instrumentation'
require 'opera/operation/result'
require 'opera/operation/config'
require 'opera/operation/instructions/executors/success'
Expand Down
4 changes: 3 additions & 1 deletion lib/opera/operation/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ class << self
def call(args = {})
operation = new(params: args.fetch(:params, {}), dependencies: args.fetch(:dependencies, {}))
executor = Executor.new(operation)
executor.evaluate_instructions(instructions)
Instrumentation.new(config).instrument(name: self.name, level: :operation) do
executor.evaluate_instructions(instructions)
end
executor.result
end

Expand Down
1 change: 1 addition & 0 deletions lib/opera/operation/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def initialize(&block)
instructions << if !blk.nil?
{
kind: instruction,
label: method,
instructions: InnerBuilder.new(&blk).instructions
}
else
Expand Down
11 changes: 9 additions & 2 deletions lib/opera/operation/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ class Config
DEVELOPMENT_MODE = :development
PRODUCTION_MODE = :production

attr_accessor :transaction_class, :transaction_method, :transaction_options, :reporter, :mode
attr_accessor :transaction_class, :transaction_method, :transaction_options,
:instrumentation_class, :instrumentation_method, :instrumentation_options, :mode, :reporter

def initialize
@transaction_class = self.class.transaction_class
@transaction_method = self.class.transaction_method || :transaction
@transaction_options = self.class.transaction_options

@instrumentation_class = self.class.instrumentation_class
@instrumentation_method = self.class.instrumentation_method || :instrument
@instrumentation_options = self.class.instrumentation_options || {}

@mode = self.class.mode || DEVELOPMENT_MODE
@reporter = custom_reporter || self.class.reporter

Expand All @@ -35,7 +41,8 @@ def validate!
end

class << self
attr_accessor :transaction_class, :transaction_method, :transaction_options, :reporter, :mode
attr_accessor :transaction_class, :transaction_method, :transaction_options,
:instrumentation_class, :instrumentation_method, :instrumentation_options, :mode, :reporter

def configure
yield self
Expand Down
9 changes: 8 additions & 1 deletion lib/opera/operation/instructions/executors/benchmark.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ module Executors
class Benchmark < Executor
def call(instruction)
benchmark = ::Benchmark.measure do
instruction[:kind] = :step
super
end

result.add_information(real: benchmark.real, total: benchmark.total)
result.add_information(benchmark_key(instruction) => { real: benchmark.real, total: benchmark.total })
end

private

def benchmark_key(instruction)
instruction[:method] || instruction[:label] || instruction[:instructions].map { |e| e[:method] }.join('-').to_sym
end
end
end
Expand Down
6 changes: 4 additions & 2 deletions lib/opera/operation/instructions/executors/step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ class Step < Executor
def call(instruction)
method = instruction[:method]

operation.result.add_execution(method) unless production_mode?
operation.send(method)
Instrumentation.new(config).instrument(name: "##{method}", level: :step) do
operation.result.add_execution(method) unless production_mode?
operation.send(method)
end
rescue StandardError => exception
reporter&.error(exception)
operation.result.add_exception(method, "#{exception.message}, for #{operation.inspect}", classname: operation.class.name)
Expand Down
44 changes: 44 additions & 0 deletions lib/opera/operation/instrumentation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Opera
module Operation
class Instrumentation
attr_reader :config

def initialize(config)
@config = config
end

def instrument(name:, level: :operation)
return yield if !instrumentation_enabled?
return yield if level == :step && instrumentation_level != :step

instrumentation_class.send(instrumentation_method, name, **instrumentation_options.except(:level)) do
yield
end
end

private

def instrumentation_options
config.instrumentation_options
end

def instrumentation_method
config.instrumentation_method
end

def instrumentation_class
config.instrumentation_class
end

def instrumentation_enabled?
!!config.instrumentation_class
end

def instrumentation_level
instrumentation_options[:level] || :operation
end
end
end
end
2 changes: 1 addition & 1 deletion lib/opera/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Opera
VERSION = '0.3.3'
VERSION = '0.3.4'
end
99 changes: 92 additions & 7 deletions spec/opera/operation/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ def step_1
kind: :step, method: :validation_2
}
],
label: nil,
kind: :validate
}
])
Expand Down Expand Up @@ -1042,6 +1043,85 @@ def step_3
end
end

context 'for instrumentation' do
let(:instrumentation_class) do
Class.new do
def self.trace(name)
puts "Trace #{name}"
yield
end
end
end

let(:operation_class) do
FakeName = Class.new(Operation::Base) do
step :step_1
step :step_2
step :step_3
step :step_4
step :step_5

def step_1
true
end

def step_2
true
end

def step_3
true
end

def step_4
1
end

def step_5
true
end
end
end

context 'for operation level' do
before do
Operation::Config.configure do |config|
config.instrumentation_class = instrumentation_class
config.instrumentation_method = :trace
end
end

it 'evaluates all steps' do
expect(subject.executions).to match_array(%i[step_1 step_2 step_3 step_4 step_5])
end

it 'calls instrumentation with correct params' do
expect(instrumentation_class).to receive(:trace).with('Opera::FakeName').and_call_original
subject
end
end

context 'for step level' do
before do
Operation::Config.configure do |config|
config.instrumentation_class = instrumentation_class
config.instrumentation_method = :trace
config.instrumentation_options = { level: :step }
end
end

it 'evaluates all steps' do
expect(subject.executions).to match_array(%i[step_1 step_2 step_3 step_4 step_5])
end

it 'calls instrumentation with correct params' do
expect(instrumentation_class).to receive(:trace).exactly(6).times.and_call_original

subject
end
end
end

context 'for operation' do
let(:failing_operation) do
Class.new(Operation::Base) do
Expand Down Expand Up @@ -1237,10 +1317,8 @@ def step_1
Class.new(Operation::Base) do
step :step_1
step :step_2
benchmark do
step :step_3
step :step_4
end
benchmark :step_3
benchmark :step_4
step :step_5

def step_1
Expand Down Expand Up @@ -1270,16 +1348,18 @@ def step_5
end

it 'add benchmark info to result' do
expect(subject.information).to have_key(:real)
expect(subject.information).to have_key(:total)
expect(subject.information[:step_3]).to have_key(:real)
expect(subject.information[:step_3]).to have_key(:total)
expect(subject.information[:step_4]).to have_key(:real)
expect(subject.information[:step_4]).to have_key(:total)
end

context 'for failing step' do
let(:operation_class) do
Class.new(Operation::Base) do
step :step_1
step :step_2
benchmark do
benchmark :example do
step :step_3
step :step_4
end
Expand Down Expand Up @@ -1310,6 +1390,11 @@ def step_5
it 'executes only first 3 instructions' do
expect(subject.executions).to match_array(%i[step_1 step_2 step_3])
end

it 'add benchmark info to result' do
expect(subject.information[:example]).to have_key(:real)
expect(subject.information[:example]).to have_key(:total)
end
end
end

Expand Down