stenotype 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ #
5
+ # A namespace containing extensions of various frameworks.
6
+ # For example Rails components could be extended
7
+ #
8
+ module Frameworks
9
+ #
10
+ # An extension for a plain Ruby class in order to track invocation of
11
+ # instance methods.
12
+ #
13
+ module ObjectExt
14
+ #
15
+ # Class methods for `Object` to be extended by
16
+ #
17
+ ClassMethodsExtension = Class.new(Module)
18
+ #
19
+ # Instance methods to be included into `Object` ancestors chain
20
+ #
21
+ InstanceMethodsExtension = Class.new(Module)
22
+
23
+ attr_reader :instance_mod,
24
+ :class_mod
25
+
26
+ # @!visibility private
27
+ def self.included(klass)
28
+ @instance_mod = InstanceMethodsExtension.new
29
+ @class_mod = ClassMethodsExtension.new
30
+
31
+ build_instance_methods
32
+ build_class_methods
33
+
34
+ klass.const_set(:InstanceProxy, Module.new)
35
+ klass.const_set(:ClassProxy, Module.new)
36
+
37
+ klass.send(:include, instance_mod)
38
+ klass.extend(class_mod)
39
+
40
+ super
41
+ end
42
+
43
+ #
44
+ # @!method emit_event(data = {}, method: caller_locations.first.label, eval_context: nil)
45
+ # A method injected into all instances of Object
46
+ # @!scope instance
47
+ # @param data {Hash} Data to be sent to the targets
48
+ # @param method {String} An optional method name
49
+ # @param eval_context {Hash} A hash linking object to context handler
50
+ # @return {Stenotype::Event} An instance of emitted event
51
+ #
52
+
53
+ #
54
+ # @!method emit_event_before(*methods)
55
+ # A method injected into all instances of Object
56
+ # @!scope instance
57
+ # @param methods {Array<Symbol>} A list of method before which an event will be emitted
58
+ #
59
+
60
+ #
61
+ # @!method emit_klass_event_before(*class_methods)
62
+ # A class method injected into all subclasses of [Object]
63
+ # @!scope class
64
+ # @param class_method {Array<Symbol>} A list of class method before which
65
+ # an event will be emitted
66
+ #
67
+
68
+ #
69
+ # rubocop:disable Metrics/MethodLength
70
+ # Adds two methods: [#emit_event] and [#emit_event_before] to every object
71
+ # inherited from [Object]
72
+ #
73
+ def build_instance_methods
74
+ instance_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
75
+ def emit_event(data = {}, method: caller_locations.first.label, eval_context: nil)
76
+ Stenotype::Event.emit!(
77
+ {
78
+ type: 'class_instance',
79
+ **data,
80
+ },
81
+ options: {
82
+ class: self.class.name,
83
+ method: method.to_sym
84
+ },
85
+ eval_context: (eval_context || { klass: self })
86
+ )
87
+ end
88
+
89
+ def emit_event_before(*methods)
90
+ proxy = const_get(:InstanceProxy)
91
+
92
+ methods.each do |method|
93
+ proxy.module_eval do
94
+ define_method(method) do |*args, **rest_args, &block|
95
+ Stenotype::Event.emit!(
96
+ { type: 'class_instance' },
97
+ options: {
98
+ class: self.class.name,
99
+ method: __method__
100
+ },
101
+ eval_context: { klass: self }
102
+ )
103
+ super(*args, **rest_args, &block)
104
+ end
105
+ end
106
+ end
107
+
108
+ send(:prepend, proxy)
109
+ end
110
+ RUBY
111
+ end
112
+
113
+ #
114
+ # Adds class method [#emit_klass_event_before] to every class
115
+ # inherited from [Object]
116
+ #
117
+ def build_class_methods
118
+ class_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
119
+ def emit_klass_event_before(*class_methods)
120
+ proxy = const_get(:ClassProxy)
121
+
122
+ class_methods.each do |method|
123
+ proxy.module_eval do
124
+ define_method(method) do |*args, **rest_args, &block|
125
+ Stenotype::Event.emit!(
126
+ { type: 'class' },
127
+ options: {
128
+ class: self.name,
129
+ method: __method__
130
+ },
131
+ eval_context: { klass: self }
132
+ )
133
+ super(*args, **rest_args, &block)
134
+ end
135
+ end
136
+
137
+ singleton_class.send(:prepend, proxy)
138
+ end
139
+ end
140
+ RUBY
141
+ end
142
+ # rubocop:enable Metrics/MethodLength
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Stenotype
6
+ module Frameworks
7
+ module Rails
8
+ #
9
+ # An ActionController extension to be injected into classes
10
+ # inheriting from [ActionController::Base]
11
+ #
12
+ module ActionControllerExtension
13
+ extend ActiveSupport::Concern
14
+
15
+ #
16
+ # Emits and event with given data
17
+ # @param data {Hash} Data to be sent to targets
18
+ #
19
+ def record_freshly_event(data)
20
+ Stenotype::Event.emit!(data, options: {}, eval_context: { controller: self })
21
+ end
22
+
23
+ #
24
+ # Class methods to be injected into classes
25
+ # inherited from [ActionController::Base]
26
+ #
27
+ module ClassMethods
28
+ # Adds a before_action to each action from the passed list. A before action
29
+ # is emitting a {Stenotype::Event}. Please note that in case track_view is
30
+ # used several times, it will keep track of the actions which emit events.
31
+ # Each time a new track_view is called it will find a symmetric difference
32
+ # of two sets: set of 'used' actions and a set passed to `track_view`.
33
+ #
34
+ # @example
35
+ # class MyController < ActionController::Base
36
+ # track_view :index, :show
37
+ #
38
+ # def index
39
+ # # do_something
40
+ # end
41
+ #
42
+ # def show
43
+ # # do something
44
+ # end
45
+ #
46
+ # # Not covered by track_view
47
+ # def update
48
+ # # do something
49
+ # end
50
+ # end
51
+ #
52
+ # @param actions {Array<Symbol>} a list of tracked controller actions
53
+ #
54
+ def track_view(*actions)
55
+ # A symmetric difference of two sets.
56
+ # This prevents accidental duplicating of events
57
+ delta = (_tracked_actions - Set[*actions]) | (Set[*actions] - _tracked_actions)
58
+
59
+ return if delta.empty?
60
+
61
+ before_action only: delta do
62
+ record_freshly_event(type: 'view')
63
+ end
64
+
65
+ _tracked_actions.merge(delta)
66
+ end
67
+
68
+ # :nodoc:
69
+ def _tracked_actions
70
+ @_tracked_actions ||= Set.new
71
+ end
72
+
73
+ # Note this action will only define a symmetric difference of
74
+ # the covered with events actions and the ones not used yet.
75
+ # @see #track_view
76
+ #
77
+ # @example
78
+ # class MyController < ActionController::Base
79
+ # track_all_views
80
+ #
81
+ # def index
82
+ # # do_something
83
+ # end
84
+ #
85
+ # def show
86
+ # # do something
87
+ # end
88
+ # end
89
+ #
90
+ def track_all_views
91
+ actions = self.action_methods
92
+
93
+ # A symmetric difference of two sets.
94
+ # This prevents accidental duplicating of events
95
+ delta = ((_tracked_actions - actions) | (actions - _tracked_actions))
96
+
97
+ return if delta.empty?
98
+
99
+ before_action only: delta.to_a do
100
+ record_freshly_event(type: "view")
101
+ end
102
+
103
+ # merge is a mutating op
104
+ _tracked_actions.merge(delta)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Stenotype
6
+ module Frameworks
7
+ #
8
+ # A namespace containing extensions for Ruby on Rails components
9
+ #
10
+ module Rails
11
+ #
12
+ # An extension for ActiveJob to enable adding a hook
13
+ # before performing an instance of [ActiveJob::Base] subclass
14
+ #
15
+ module ActiveJobExtension
16
+ # @!visibility private
17
+ def self.extended(base)
18
+ base.const_set(:JobExt, Module.new)
19
+ super
20
+ end
21
+
22
+ #
23
+ # @example
24
+ # class MyJob < ApplicationJob
25
+ # trackable_job! # => will prepend a perform action with event recorder
26
+ #
27
+ # def perform(data)
28
+ # # do_something
29
+ # end
30
+ # end
31
+ #
32
+ # rubocop:disable Metrics/MethodLength
33
+ #
34
+ def trackable_job!
35
+ proxy = const_get(:JobExt)
36
+ proxy.module_eval do
37
+ define_method(:perform) do |*args, **rest_args, &block|
38
+ Stenotype::Event.emit!(
39
+ { type: "active_job" },
40
+ options: {},
41
+ eval_context: { active_job: self }
42
+ )
43
+ super(*args, **rest_args, &block)
44
+ end
45
+ end
46
+
47
+ # Prepend an instance of module so that
48
+ # super() can be chained down the ancestors
49
+ # without changing existing ActiveJob interface
50
+ #
51
+ send(:prepend, proxy)
52
+ end
53
+ # rubocop:enable Metrics/MethodLength
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ # :nodoc:
5
+ VERSION = '0.1.0'
6
+ # :nodoc:
7
+ end
data/lib/stenotype.rb ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # A top level namespace for the freshly-events gem
5
+ #
6
+ module Stenotype
7
+ class << self
8
+ ##
9
+ # Configures the library.
10
+ # @yield {Stenotype::Configuration}
11
+ #
12
+ # @example
13
+ #
14
+ # Stenotype.configure do |config|
15
+ # config.targets = [
16
+ # Stenotype::Adapters::StdoutAdapter.new,
17
+ # Stenotype::Adapters::GoogleCloud.new
18
+ # ]
19
+ #
20
+ # config.dispatcher = Stenotype::Dispatcher.new
21
+ # config.gc_project_id = ENV['GC_PROJECT_ID']
22
+ # config.gc_credentials = ENV['GC_CREDENTIALS']
23
+ # config.gc_topic = ENV['GC_TOPIC']
24
+ # config.gc_mode = :async
25
+ # end
26
+ #
27
+ def configure(&block)
28
+ Stenotype::Configuration.configure(&block)
29
+ end
30
+
31
+ ##
32
+ # @return {Stenotype::Configuration}
33
+ #
34
+ def config
35
+ Stenotype::Configuration
36
+ end
37
+ end
38
+ end
39
+
40
+ require "stenotype/adapters"
41
+ require "stenotype/configuration"
42
+ require "stenotype/context_handlers"
43
+ require "stenotype/dispatcher"
44
+ require "stenotype/event"
45
+ require "stenotype/event_serializer"
46
+ require "stenotype/exceptions"
47
+ require "stenotype/version"
48
+ require "stenotype/frameworks/object_ext"
49
+
50
+ Stenotype.configure do |config|
51
+ config.uuid_generator = SecureRandom
52
+ config.dispatcher = Stenotype::Dispatcher.new
53
+
54
+ config.gc_project_id = ENV['GC_PROJECT_ID']
55
+ config.gc_credentials = ENV['GC_CREDENTIALS']
56
+ config.gc_topic = ENV['GC_TOPIC']
57
+ config.gc_mode = :async
58
+
59
+ config.targets = [
60
+ Stenotype::Adapters::StdoutAdapter.new,
61
+ Stenotype::Adapters::GoogleCloud.new
62
+ ]
63
+
64
+ Stenotype::ContextHandlers.module_eval do
65
+ register Stenotype::ContextHandlers::Klass
66
+ end
67
+
68
+ Object.send(:include, Stenotype::Frameworks::ObjectExt)
69
+ end
70
+
71
+ if defined?(Rails)
72
+ require "stenotype/frameworks/rails/action_controller"
73
+ require "stenotype/frameworks/rails/active_job"
74
+
75
+ module Stenotype
76
+ class Railtie < Rails::Railtie # :nodoc:
77
+ config.stenotype = Stenotype.config
78
+
79
+ #
80
+ # Register Rails handlers
81
+ #
82
+ Stenotype::ContextHandlers.module_eval do
83
+ register Stenotype::ContextHandlers::Rails::Controller
84
+ register Stenotype::ContextHandlers::Rails::ActiveJob
85
+ end
86
+
87
+ ActiveSupport.on_load(:action_controller) do
88
+ include Stenotype::Frameworks::Rails::ActionControllerExtension
89
+ end
90
+
91
+ ActiveSupport.on_load(:active_job) do
92
+ # @todo: consider using `::ActiveJob::Base.around_perform`
93
+ # or `::ActiveJob::Base.around_enqueue`
94
+ extend Stenotype::Frameworks::Rails::ActiveJobExtension
95
+ end
96
+ end
97
+ end
98
+ end
data/stenotype.gemspec ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require "stenotype/version"
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'stenotype'
10
+ spec.version = Stenotype::VERSION
11
+ spec.authors = ["Roman Kapitonov"]
12
+ spec.email = ["roman.kapitonov@freshly.com"]
13
+
14
+ spec.summary = 'Gem for emitting events and sending them to an external system.'
15
+ spec.description = 'Pretty much it'
16
+ spec.homepage = 'https://github.com/Freshly/stenotype'
17
+ spec.license = 'MIT'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/Freshly/stenotype'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/Freshly/stenotype/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_dependency 'activesupport', '>= 5.0.0'
33
+ spec.add_dependency 'google-cloud-pubsub', '~> 1.0.0'
34
+
35
+ spec.add_development_dependency 'bundler', '~> 2.0'
36
+ spec.add_development_dependency 'github-markup', '~> 3.0'
37
+ spec.add_development_dependency 'rake', '~> 10.0'
38
+ spec.add_development_dependency 'redcarpet', '~> 3.5'
39
+ spec.add_development_dependency 'yard', '~> 0.9'
40
+
41
+ spec.add_development_dependency 'pry', '~> 0.12'
42
+ spec.add_development_dependency 'rails', '~> 5.2.3'
43
+ spec.add_development_dependency 'rspec', '~> 3.0'
44
+ spec.add_development_dependency 'rubocop', '~> 0.76'
45
+ spec.add_development_dependency 'simplecov', '~> 0.17'
46
+ spec.add_development_dependency 'timecop', '~> 0.9'
47
+ end