Stenotype
This gem is a tool providing extensions to several rails components in order to track events along with the execution context. Currently ActionController and ActionJob are supported to name a few.
Installation
Add this line to your application's Gemfile:
gem "Stenotype"
And then execute:
$ bundle
Or install it yourself as:
$ gem install Stenotype
Usage
Configuration
Configuring the library is as simple as:
Stenotype.configure do |config|
config.enabled = true
config.targets = [ # Supported targets
Stenotype::Adapters::StdoutAdapter.new,
Stenotype::Adapters::GoogleCloud.new
]
config.uuid_generator = SecureRandom
config.dispatcher = Stenotype::Dispatcher.new
config.logger = Logger.new(STDOUT)
config.graceful_error_handling = true
config.auto_adapter_initialization = true
config.google_cloud do |gc_config|
gc_config.project_id = "google_cloud_project_id"
gc_config.credentials = "path_to_key_json"
gc_config.topic = "google_cloud_topic"
gc_config.async = true # either true or false
end
config.rails do |rails_config|
rails_config.enable_action_controller_ext = true
rails_config.enable_active_job_ext = true
end
end
config.enabled
A flag checked upon emission of an event. Will prevent event emission if set to false. An event is emitted if set to true.
config.targets
Contain an array of targets for the events to be published to. Targets must implement method #publish(event_data, **additional_arguments)
.
config.logger
Specifies a logger for messages and exceptions to be output to. If not set defaults to Logger.new(STDOUT)
, otherwise a manually set logger is used.
config.graceful_error_handling
This flag if set to true
is going to suppress all StandardError
's raised within a gem. Raises the error to the caller if set to false
config.uuid_generator
An object that must implement method #uuid
. Used when an event is emitted to generate a unique id for each event.
config.dispatcher
Dispatcher used to dispatch the event. A dispatcher must implement method #publish(even, serializer: Stenotype::EventSerializer)
. By default Stenotype::EventSerializer
is used, which is responsible for collecting the data from the event and evaluation context.
config.google_cloud.project_id
Google cloud project ID. Please refer to Google Cloud management console to get one.
config.google_cloud.credentials
Google cloud credentials. Might be obtained from Google Cloud management console.
config.google_cloud.topic
Google Cloud topic used for publishing the events.
config.google_cloud.async
Google Cloud publish mode. The mode is either sync or async. When in sync
mode the event will be published in the same thread (which might influence performance). For async
mode the event will be put into a pull which is going to be flushed after a threshold is met.
config.rails.enable_action_controller_ext
Allows to enable/disable Rails ActionController extension
config.rails.enable_active_job_ext
Allows to enable/disable Rails ActiveJob extension
config.rails.auto_adapter_initialization
Controls whether the hook auto_initialize!
is run for each adapter. If set to true auto_initialize!
is invoked for every adapter. If false auto_initialize!
is not run. For example for google cloud adapter this will instantiate client
and topic
objects before first publish. If set to false client
and topic
are lazy initialized.
Configuring context handlers
Each event is emitted in a context which might be an ActionController instance or an ActiveJob instance or potentially any other place. Context handlers are implemented as plain ruby classes. By default a plain Class
handler is registered when not used with any framework. In case Ruby on Rails is used, then there are two additional context handlers for ActionController
and ActiveJob
instances. Registration of the context handler happens upon inheriting from Stenotype::ContextHandlers::Base
.
Emitting Events
Emitting an event is as simple as:
Stenotype::Event.emit!(
"Event Name",
{ attr1: :value1, attr2: :value2 },
eval_context: { name_of_registered_context_handler: context_object }
)
The event is then going to be passed to a dispatcher responsible for sending the evens to targets. See Custom context handlers for more details.
ActionController
Upon loading the library ActionController
is going to be extended with a class method track_view(*actions)
, where actions
is a list of trackable controller actions.
Here is an example usage:
class MyController < ActionController::Base
track_view :index, :show
def index
# do_something
end
def show
# do something
end
end
ActiveJob
Upon loading the library ActiveJob
is going to be extended with a class method trackable_job!
.
Example:
class MyJob < ActiveJob::Base
trackable_job!
def perform(data)
# do_something
end
end
Plain Ruby classes
To track methods from arbitrary ruby classes Object
is extended. Any instance method of a Ruby class might be prepended with sending an event:
class PlainRubyClass
emit_event_before :some_method, :another_method
emit_klass_event_before :class_method
def some_method(data)
# do something
end
def another_method(args)
# do something
end
def self.class_method
# do something
end
end
You could also use a generic method emit_event
from anywhere. The method is mixed into Object
class. It takes several optional kw arguments. data
is a hash which is going to be serialized and sent as event data, method
is by default the method you trigger emit_event
from. eval_context
is a hash containing the name of context handler and a context object itself.
An example usage is as follows (see Custom context handlers for more details.):
# BaseClass sets some state
class BaseClass
attr_reader :local_state
def initialize
@local_state = "some state"
end
end
# A custom handler is introduced
class CustomHandler < Stenotype::ContextHandlers::Base
self.handler_name = :overriden_handler
def as_json(*_args)
{
state: context.local_state
}
end
end
# Event is being emitted twice. First time with default options.
# Second time with overriden method name and eval_context.
class PlainRubyClass < BaseClass
def some_method(data)
event_data = collect_some_data_as_a_hash
emit_event("event_name", event_data) # method name will be `some_method`, eval_context: { klass: self }
other_event_data = do_something_else
emit_event("other_event_name", other_event_data, method: :custom_method_name, eval_context: { overriden_handler: self })
end
end
Adding customizations
Custom adapters
By default two adapters are implemented: Google Cloud and simple Stdout adapter.
Adding a new one might be performed by defining a class inheriting from Stenotype::Adapters::Base
:
class CustomAdapter < Stenotype::Adapters::Base
# A client might be optionally passed to
# the constructor.
#
# def initialize(client: nil)
# @client = client
# end
def publish(event_data, **additional_arguments)
# custom publishing logic
end
def flush!
# actions to be taken to flush the messages
end
def auto_initialize!
# actions to be taken to setup internal adapter state (client, endpoint, whatsoever)
end
end
After defining a custom adapter it must be added to the list of adapters:
Stenotype.config.targets.push(CustomAdapter.new)
Custom context handlers
A list of context handlers might be extended by defining a class inheriting from Stenotype::ContextHandlers::Base
. Event handler must have a self.handler_name
in order to use it during context serialization. Also custom handler must implement method #as_json
:
class CustomHandler < Stenotype::ContextHandlers::Base
self.handler_name = :custom_handler_name
def as_json(*_args)
{
something: something,
another: another
}
end
private
def something_from_context
context.something
end
def another_from_context
context.another
end
end
You do not have to manually register the context handler since it happens upon inheriting from Stenotype::ContextHandlers::Base
Testing
Stenotype currently supports RSpec integration. To be able to test even emission you can use a predefined matcher by adding the following to spec helper:
RSpec.configure do |config|
config.around(:each, type: :stenotype_event) do |example|
require 'stenotype/adapters/test_adapter'
config.include Stenotype::Test::Matchers
RSpec::Mocks.with_temporary_scope do
allow(Stenotype.config).to receive(:targets).and_return(Array.wrap(Stenotype::Adapters::TestAdapter.new))
example.run
allow(Stenotype.config).to receive(:targets).and_call_original
end
end
end
After adding the configuration you can use the matchers:
class Example
include Stenotype::Emitter
def trigger
emit_event(:user_subscription)
end
end
RSpec.describe Stenotype::Emitter do
describe "POST #create" do
subject(:post) { Example.new.trigger }
it "emits a user_subscription event", type: :stenotype_event do
expect { post }.to emit_an_event(:user_subscription).
with_arguments_including({ uuid: "abcd" }).
exactly(1).times
end
end
end
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/Freshly/Stenotype.
License
The gem is available as open source under the terms of the MIT License.