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.
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'google/cloud/pubsub'
4
+
5
+ module Stenotype
6
+ module Adapters
7
+ #
8
+ # An adapter implementing method {#publish} to send data to Google Cloud PubSub
9
+ #
10
+ class GoogleCloud < Base
11
+ #
12
+ # @param event_data {Hash} The data to be published to Google Cloud
13
+ # @raise {Stenotype::Exceptions::GoogleCloudUnsupportedMode} unless the mode
14
+ # in configured to be :sync or :async
15
+ # @raise {Stenotype::Exceptions::MessageNotPublished} unless message is published
16
+ #
17
+ # rubocop:disable Metrics/MethodLength
18
+ #
19
+ def publish(event_data, **additional_arguments)
20
+ case config.gc_mode
21
+ when :async
22
+ topic.publish_async(event_data, additional_arguments) do |result|
23
+ raise Stenotype::Exceptions::MessageNotPublished unless result.succeeded?
24
+ end
25
+ when :sync
26
+ topic.publish(event_data, additional_arguments)
27
+ else
28
+ raise Stenotype::Exceptions::GoogleCloudUnsupportedMode,
29
+ 'Please use either :sync or :async modes for publishing the events.'
30
+ end
31
+ end
32
+ # rubocop:enable Metrics/MethodLength
33
+
34
+ private
35
+
36
+ # :nocov:
37
+ def client
38
+ @client ||= Google::Cloud::PubSub.new(
39
+ project_id: config.gc_project_id,
40
+ credentials: config.gc_credentials
41
+ )
42
+ end
43
+
44
+ # Use memoization, otherwise a new topic will be created
45
+ # every time. And a new async_publisher will be created.
46
+ # :nocov:
47
+ def topic
48
+ @topic ||= client.topic config.gc_topic
49
+ end
50
+
51
+ def config
52
+ Stenotype.config
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ module Adapters
5
+ #
6
+ # An adapter implementing method {#publish} to send data to STDOUT
7
+ #
8
+ class StdoutAdapter < Base
9
+ #
10
+ # @param event_data {Hash} The data to be published to STDOUT
11
+ #
12
+ def publish(event_data, **additional_arguments)
13
+ client.info(event_data, **additional_arguments)
14
+ end
15
+
16
+ private
17
+
18
+ def client
19
+ @client ||= Logger.new(STDOUT)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stenotype/adapters/base'
4
+ require 'stenotype/adapters/google_cloud'
5
+ require 'stenotype/adapters/stdout_adapter'
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ #
5
+ # A module containing freshly-event gem configuration
6
+ #
7
+ module Configuration
8
+ class << self
9
+ # @return {Array<#publish>} a list of targets responding to method [#publish]
10
+ attr_writer :targets
11
+ # @return {Sting} a string with GC API credential. Refer to GC PubSub documentation
12
+ attr_accessor :gc_credentials
13
+ # @return {String} a name of the project in GC PubSub
14
+ attr_accessor :gc_project_id
15
+ # @return {String} a name of the topic in GC PubSub
16
+ attr_accessor :gc_topic
17
+ # @return [:sync, :async] GC publish mode
18
+ attr_accessor :gc_mode
19
+ # @return {#publish} as object responding to method [#publish]
20
+ attr_accessor :dispatcher
21
+ # @return {#uuid} an object responding to method [#uuid]
22
+ attr_accessor :uuid_generator
23
+
24
+ #
25
+ # Yields control to the caller
26
+ # @yield {Stenotype::Configuration}
27
+ # @return {Stenotype::Configuration}
28
+ #
29
+ def configure
30
+ yield self
31
+ self
32
+ end
33
+
34
+ #
35
+ # @raise {Stenotype::Exceptions::NoTargetsSpecified} in case no targets are configured
36
+ # @return {Array<#publish>} An array of targets implementing method [#publish]
37
+ #
38
+ def targets
39
+ if @targets.nil? || @targets.empty?
40
+ raise Stenotype::Exceptions::NoTargetsSpecified,
41
+ 'Please configure a target(s) for events to be sent to. ' \
42
+ 'See Stenotype::Configuration for reference.'
43
+ else
44
+ @targets
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ module ContextHandlers
5
+ #
6
+ # An abstract base class for implementing contexts handlers
7
+ #
8
+ # @abstract
9
+ # @attr_reader {Object} context A context in which the event was emitted
10
+ # @attr_reader {Hash} options A hash of additional options
11
+ #
12
+ class Base
13
+ attr_reader :context, :options
14
+
15
+ #
16
+ # @param context {Object} A context where the event was emitted
17
+ # @param options {Hash} A hash of additional options
18
+ #
19
+ # @return {#as_json} A context handler implementing [#as_json]
20
+ #
21
+ def initialize(context, options = {})
22
+ @context = context
23
+ @options = options
24
+ end
25
+
26
+ #
27
+ # @abstract
28
+ # @raise {NotImplementedError} subclasses must implement this method
29
+ #
30
+ def as_json(*_args)
31
+ raise NotImplementedError, "#{self} must implement method ##{__method__}"
32
+ end
33
+
34
+ #
35
+ # @attr_writer {Symbol} handler_name The name under which a handler is going to be registered
36
+ #
37
+ class << self
38
+ # Handler name by which it will be registered in {Stenotype::ContextHandlers::Collection}
39
+ attr_writer :handler_name
40
+
41
+ #
42
+ # @return {Symbol} Name of the handler
43
+ # @raise {NotImplementedError} in case handler name is not specified.
44
+ #
45
+ def handler_name
46
+ @handler_name || raise(NotImplementedError,
47
+ "Please, specify the handler_name of #{self}")
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ module ContextHandlers
5
+ #
6
+ # A class representing a list of available context handlers
7
+ #
8
+ class Collection < Array
9
+ #
10
+ # @param handler_name {Symbol} a handler to be found in the collection
11
+ # @raise {Stenotype::Exceptions::UnknownHandler} in case a handler is not registered
12
+ # @return {#as_json} A handler which respond to #as_json
13
+ #
14
+ def choose(handler_name:)
15
+ handler = detect { |e| e.handler_name == handler_name }
16
+ handler || raise(Stenotype::Exceptions::UnknownHandler,
17
+ "Handler '#{handler_name}' is not found. " \
18
+ "Please make sure the handler you've specified is " \
19
+ 'registered in the list of known handlers. ' \
20
+ "See #{Stenotype::ContextHandlers} for more information.")
21
+ end
22
+
23
+ #
24
+ # @param handler {#as_json} a new handler to be added to collection
25
+ # @raise {ArgumentError} in case handler does not inherit from {Stenotype::ContextHandlers::Base}
26
+ # @return {Stenotype::ContextHandlers::Collection} a collection object
27
+ #
28
+ def register(handler)
29
+ unless handler < Stenotype::ContextHandlers::Base
30
+ raise ArgumentError,
31
+ "Handler must inherit from #{Stenotype::ContextHandlers::Base}, " \
32
+ "but inherited from #{handler.superclass}"
33
+ end
34
+
35
+ push(handler) unless registered?(handler)
36
+ self
37
+ end
38
+
39
+ #
40
+ # @param handler {#as_json} a handler to be removed from the collection of known handlers
41
+ # @raise {ArgumentError} in case handler does not inherit from {Stenotype::ContextHandlers::Base}
42
+ # @return {Stenotype::ContextHandlers::Collection} a collection object
43
+ #
44
+ def unregister(handler)
45
+ unless handler < Stenotype::ContextHandlers::Base
46
+ raise ArgumentError,
47
+ "Handler must inherit from #{Stenotype::ContextHandlers::Base}, " \
48
+ "but inherited from #{handler.superclass}"
49
+ end
50
+
51
+ delete(handler) if registered?(handler)
52
+ self
53
+ end
54
+
55
+ #
56
+ # @param handler {#as_json} a handler to be checked for presence in a collection
57
+ # @return [true, false]
58
+ #
59
+ def registered?(handler)
60
+ include?(handler)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ module ContextHandlers
5
+ #
6
+ # Plain Ruby class handler to support fetching data from a class
7
+ #
8
+ class Klass < Stenotype::ContextHandlers::Base
9
+ self.handler_name = :klass
10
+
11
+ #
12
+ # @todo r.kapitonov Figure out the params
13
+ # @return {Hash} a JSON representation of a Ruby class
14
+ #
15
+ def as_json(*_args)
16
+ {}
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ module ContextHandlers
5
+ #
6
+ # A namespace containing extension of rails components
7
+ # for fetching specific data from those components.
8
+ # For example fetching request from a controller context,
9
+ # or fetching ActiveJob attributes from an ActiveJob instance
10
+ #
11
+ module Rails
12
+ #
13
+ # ActiveJob handler to support fetching data out of an ActiveJob instance
14
+ #
15
+ class ActiveJob < Stenotype::ContextHandlers::Base
16
+ self.handler_name = :active_job
17
+
18
+ #
19
+ # @todo How to deal with _args? It won't necessarily respond to #as_json
20
+ # @return {Hash} a JSON representation of job's data
21
+ #
22
+ def as_json(*_args)
23
+ {
24
+ job_id: job_id,
25
+ enqueued_at: Time.now,
26
+ queue_name: queue_name,
27
+ class: context.class.name
28
+ }
29
+ end
30
+
31
+ private
32
+
33
+ def job_id
34
+ context.job_id
35
+ end
36
+
37
+ def queue_name
38
+ context.queue_name
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ module ContextHandlers
5
+ module Rails
6
+ #
7
+ # ActionController handler to support fetching data out of a rails controller instance
8
+ #
9
+ class Controller < Stenotype::ContextHandlers::Base
10
+ self.handler_name = :controller
11
+
12
+ #
13
+ # @return {Hash} a JSON representation of controller's data
14
+ #
15
+ def as_json(*_args)
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
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def request
29
+ context.request
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stenotype/context_handlers/base'
4
+ require 'stenotype/context_handlers/rails/controller'
5
+ require 'stenotype/context_handlers/rails/active_job'
6
+ require 'stenotype/context_handlers/klass'
7
+ require 'stenotype/context_handlers/collection'
8
+
9
+ module Stenotype
10
+ #
11
+ # A namespace to contain various context
12
+ # handlers implementations
13
+ #
14
+ module ContextHandlers
15
+ class << self
16
+ attr_writer :known
17
+ #
18
+ # @return {Array<#publish>} A list of handlers implementing [#publish]
19
+ #
20
+ def known
21
+ @known ||= Stenotype::ContextHandlers::Collection.new
22
+ end
23
+
24
+ #
25
+ # @param handler {#publish} A handler with implemented method [#publish]
26
+ #
27
+ def register(handler)
28
+ known.register(handler)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ #
5
+ # {Stenotype::Dispatcher} is responsible for gluing together
6
+ # publishing targets and data gathering.
7
+ #
8
+ class Dispatcher
9
+ #
10
+ # Publishes an event to the list of configured targets.
11
+ #
12
+ # @example
13
+ #
14
+ # event = Stenotype::Event.new(data, options, eval_context)
15
+ # Stenotype::Dispatcher.new.publish(event)
16
+ #
17
+ # @param event {Stenotype::Event} An instance of event to be published.
18
+ # @param serializer {#serialize} A class responsible for serializing the event
19
+ # @return {Stenotype::Dispatcher} for the sake of chaining
20
+ #
21
+ def publish(event, serializer: Stenotype::EventSerializer)
22
+ event_data = serializer.new(event).serialize
23
+
24
+ targets.each do |t|
25
+ t.publish(event_data)
26
+ end
27
+
28
+ self
29
+ end
30
+
31
+ private
32
+
33
+ def targets
34
+ Stenotype.config.targets
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ #
5
+ # {Stenotype::Event} represents a triggered event
6
+ #
7
+ class Event
8
+ #
9
+ # Delegates event to instance of {Stenotype::Event}.
10
+ #
11
+ # @example
12
+ #
13
+ # Stenotype::Event.emit!(data, options, eval_context)
14
+ #
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]
19
+ # @return {Stenotype::Event} An instance of {Stenotype::Event}
20
+ #
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
25
+ end
26
+
27
+ attr_reader :data, :options, :eval_context, :dispatcher
28
+
29
+ #
30
+ # @example
31
+ #
32
+ # Stenotype::Event.emit!(data, options, eval_context)
33
+ #
34
+ # @param {Hash} data Data to be published to the targets.
35
+ # @param {Hash} options A hash of additional options to be tracked.
36
+ # @param {Hash} eval_context A context having handler defined in {Stenotype::ContextHandlers}.
37
+ # @param dispatcher {#publish} A dispatcher object responding to [#publish].
38
+ # @return {Stenotype::Event} An instance of event
39
+ #
40
+ def initialize(data, options: {}, eval_context: {}, dispatcher: Stenotype.config.dispatcher)
41
+ @data = data
42
+ @options = options
43
+ @eval_context = eval_context
44
+ @dispatcher = dispatcher
45
+ end
46
+
47
+ #
48
+ # Emits a {Stenotype::Event}.
49
+ #
50
+ # @example
51
+ #
52
+ # event = Stenotype::Event.new(data, options, eval_context)
53
+ # event.emit!
54
+ #
55
+ def emit!
56
+ dispatcher.publish(self)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require 'securerandom'
3
+
4
+ module Stenotype
5
+ #
6
+ # A class used to serialize a {Stenotype::Event}
7
+ # upon publishing it to targets
8
+ #
9
+ class EventSerializer
10
+ attr_reader :event, :uuid_generator
11
+
12
+ #
13
+ # @param event {Stenotype::Event}
14
+ # @param uuid_generator {#uuid} an object responding to [#uuid]
15
+ #
16
+ def initialize(event, uuid_generator: Stenotype.config.uuid_generator)
17
+ @event = event
18
+ @uuid_generator = uuid_generator
19
+ end
20
+
21
+ #
22
+ # @return {Hash} A hash representation of the event and its context
23
+ #
24
+ def serialize
25
+ {
26
+ **event_data,
27
+ **event_options,
28
+ **default_options,
29
+ **eval_context_options
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ def event_data
36
+ event.data
37
+ end
38
+
39
+ def event_options
40
+ event.options
41
+ end
42
+
43
+ def eval_context
44
+ event.eval_context
45
+ end
46
+
47
+ def eval_context_options
48
+ eval_context.map do |context_name, context|
49
+ handler = Stenotype::ContextHandlers.known.choose(handler_name: context_name)
50
+ handler.new(context).as_json
51
+ end.reduce(:merge!) || {}
52
+ end
53
+
54
+ def default_options
55
+ {
56
+ timestamp: Time.now.utc,
57
+ uuid: uuid_generator.uuid
58
+ }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stenotype
4
+ #
5
+ # A namespace for holding library-level exceptions.
6
+ #
7
+ module Exceptions
8
+ #
9
+ # This exception is being raised in case an unsupported mode
10
+ # for Google Cloud is specified.
11
+ #
12
+ class GoogleCloudUnsupportedMode < StandardError; end
13
+
14
+ #
15
+ # This exception is being raised upon unsuccessful publishing of an event.
16
+ #
17
+ class MessageNotPublished < StandardError; end
18
+
19
+ #
20
+ # This exception is being raised in case no targets are
21
+ # specified {Stenotype::Configuration}.
22
+ #
23
+ class NoTargetsSpecified < StandardError; end
24
+
25
+ #
26
+ # This exception is being raised upon using a context handler which
27
+ # has never been registered in known handlers in {Stenotype::ContextHandlers::Collection}.
28
+ #
29
+ class UnknownHandler < StandardError; end
30
+ end
31
+ end