stenotype 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +6 -0
- data/.travis.yml +7 -0
- data/.yardopts +2 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +239 -0
- data/LICENSE.txt +21 -0
- data/README.md +251 -0
- data/Rakefile +8 -0
- data/TODO.md +17 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/stenotype/adapters/base.rb +36 -0
- data/lib/stenotype/adapters/google_cloud.rb +56 -0
- data/lib/stenotype/adapters/stdout_adapter.rb +23 -0
- data/lib/stenotype/adapters.rb +5 -0
- data/lib/stenotype/configuration.rb +49 -0
- data/lib/stenotype/context_handlers/base.rb +52 -0
- data/lib/stenotype/context_handlers/collection.rb +64 -0
- data/lib/stenotype/context_handlers/klass.rb +20 -0
- data/lib/stenotype/context_handlers/rails/active_job.rb +43 -0
- data/lib/stenotype/context_handlers/rails/controller.rb +34 -0
- data/lib/stenotype/context_handlers.rb +32 -0
- data/lib/stenotype/dispatcher.rb +37 -0
- data/lib/stenotype/event.rb +59 -0
- data/lib/stenotype/event_serializer.rb +61 -0
- data/lib/stenotype/exceptions.rb +31 -0
- data/lib/stenotype/frameworks/object_ext.rb +145 -0
- data/lib/stenotype/frameworks/rails/action_controller.rb +110 -0
- data/lib/stenotype/frameworks/rails/active_job.rb +57 -0
- data/lib/stenotype/version.rb +7 -0
- data/lib/stenotype.rb +98 -0
- data/stenotype.gemspec +47 -0
- metadata +262 -0
@@ -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
|
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
|