stenotype 0.1.0 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +3 -2
  4. data/CHANGELOG.md +43 -0
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +111 -60
  7. data/README.md +48 -17
  8. data/Rakefile +2 -2
  9. data/TODO.md +18 -0
  10. data/bin/console +3 -3
  11. data/lib/generators/USAGE +8 -0
  12. data/lib/generators/stenotype/initializer/initializer_generator.rb +23 -0
  13. data/lib/generators/stenotype/initializer/templates/initializer.rb.erb +82 -0
  14. data/lib/stenotype.rb +24 -85
  15. data/lib/stenotype/adapters.rb +3 -3
  16. data/lib/stenotype/adapters/base.rb +25 -2
  17. data/lib/stenotype/adapters/google_cloud.rb +70 -22
  18. data/lib/stenotype/adapters/stdout_adapter.rb +36 -2
  19. data/lib/stenotype/at_exit.rb +8 -0
  20. data/lib/stenotype/configuration.rb +127 -36
  21. data/lib/stenotype/context_handlers.rb +6 -8
  22. data/lib/stenotype/context_handlers/base.rb +14 -3
  23. data/lib/stenotype/context_handlers/collection.rb +78 -34
  24. data/lib/stenotype/context_handlers/rails/active_job.rb +3 -11
  25. data/lib/stenotype/context_handlers/rails/controller.rb +10 -11
  26. data/lib/stenotype/dispatcher.rb +1 -2
  27. data/lib/stenotype/emitter.rb +166 -0
  28. data/lib/stenotype/event.rb +48 -25
  29. data/lib/stenotype/event_serializer.rb +31 -11
  30. data/lib/stenotype/frameworks/rails/action_controller.rb +44 -21
  31. data/lib/stenotype/frameworks/rails/active_job.rb +3 -5
  32. data/lib/stenotype/railtie.rb +37 -0
  33. data/lib/stenotype/version.rb +1 -1
  34. data/stenotype.gemspec +30 -26
  35. metadata +70 -19
  36. data/lib/stenotype/exceptions.rb +0 -31
  37. 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: request.controller_class.name,
18
- method: request.method,
19
- url: request.url,
20
- referer: request.referer,
21
- params: request.params.except('controller', 'action'),
22
- ip: request.remote_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
- private
27
-
28
- def request
29
- context.request
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
@@ -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
@@ -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 data {Hash} Data to be published to the targets.
16
- # @param options {Hash} A hash of additional options to be tracked.
17
- # @param eval_context {Hash} A context having handler defined in {Stenotype::ContextHandlers}.
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!(data, options: {}, eval_context: {}, dispatcher: Stenotype.config.dispatcher)
22
- event = new(data, options: options, eval_context: eval_context, dispatcher: dispatcher)
23
- event.emit!
24
- event
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 :data, :options, :eval_context, :dispatcher
38
+ attr_reader :name, :attributes, :eval_context, :dispatcher
28
39
 
29
40
  #
30
- # @example
31
- #
32
- # Stenotype::Event.emit!(data, options, eval_context)
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 {Hash} data Data to be published to the targets.
35
- # @param {Hash} options A hash of additional options to be tracked.
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(data, options: {}, eval_context: {}, dispatcher: Stenotype.config.dispatcher)
41
- @data = data
42
- @options = options
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 = Stenotype::Event.new(data, options, eval_context)
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
- dispatcher.publish(self)
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
- **event_data,
27
- **event_options,
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 event_data
36
- event.data
54
+ def event_name
55
+ event.name
37
56
  end
38
57
 
39
- def event_options
40
- event.options
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.reduce(:merge!) || {}
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 data {Hash} Data to be sent to targets
23
+ # @param name {[String, Symbol]} Event name
24
+ # @todo What is really the name here?
18
25
  #
19
- def record_freshly_event(data)
20
- Stenotype::Event.emit!(data, options: {}, eval_context: { controller: self })
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
- # @example
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
- record_freshly_event(type: 'view')
76
+ _record_freshly_event("view")
63
77
  end
64
78
 
65
79
  _tracked_actions.merge(delta)
66
80
  end
67
81
 
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.
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 MyController < ActionController::Base
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
- # # do_something
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 = self.action_methods
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
- record_freshly_event(type: "view")
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