Project

stenotype

0.0
There's a lot of open issues
Pretty much it
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
 Dependencies

Development

~> 2.0
~> 0.12
~> 5.2.3
~> 10.0
~> 3.5
~> 3.0
>= 0.19.0, < 1.0
~> 0.17
>= 0.19.0, < 1.0
~> 0.9
~> 0.9

Runtime

>= 0.22.0, < 1.0
 Project Readme

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.