stenotype 0.1.0 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +3 -2
- data/CHANGELOG.md +43 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +111 -60
- data/README.md +48 -17
- data/Rakefile +2 -2
- data/TODO.md +18 -0
- data/bin/console +3 -3
- data/lib/generators/USAGE +8 -0
- data/lib/generators/stenotype/initializer/initializer_generator.rb +23 -0
- data/lib/generators/stenotype/initializer/templates/initializer.rb.erb +82 -0
- data/lib/stenotype.rb +24 -85
- data/lib/stenotype/adapters.rb +3 -3
- data/lib/stenotype/adapters/base.rb +25 -2
- data/lib/stenotype/adapters/google_cloud.rb +70 -22
- data/lib/stenotype/adapters/stdout_adapter.rb +36 -2
- data/lib/stenotype/at_exit.rb +8 -0
- data/lib/stenotype/configuration.rb +127 -36
- data/lib/stenotype/context_handlers.rb +6 -8
- data/lib/stenotype/context_handlers/base.rb +14 -3
- data/lib/stenotype/context_handlers/collection.rb +78 -34
- data/lib/stenotype/context_handlers/rails/active_job.rb +3 -11
- data/lib/stenotype/context_handlers/rails/controller.rb +10 -11
- data/lib/stenotype/dispatcher.rb +1 -2
- data/lib/stenotype/emitter.rb +166 -0
- data/lib/stenotype/event.rb +48 -25
- data/lib/stenotype/event_serializer.rb +31 -11
- data/lib/stenotype/frameworks/rails/action_controller.rb +44 -21
- data/lib/stenotype/frameworks/rails/active_job.rb +3 -5
- data/lib/stenotype/railtie.rb +37 -0
- data/lib/stenotype/version.rb +1 -1
- data/stenotype.gemspec +30 -26
- metadata +70 -19
- data/lib/stenotype/exceptions.rb +0 -31
- data/lib/stenotype/frameworks/object_ext.rb +0 -145
@@ -14,20 +14,19 @@ module Stenotype
|
|
14
14
|
#
|
15
15
|
def as_json(*_args)
|
16
16
|
{
|
17
|
-
class:
|
18
|
-
method:
|
19
|
-
url:
|
20
|
-
referer:
|
21
|
-
params:
|
22
|
-
ip:
|
17
|
+
class: controller_class.name,
|
18
|
+
method: method,
|
19
|
+
url: url,
|
20
|
+
referer: referer,
|
21
|
+
params: params.except("controller", "action"),
|
22
|
+
ip: remote_ip,
|
23
23
|
}
|
24
24
|
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
26
|
+
delegate :request, to: :context
|
27
|
+
delegate :method, :url, :referer, :remote_ip, :params,
|
28
|
+
:controller_class, to: :request
|
29
|
+
private :request, :method, :url, :referer, :remote_ip, :params, :controller_class
|
31
30
|
end
|
32
31
|
end
|
33
32
|
end
|
data/lib/stenotype/dispatcher.rb
CHANGED
@@ -9,8 +9,7 @@ module Stenotype
|
|
9
9
|
#
|
10
10
|
# Publishes an event to the list of configured targets.
|
11
11
|
#
|
12
|
-
# @example
|
13
|
-
#
|
12
|
+
# @example Manually dispatching an event
|
14
13
|
# event = Stenotype::Event.new(data, options, eval_context)
|
15
14
|
# Stenotype::Dispatcher.new.publish(event)
|
16
15
|
#
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stenotype
|
4
|
+
#
|
5
|
+
# An extension for a plain Ruby class in order to track invocation of
|
6
|
+
# instance methods.
|
7
|
+
#
|
8
|
+
module Emitter
|
9
|
+
#
|
10
|
+
# Class methods for target to be extended by
|
11
|
+
#
|
12
|
+
ClassMethodsExtension = Class.new(Module)
|
13
|
+
#
|
14
|
+
# Instance methods to be included into target class ancestors chain
|
15
|
+
#
|
16
|
+
InstanceMethodsExtension = Class.new(Module)
|
17
|
+
|
18
|
+
# @!visibility private
|
19
|
+
def self.included(klass)
|
20
|
+
instance_mod = InstanceMethodsExtension.new
|
21
|
+
class_mod = ClassMethodsExtension.new
|
22
|
+
|
23
|
+
build_instance_methods(instance_mod)
|
24
|
+
build_class_methods(class_mod)
|
25
|
+
|
26
|
+
klass.const_set(:InstanceProxy, Module.new)
|
27
|
+
klass.const_set(:ClassProxy, Module.new)
|
28
|
+
|
29
|
+
klass.public_send(:include, instance_mod)
|
30
|
+
klass.extend(class_mod)
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# @!method emit_event(name, attributes = {}, method: caller_locations.first.label, eval_context: nil)
|
37
|
+
# A method injected into target class to manually track events
|
38
|
+
# @!scope instance
|
39
|
+
# @param name {[String, Symbol]} Event name.
|
40
|
+
# @param attributes {Hash} Data to be sent to the targets.
|
41
|
+
# @param method {String} An optional method name.
|
42
|
+
# @param eval_context {Hash} A hash linking object to context handler
|
43
|
+
# @return {Stenotype::Event} An instance of emitted event
|
44
|
+
# @example Usage of emit_event
|
45
|
+
# class SomeRubyClass
|
46
|
+
# include Stenotype::Emitter
|
47
|
+
#
|
48
|
+
# def some_method
|
49
|
+
# data = collection_data
|
50
|
+
# emit_event(data, eval_context: self) # Track event with given data
|
51
|
+
# data
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
|
56
|
+
#
|
57
|
+
# @!method emit_event_before(*methods)
|
58
|
+
# A class method injected into target class to track instance methods invocation
|
59
|
+
# @param methods {Array<Symbol>} A list of method before which an event will be emitted
|
60
|
+
# @!scope class
|
61
|
+
# @example Usage of emit_event_before
|
62
|
+
# class SomeRubyClass
|
63
|
+
# include Stenotype::Emitter
|
64
|
+
# emit_event_before :some_method # Triggers an event upon calling some_method
|
65
|
+
#
|
66
|
+
# def some_method
|
67
|
+
# # do something
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
|
72
|
+
#
|
73
|
+
# @!method emit_klass_event_before(*class_methods)
|
74
|
+
# A class method injected into a target class to track class methods invocation
|
75
|
+
# @!scope class
|
76
|
+
# @param class_methods {Array<Symbol>} A list of class method before which
|
77
|
+
# an event will be emitted
|
78
|
+
# @example Usage emit_klass_event_before
|
79
|
+
# class SomeRubyClass
|
80
|
+
# include Stenotype::Emitter
|
81
|
+
# emit_klass_event_before :some_method # Triggers an event upon calling some_method
|
82
|
+
#
|
83
|
+
# def self.some_method
|
84
|
+
# # do something
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
|
89
|
+
#
|
90
|
+
# Adds an instance method: [#emit_event] to a target class
|
91
|
+
# where {Stenotype::Emitter} in included in
|
92
|
+
#
|
93
|
+
def self.build_instance_methods(instance_mod)
|
94
|
+
instance_mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
95
|
+
def emit_event(event_name, attributes = {}, method: caller_locations.first.label, eval_context: nil)
|
96
|
+
Stenotype::Event.emit!(
|
97
|
+
event_name,
|
98
|
+
{
|
99
|
+
type: "instance_method",
|
100
|
+
class: self.class.name,
|
101
|
+
method: method.to_sym,
|
102
|
+
**attributes,
|
103
|
+
},
|
104
|
+
eval_context: (eval_context || { klass: self })
|
105
|
+
)
|
106
|
+
end
|
107
|
+
RUBY
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Adds class method [#emit_klass_event_before] to every class
|
112
|
+
# inherited from [Object]
|
113
|
+
#
|
114
|
+
def self.build_class_methods(class_mod)
|
115
|
+
class_mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
116
|
+
def emit_event_before(*methods)
|
117
|
+
proxy = const_get(:InstanceProxy)
|
118
|
+
|
119
|
+
methods.each do |method|
|
120
|
+
proxy.module_eval do
|
121
|
+
define_method(method) do |*args, **rest_args, &block|
|
122
|
+
Stenotype::Event.emit!(
|
123
|
+
# @todo How do we name such events?
|
124
|
+
"instance_method",
|
125
|
+
{
|
126
|
+
type: "instance_method",
|
127
|
+
class: self.class.name,
|
128
|
+
method: __method__,
|
129
|
+
},
|
130
|
+
eval_context: { klass: self },
|
131
|
+
)
|
132
|
+
super(*args, **rest_args, &block)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
send(:prepend, proxy)
|
138
|
+
end
|
139
|
+
|
140
|
+
def emit_klass_event_before(*class_methods)
|
141
|
+
proxy = const_get(:ClassProxy)
|
142
|
+
|
143
|
+
class_methods.each do |method|
|
144
|
+
proxy.module_eval do
|
145
|
+
define_method(method) do |*args, **rest_args, &block|
|
146
|
+
Stenotype::Event.emit!(
|
147
|
+
# @todo How do we name such events?
|
148
|
+
"class_method",
|
149
|
+
{
|
150
|
+
type: "class_method",
|
151
|
+
class: self.name,
|
152
|
+
method: __method__,
|
153
|
+
},
|
154
|
+
eval_context: { klass: self },
|
155
|
+
)
|
156
|
+
super(*args, **rest_args, &block)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
singleton_class.send(:prepend, proxy)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
RUBY
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
data/lib/stenotype/event.rb
CHANGED
@@ -8,52 +8,75 @@ module Stenotype
|
|
8
8
|
#
|
9
9
|
# Delegates event to instance of {Stenotype::Event}.
|
10
10
|
#
|
11
|
-
# @example
|
12
|
-
#
|
11
|
+
# @example Emit an event using class method
|
13
12
|
# Stenotype::Event.emit!(data, options, eval_context)
|
14
13
|
#
|
15
|
-
# @param
|
16
|
-
# @param
|
17
|
-
# @param
|
18
|
-
# @param dispatcher {#publish} A dispatcher object responding to [#publish]
|
14
|
+
# @param {[String, Symbol]} name Event name.
|
15
|
+
# @param {Hash} attributes Data to be published to the targets.
|
16
|
+
# @param {Hash} eval_context A context having handler defined in {Stenotype::ContextHandlers}.
|
17
|
+
# @param dispatcher {#publish} A dispatcher object responding to [#publish].
|
19
18
|
# @return {Stenotype::Event} An instance of {Stenotype::Event}
|
20
19
|
#
|
21
|
-
def self.emit!(
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
def self.emit!(name, attributes = {}, eval_context: {}, dispatcher: Stenotype.config.dispatcher)
|
21
|
+
return unless Stenotype.config.enabled
|
22
|
+
|
23
|
+
begin
|
24
|
+
event = new(name, attributes, eval_context: eval_context, dispatcher: dispatcher)
|
25
|
+
event.emit!
|
26
|
+
event
|
27
|
+
rescue => error
|
28
|
+
#
|
29
|
+
# @todo This is a temporary solution to enable conditional logger fetching
|
30
|
+
# needs a fix to use default Spicerack::Configuration functionality
|
31
|
+
#
|
32
|
+
Stenotype::Configuration.logger.error(error)
|
33
|
+
|
34
|
+
raise Stenotype::Error unless Stenotype.config.graceful_error_handling
|
35
|
+
end
|
25
36
|
end
|
26
37
|
|
27
|
-
attr_reader :
|
38
|
+
attr_reader :name, :attributes, :eval_context, :dispatcher
|
28
39
|
|
29
40
|
#
|
30
|
-
# @example
|
31
|
-
#
|
32
|
-
#
|
41
|
+
# @example Create an event
|
42
|
+
# event = Stenotype::Event.new(data, options, eval_context)
|
43
|
+
# @example Create an event with custom dispatcher
|
44
|
+
# event = Stenotype::Event.new(data, options, eval_context, dispatcher: MyDispatcher.new)
|
33
45
|
#
|
34
|
-
# @param {
|
35
|
-
# @param {Hash}
|
46
|
+
# @param {[String, Symbol]} name Event name.
|
47
|
+
# @param {Hash} attributes Data to be published to the targets.
|
36
48
|
# @param {Hash} eval_context A context having handler defined in {Stenotype::ContextHandlers}.
|
37
49
|
# @param dispatcher {#publish} A dispatcher object responding to [#publish].
|
38
50
|
# @return {Stenotype::Event} An instance of event
|
39
51
|
#
|
40
|
-
def initialize(
|
41
|
-
@
|
42
|
-
@
|
52
|
+
def initialize(name, attributes = {}, eval_context: {}, dispatcher: Stenotype.config.dispatcher)
|
53
|
+
@name = name
|
54
|
+
@attributes = attributes
|
43
55
|
@eval_context = eval_context
|
44
|
-
@dispatcher = dispatcher
|
56
|
+
@dispatcher = dispatcher.new
|
45
57
|
end
|
46
58
|
|
47
59
|
#
|
48
60
|
# Emits a {Stenotype::Event}.
|
49
61
|
#
|
50
|
-
# @example
|
51
|
-
#
|
52
|
-
# event
|
53
|
-
# event.emit!
|
62
|
+
# @example Emit an instance of event
|
63
|
+
# event = Stenotype::Event.new('events_name', { key: :value }, eval_context: { controller: ctrl })
|
64
|
+
# event.emit! #=> Publishes the event to targets
|
54
65
|
#
|
55
66
|
def emit!
|
56
|
-
|
67
|
+
return unless Stenotype.config.enabled
|
68
|
+
|
69
|
+
begin
|
70
|
+
dispatcher.publish(self)
|
71
|
+
rescue => error
|
72
|
+
#
|
73
|
+
# @todo This is a temporary solution to enable conditional logger fetching
|
74
|
+
# needs a fix to use default Spicerack::Configuration functionality
|
75
|
+
#
|
76
|
+
Stenotype::Configuration.logger.error(error)
|
77
|
+
|
78
|
+
raise Stenotype::Error unless Stenotype.config.graceful_error_handling
|
79
|
+
end
|
57
80
|
end
|
58
81
|
end
|
59
82
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require 'securerandom'
|
3
2
|
|
4
3
|
module Stenotype
|
5
4
|
#
|
@@ -9,6 +8,14 @@ module Stenotype
|
|
9
8
|
class EventSerializer
|
10
9
|
attr_reader :event, :uuid_generator
|
11
10
|
|
11
|
+
#
|
12
|
+
# @example Serializing an event with default UUID generator
|
13
|
+
# event = Stenotype::Event.new(data, attributes, eval_context)
|
14
|
+
# serializer = Stenotype::EventSerializer.new(event)
|
15
|
+
#
|
16
|
+
# @example Serializing an event with custom UUID generator
|
17
|
+
# event = Stenotype::Event.new(data, attributes, eval_context)
|
18
|
+
# serializer = Stenotype::EventSerializer.new(event, uuid_generator: CustomUUIDGen)
|
12
19
|
#
|
13
20
|
# @param event {Stenotype::Event}
|
14
21
|
# @param uuid_generator {#uuid} an object responding to [#uuid]
|
@@ -18,26 +25,38 @@ module Stenotype
|
|
18
25
|
@uuid_generator = uuid_generator
|
19
26
|
end
|
20
27
|
|
28
|
+
#
|
29
|
+
# @example Serializing an event with default uuid generator (SecureRandom)
|
30
|
+
# event = Stenotype::Event.new(data, attributes, eval_context)
|
31
|
+
# serializer = Stenotype::EventSerializer.new(event)
|
32
|
+
# serializer.serialize #=> A hash with event.data, event.options,
|
33
|
+
# # default_options and eval_context_options
|
34
|
+
#
|
35
|
+
# @example Serializing an event with custom uuid generator
|
36
|
+
# event = Stenotype::Event.new(data, attributes, eval_context)
|
37
|
+
# serializer = Stenotype::EventSerializer.new(event, uuid_generator: CustomUUIDGen)
|
38
|
+
# serializer.serialize #=> A hash with event.data, event.options,
|
39
|
+
# # default_options and eval_context_options
|
21
40
|
#
|
22
41
|
# @return {Hash} A hash representation of the event and its context
|
23
42
|
#
|
24
43
|
def serialize
|
25
44
|
{
|
26
|
-
|
27
|
-
**
|
45
|
+
name: event_name,
|
46
|
+
**event_attributes,
|
28
47
|
**default_options,
|
29
|
-
**eval_context_options
|
48
|
+
**eval_context_options,
|
30
49
|
}
|
31
50
|
end
|
32
51
|
|
33
52
|
private
|
34
53
|
|
35
|
-
def
|
36
|
-
event.
|
54
|
+
def event_name
|
55
|
+
event.name
|
37
56
|
end
|
38
57
|
|
39
|
-
def
|
40
|
-
event.
|
58
|
+
def event_attributes
|
59
|
+
event.attributes
|
41
60
|
end
|
42
61
|
|
43
62
|
def eval_context
|
@@ -45,16 +64,17 @@ module Stenotype
|
|
45
64
|
end
|
46
65
|
|
47
66
|
def eval_context_options
|
48
|
-
eval_context.map do |context_name, context|
|
67
|
+
context_attributes = eval_context.map do |context_name, context|
|
49
68
|
handler = Stenotype::ContextHandlers.known.choose(handler_name: context_name)
|
50
69
|
handler.new(context).as_json
|
51
|
-
end
|
70
|
+
end
|
71
|
+
context_attributes.reduce(:merge!) || {}
|
52
72
|
end
|
53
73
|
|
54
74
|
def default_options
|
55
75
|
{
|
56
76
|
timestamp: Time.now.utc,
|
57
|
-
uuid: uuid_generator.uuid
|
77
|
+
uuid: uuid_generator.uuid,
|
58
78
|
}
|
59
79
|
end
|
60
80
|
end
|
@@ -3,6 +3,10 @@
|
|
3
3
|
require "active_support/concern"
|
4
4
|
|
5
5
|
module Stenotype
|
6
|
+
#
|
7
|
+
# A namespace containing extensions of various frameworks.
|
8
|
+
# For example Rails components could be extended
|
9
|
+
#
|
6
10
|
module Frameworks
|
7
11
|
module Rails
|
8
12
|
#
|
@@ -12,12 +16,15 @@ module Stenotype
|
|
12
16
|
module ActionControllerExtension
|
13
17
|
extend ActiveSupport::Concern
|
14
18
|
|
19
|
+
private
|
20
|
+
|
15
21
|
#
|
16
22
|
# Emits and event with given data
|
17
|
-
# @param
|
23
|
+
# @param name {[String, Symbol]} Event name
|
24
|
+
# @todo What is really the name here?
|
18
25
|
#
|
19
|
-
def
|
20
|
-
Stenotype::Event.emit!(
|
26
|
+
def _record_freshly_event(name)
|
27
|
+
Stenotype::Event.emit!(name, { type: "controller_action" }, { eval_context: { controller: self }})
|
21
28
|
end
|
22
29
|
|
23
30
|
#
|
@@ -28,12 +35,19 @@ module Stenotype
|
|
28
35
|
# Adds a before_action to each action from the passed list. A before action
|
29
36
|
# is emitting a {Stenotype::Event}. Please note that in case track_view is
|
30
37
|
# 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
38
|
#
|
34
|
-
# @
|
39
|
+
# @note Each time a new track_view is called it will find a symmetric difference
|
40
|
+
# of two sets: set of already tracked actions and a set passed to `track_view`.
|
41
|
+
#
|
42
|
+
# @example Tracking multiple actions with track_view
|
43
|
+
# Stenotype.configure do |config|
|
44
|
+
# config.enable_action_controller_extension = true
|
45
|
+
# config.enable_active_job_extension = true
|
46
|
+
# end
|
47
|
+
#
|
35
48
|
# class MyController < ActionController::Base
|
36
|
-
# track_view :index, :show
|
49
|
+
# track_view :index, :show # Emits an event upon calling index and show actions,
|
50
|
+
# # but does not trigger an event on create
|
37
51
|
#
|
38
52
|
# def index
|
39
53
|
# # do_something
|
@@ -59,36 +73,36 @@ module Stenotype
|
|
59
73
|
return if delta.empty?
|
60
74
|
|
61
75
|
before_action only: delta do
|
62
|
-
|
76
|
+
_record_freshly_event("view")
|
63
77
|
end
|
64
78
|
|
65
79
|
_tracked_actions.merge(delta)
|
66
80
|
end
|
67
81
|
|
68
|
-
#
|
69
|
-
|
70
|
-
|
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.
|
82
|
+
#
|
83
|
+
# @note This action will only define a symmetric difference of
|
84
|
+
# the tracked actions and the ones not tracked yet.
|
75
85
|
# @see #track_view
|
76
86
|
#
|
77
|
-
# @example
|
78
|
-
# class
|
79
|
-
# track_all_views
|
87
|
+
# @example Emitting an event before all actions in controller
|
88
|
+
# class UsersController < ApplicationController
|
89
|
+
# track_all_views # Emits an event before all actions in a controller
|
80
90
|
#
|
81
91
|
# def index
|
82
|
-
# #
|
92
|
+
# # do something
|
83
93
|
# end
|
84
94
|
#
|
85
95
|
# def show
|
86
96
|
# # do something
|
87
97
|
# end
|
98
|
+
#
|
99
|
+
# def create
|
100
|
+
# # do something
|
101
|
+
# end
|
88
102
|
# end
|
89
103
|
#
|
90
104
|
def track_all_views
|
91
|
-
actions =
|
105
|
+
actions = action_methods
|
92
106
|
|
93
107
|
# A symmetric difference of two sets.
|
94
108
|
# This prevents accidental duplicating of events
|
@@ -97,12 +111,21 @@ module Stenotype
|
|
97
111
|
return if delta.empty?
|
98
112
|
|
99
113
|
before_action only: delta.to_a do
|
100
|
-
|
114
|
+
_record_freshly_event("view")
|
101
115
|
end
|
102
116
|
|
103
117
|
# merge is a mutating op
|
104
118
|
_tracked_actions.merge(delta)
|
105
119
|
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
#
|
124
|
+
# @return {Set<Symbol>} a set of tracked actions
|
125
|
+
#
|
126
|
+
def _tracked_actions
|
127
|
+
@_tracked_actions ||= Set.new
|
128
|
+
end
|
106
129
|
end
|
107
130
|
end
|
108
131
|
end
|