stenotype 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|