stenotype 0.1.0 → 0.1.6

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.
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