# 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: ```ruby gem "Stenotype" ``` And then execute: $ bundle Or install it yourself as: $ gem install Stenotype ## Usage ### Configuration Configuring the library is as simple as: ```ruby 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: ```ruby 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](#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: ```ruby 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: ```ruby 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: ```ruby 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](#custom-context-handlers) for more details.): ```ruby # 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`: ```ruby 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: ```ruby 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`: ```ruby 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: ```ruby 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: ```ruby 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](https://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](https://opensource.org/licenses/MIT).