aether_observatory 0.0.1pre4

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,318 @@
1
+ # AetherObservatory Guide
2
+
3
+ In this guide we are going to walk through example code to illustrate the usage of the `AetherObservatory::`. When finished you will have a class to create events and a class that subscribes to those events.
4
+
5
+ #### Table of Contents
6
+ - [Creating Events](#creating-events)
7
+ - [Creating an Observer and Subscribing to Events](#creating-an-observer-and-subscribing-to-events)
8
+ - [Sending an Event to your Observer](#sending-an-event-to-your-observer)
9
+ - [Stopping Observers](#stopping-observers)
10
+ - [Using Dynamic Event Names](#using-dynamic-event-names)
11
+ - [Multiple Event Topics](#multiple-event-topics)
12
+
13
+ ## Creating Events
14
+
15
+ To begin create an `ApplicationEvent` class that extends the `AetherObservatory::EventBase` class. Next configure a prefix for event names using `event_prefix`. This is optional, but encouraged to help prevent naming collisions with other domains. Every domain event we define as a sub-class to the `ApplicationEvent` will inherit this prefix.
16
+
17
+ ```ruby
18
+ module AetherObservatory
19
+ module Examples
20
+ class ApplicationEvent < AetherObservatory::EventBase
21
+ event_prefix 'talkbox'
22
+ end
23
+ end
24
+ end
25
+ ```
26
+
27
+ Next we create an event class called `ExampleEvent` that extends our `ApplicationEvent`. In this class we define the topic we would like our event sent to using the `event_name` method. Lastly we will define our data using the `attribute` method.
28
+
29
+ ```ruby
30
+ module AetherObservatory
31
+ module Examples
32
+ class ExampleEvent < AetherObservatory::Examples::ApplicationEvent
33
+ event_name 'example1'
34
+
35
+ attribute :message
36
+ attribute :timestamp, default: -> { Time.current }
37
+ end
38
+ end
39
+ end
40
+ ```
41
+
42
+ Now we have a class to create new events. Each time you create a new event, it will be sent to each topic you added via the `event_name` method.
43
+
44
+ ```ruby
45
+ AetherObservatory::Examples::ExampleEvent.create(message: 'hello world')
46
+ ```
47
+
48
+ Running the command above will display a log message like you see below.
49
+
50
+ ```irb
51
+ irb(main):018:0> AetherObservatory::Examples::ExampleEvent.create(message: 'hello world')
52
+ [AetherObservatory::Examples::ExampleEvent] Create event for topic: [talkbox.example1]
53
+ => nil
54
+ irb(main):019:0>
55
+ ```
56
+
57
+ Now that we have an `ExampleEvent` class to create events we need to create an observer to listen for those events.
58
+
59
+ <div align="right">
60
+ <a href="#aetherobservatory-guide">Top</a>
61
+ </div>
62
+
63
+ ## Creating an Observer and Subscribing to Events
64
+
65
+ Our new event class `ExampleEvent` creates a new event on the `talkbox.example1` topic so this is the topic we need to create a observer for.
66
+
67
+ We start by creating another class called `ExampleObserver` that extends the `AetherObservatory::ObserverBase` class. Next we use the `subscribe_to` method to register this observer to the topic `talkbox.example1`. We also need to define a `process` method that will be called each time your observer receives an event. In this `process` method you have access to `event_payload` and `event_name` objects for your own logic.
68
+
69
+ ```ruby
70
+ module AetherObservatory
71
+ module Examples
72
+ class ExampleObserver < AetherObservatory::ObserverBase
73
+ subscribe_to 'talkbox.example1'
74
+
75
+ def process
76
+ puts <<-EVENT
77
+ ************************************
78
+ Event processed:
79
+ Name: #{event_name.inspect}
80
+ Message: #{event_payload.message}
81
+ Timestamp: #{event_payload.timestamp}
82
+ Event Payload: #{event_payload.inspect}
83
+ ************************************
84
+ EVENT
85
+ end
86
+ end
87
+ end
88
+ end
89
+ ```
90
+ Now that we have a new observer named `ExampleObserver`, we will need to start our observer before it will process any events. Observers default to `stopped`, so we need to call `start` on each observer before they will recieve events. Inside an initilizer is the recommended location to start your observers.
91
+
92
+ ```ruby
93
+ AetherObservatory::Examples::ExampleObserver.start
94
+ ```
95
+
96
+ <div align="right">
97
+ <a href="#aetherobservatory-guide">Top</a>
98
+ </div>
99
+
100
+ ## Sending an Event to your Observer
101
+
102
+ Now that you have all your classes created you can send events to your observer via the `create` method.
103
+
104
+ ```ruby
105
+ AetherObservatory::Examples::ExampleEvent.create(message: 'hello world')
106
+ ```
107
+
108
+ Calling create on your `ExampleEvent` class will trigger the `process` method in the `ExampleObserver` class. You should see the following logged output.
109
+
110
+ ```irb
111
+ irb(main):040:0> AetherObservatory::Examples::ExampleEvent.create(message: 'hello world')
112
+ ************************************
113
+ Event processed:
114
+ Name: "talkbox.example1"
115
+ Message: hello world
116
+ Timestamp: 2024-05-23 15:17:16 UTC
117
+ Event Payload: #<AetherObservatory::Examples::ExampleEvent:0x0000aaaadc2b2118 @attributes=#<ActiveModel::AttributeSet:0x0000aaaadc2b1f38 @attributes={"message"=>#<ActiveModel::Attribute::FromUser:0x0000aaaadc2b1fb0 @name="message", @value_before_type_cast="hello world", @type=#<ActiveModel::Type::Value:0x0000aaaadc101d28 @precision=nil, @scale=nil, @limit=nil>, @original_attribute=#<ActiveModel::Attribute::WithCastValue:0x0000aaaadc2b2dc0 @name="message", @value_before_type_cast=nil, @type=#<ActiveModel::Type::Value:0x0000aaaadc101d28 @precision=nil, @scale=nil, @limit=nil>, @original_attribute=nil>, @value="hello world">, "timestamp"=>#<ActiveModel::Attribute::UserProvidedDefault:0x0000aaaadc2b1f60 @user_provided_value=#<Proc:0x0000aaaadc0f3b38 (irb):15 (lambda)>, @name="timestamp", @value_before_type_cast=#<Proc:0x0000aaaadc0f3b38 (irb):15 (lambda)>, @type=#<ActiveModel::Type::Value:0x0000aaaadc0f3ac0 @precision=nil, @scale=nil, @limit=nil>, @original_attribute=nil, @memoized_value_before_type_cast=Thu, 23 May 2024 15:17:16.082153128 UTC +00:00, @value=Thu, 23 May 2024 15:17:16.082153128 UTC +00:00>}>>
118
+ ************************************
119
+ [AetherObservatory::Examples::ExampleEvent] Create event for topic: [talkbox.example1]
120
+ => nil
121
+ ```
122
+
123
+ <div align="right">
124
+ <a href="#aetherobservatory-guide">Top</a>
125
+ </div>
126
+
127
+ ## Stopping Observers
128
+
129
+ To stop your observer from processing events you can call the `stop` method on your observer class. This stops only that observer class from processing events.
130
+
131
+ ```ruby
132
+ AetherObservatory::Examples::ExampleObserver.stop
133
+ ```
134
+
135
+ <div align="right">
136
+ <a href="#aetherobservatory-guide">Top</a>
137
+ </div>
138
+
139
+ ## Using Dynamic Event Names
140
+
141
+ Create a new class called `RandomEvent` that extends `ApplicationEvent`. Then pass a block to the `event_name` method. This allows you to dynamiclly select your topic at the time of event creation.
142
+
143
+ <sup>*Note: [ApplicationEvent](#creating-events) class was created at the beginning of this guide.*</sup>
144
+
145
+ ```ruby
146
+ module AetherObservatory
147
+ module Examples
148
+ class RandomEvent < AetherObservatory::Examples::ApplicationEvent
149
+ event_name { select_a_topic_at_random }
150
+
151
+ attribute :message
152
+
153
+ private
154
+
155
+ def select_a_topic_at_random
156
+ %w(test support customer).sample
157
+ end
158
+ end
159
+ end
160
+ end
161
+ ```
162
+
163
+ You can now create a few events with your new class using the `create` method of that class.
164
+
165
+ ```ruby
166
+ AetherObservatory::Examples::RandomEvent.create(message: 'hello world')
167
+ ```
168
+
169
+ As you can see from the following output a random event name is selected each time you call `create`.
170
+
171
+ ```irb
172
+ irb(main):078:0> AetherObservatory::Examples::RandomEvent.create(message: 'hello world')
173
+ [AetherObservatory::Examples::RandomEvent] Create event for topic: [talkbox.support]
174
+ => nil
175
+ irb(main):079:0> AetherObservatory::Examples::RandomEvent.create(message: 'hello world')
176
+ [AetherObservatory::Examples::RandomEvent] Create event for topic: [talkbox.test]
177
+ => nil
178
+ irb(main):080:0> AetherObservatory::Examples::RandomEvent.create(message: 'hello world')
179
+ [AetherObservatory::Examples::RandomEvent] Create event for topic: [talkbox.support]
180
+ => nil
181
+ irb(main):081:0> AetherObservatory::Examples::RandomEvent.create(message: 'hello world')
182
+ [AetherObservatory::Examples::RandomEvent] Create event for topic: [talkbox.customer]
183
+ => nil
184
+ ```
185
+
186
+ <div align="right">
187
+ <a href="#aetherobservatory-guide">Top</a>
188
+ </div>
189
+
190
+ ## Multiple Event Topics
191
+
192
+ In this example we are going to create an event class that sends events to two different topics based on the `level` attribute from the event class. We are also going to make two observer classes that subscribe to different events based on their role in the system.
193
+
194
+ <sup>*Note: [ApplicationEvent](#creating-events) class was created at the beginning of this guide.*</sup>
195
+
196
+ We first create the `TalkboxCallQueueEvent` class. This class will send each event to the `talkbox.call_queues.events.all` topic and to the `level` scoped topic.
197
+
198
+ ```ruby
199
+ module AetherObservatory
200
+ module Examples
201
+ class TalkboxCallQueueEvent < AetherObservatory::Examples::ApplicationEvent
202
+ event_name 'call_queues.events.all'
203
+ event_name { "call_queues.events.#{level}" }
204
+
205
+ attribute :level, default: 'info'
206
+ end
207
+ end
208
+ end
209
+ ```
210
+
211
+ The new `TalkboxCallQueueEvent` class will send all events to the `all` topic. However the events will also be sent to their specific event `level` scoped topic. This allows us to have one observer logging call history and a second observer that handles events with the scoped `level` or error for topic `talkbox.call_queues.events.error`.
212
+
213
+ Next we need to create a new class called `TalkboxCallHistoryObserver`. This observer will subscribe to the `talkbox.call_queues.events.all` topic. This classes function is to record all call queue events.
214
+
215
+ ```ruby
216
+ module AetherObservatory
217
+ module Examples
218
+ class TalkboxCallHistoryObserver < AetherObservatory::ObserverBase
219
+ subscribe_to 'talkbox.call_queues.events.all'
220
+
221
+ delegate :level, to: :event_payload
222
+
223
+ def process
224
+ puts <<-EVENT
225
+ ************************************
226
+ Event processed:
227
+ Name: #{event_name.inspect}
228
+ Level: #{event_payload.level}
229
+ Event Payload: #{event_payload.inspect}
230
+ ************************************
231
+ EVENT
232
+ end
233
+ end
234
+ end
235
+ end
236
+ ```
237
+
238
+ Next we need a class called `TalkboxCallErrorObserver`. This class only subscribes to the `talkbox.call_queues.events.error` topic. It only cares about `error` level events and nothing else.
239
+
240
+ ```ruby
241
+ module AetherObservatory
242
+ module Examples
243
+ class TalkboxCallErrorObserver < AetherObservatory::ObserverBase
244
+ subscribe_to 'talkbox.call_queues.events.error'
245
+
246
+ def process
247
+ puts <<-EVENT
248
+ ************************************
249
+ Error Event processed:
250
+ Name: #{event_name.inspect}
251
+ Level: #{event_payload.level}
252
+ Event Payload: #{event_payload.inspect}
253
+ ************************************
254
+ EVENT
255
+ end
256
+ end
257
+ end
258
+ end
259
+ ```
260
+
261
+ We need to be sure to start our new observers before they will recieve any events.
262
+
263
+ ```ruby
264
+ AetherObservatory::Examples::TalkboxCallHistoryObserver.start
265
+ AetherObservatory::Examples::TalkboxCallErrorObserver.start
266
+ ```
267
+
268
+ Finally we are ready to create a new event and see what happens. First we create an event with a default level.
269
+
270
+ ```ruby
271
+ AetherObservatory::Examples::TalkboxCallQueueEvent.create
272
+ ```
273
+
274
+ Running the create with no parameters will have a default level of `info`. You will see the following output.
275
+
276
+ ```irb
277
+ irb(main):058:0> AetherObservatory::Examples::TalkboxCallQueueEvent.create
278
+ ************************************
279
+ Event processed:
280
+ Name: "talkbox.call_queues.events.all"
281
+ Level: info
282
+ Event Payload: #<AetherObservatory::Examples::TalkboxCallQueueEvent:0x0000aaab112f75d0 @attributes=#<ActiveModel::AttributeSet:0x0000aaab112f5e88 @attributes={"level"=>#<ActiveModel::Attribute::UserProvidedDefault:0x0000aaab112f73a0 @user_provided_value="info", @name="level", @value_before_type_cast="info", @type=#<ActiveModel::Type::Value:0x0000aaab13a76e08 @precision=nil, @scale=nil, @limit=nil>, @original_attribute=nil, @value="info">}>>
283
+ ************************************
284
+ [AetherObservatory::Examples::TalkboxCallQueueEvent] Create event for topic: [talkbox.call_queues.events.all]
285
+ [AetherObservatory::Examples::TalkboxCallQueueEvent] Create event for topic: [talkbox.call_queues.events.info]
286
+ => nil
287
+ ```
288
+
289
+ Next we will try creating a new event but this time we set the `level` to `error`.
290
+
291
+ ```ruby
292
+ AetherObservatory::Examples::TalkboxCallQueueEvent.create(level: 'error')
293
+ ```
294
+
295
+ As you can see from the output, setting the `level` to `error` will send an event to both classes.
296
+
297
+ ```irb
298
+ irb(main):059:0> AetherObservatory::Examples::TalkboxCallQueueEvent.create(level: 'error')
299
+ ************************************
300
+ Event processed:
301
+ Name: "talkbox.call_queues.events.all"
302
+ Level: error
303
+ Event Payload: #<AetherObservatory::Examples::TalkboxCallQueueEvent:0x0000aaab135cff30 @attributes=#<ActiveModel::AttributeSet:0x0000aaab135cfe18 @attributes={"level"=>#<ActiveModel::Attribute::FromUser:0x0000aaab135cfe68 @name="level", @value_before_type_cast="error", @type=#<ActiveModel::Type::Value:0x0000aaab13a76e08 @precision=nil, @scale=nil, @limit=nil>, @original_attribute=#<ActiveModel::Attribute::UserProvidedDefault:0x0000aaab135e0bc8 @user_provided_value="info", @name="level", @value_before_type_cast="info", @type=#<ActiveModel::Type::Value:0x0000aaab13a76e08 @precision=nil, @scale=nil, @limit=nil>, @original_attribute=nil>, @value="error">}>>
304
+ ************************************
305
+ [AetherObservatory::Examples::TalkboxCallQueueEvent] Create event for topic: [talkbox.call_queues.events.all]
306
+ ************************************
307
+ Error Event processed:
308
+ Name: "talkbox.call_queues.events.error"
309
+ Level: error
310
+ Event Payload: #<AetherObservatory::Examples::TalkboxCallQueueEvent:0x0000aaab135cef90 @attributes=#<ActiveModel::AttributeSet:0x0000aaab135ceea0 @attributes={"level"=>#<ActiveModel::Attribute::FromUser:0x0000aaab135cef40 @name="level", @value_before_type_cast="error", @type=#<ActiveModel::Type::Value:0x0000aaab13a76e08 @precision=nil, @scale=nil, @limit=nil>, @original_attribute=#<ActiveModel::Attribute::UserProvidedDefault:0x0000aaab135e0bc8 @user_provided_value="info", @name="level", @value_before_type_cast="info", @type=#<ActiveModel::Type::Value:0x0000aaab13a76e08 @precision=nil, @scale=nil, @limit=nil>, @original_attribute=nil>, @value="error">}>>
311
+ ************************************
312
+ [AetherObservatory::Examples::TalkboxCallQueueEvent] Create event for topic: [talkbox.call_queues.events.error]
313
+ => nil
314
+ ```
315
+
316
+ <div align="right">
317
+ <a href="#aetherobservatory-guide">Top</a>
318
+ </div>
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AetherObservatory
4
+ module Configuration
5
+ include ActiveSupport::Configurable
6
+
7
+ config_accessor(:logger) do
8
+ defined?(Rails) ? Rails.logger : Logger.new($stdout)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module AetherObservatory
6
+ class EventBase
7
+ include ActiveModel::AttributeAssignment
8
+ include ActiveModel::Attributes
9
+
10
+ class << self
11
+ def inherited(subclass)
12
+ super
13
+ subclass.event_prefix(&event_prefix)
14
+ end
15
+
16
+ def create(**attributes)
17
+ event = new(**attributes)
18
+ event_names_with_prefix.each do |event_name_parts|
19
+ event_name = event_name_parts.filter_map do |part|
20
+ event.instance_exec(&part) unless part.nil?
21
+ end.join(".")
22
+ logger.debug("[#{name}] Create event for topic: [#{event_name}]")
23
+ ActiveSupport::Notifications.instrument(event_name, event)
24
+ end
25
+
26
+ nil
27
+ end
28
+
29
+ def event_prefix(value = nil, &block)
30
+ @event_prefix = -> { value } if value.present?
31
+ @event_prefix = block if block.present?
32
+
33
+ @event_prefix
34
+ end
35
+
36
+ def event_name(value = nil, &block)
37
+ event_names << -> { value } if value.present?
38
+ event_names << block if block.present?
39
+
40
+ nil
41
+ end
42
+
43
+ def event_names_with_prefix
44
+ event_names.map { |event_name| [event_prefix, event_name] }
45
+ end
46
+
47
+ def event_names
48
+ @event_names ||= []
49
+ end
50
+
51
+ def logger(value = nil)
52
+ @logger = value if value.present?
53
+
54
+ @logger || AetherObservatory.config.logger
55
+ end
56
+ end
57
+
58
+ delegate :event_name, to: "self.class"
59
+ delegate :logger, to: "self.class"
60
+
61
+ def initialize(attributes = {})
62
+ super()
63
+ assign_attributes(attributes) if attributes
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AetherObservatory
4
+ class ObserverBase
5
+ class << self
6
+ def inherited(subclass)
7
+ super
8
+ subclass.instance_variable_set(:@subscribed_topics, Set.new)
9
+ subclass.instance_variable_set(:@state, :stopped)
10
+ subclass.instance_variable_set(:@subscriptions, {})
11
+ end
12
+
13
+ def start
14
+ return if started?
15
+
16
+ logger.debug("[#{name}] Starting")
17
+
18
+ subscribed_to.each do |topic|
19
+ next if subscriptions.include?(topic)
20
+
21
+ register_subscription_to(topic)
22
+ end
23
+
24
+ self.state = :started
25
+ end
26
+
27
+ def stop
28
+ return if stopped?
29
+
30
+ logger.debug("[#{name}] Stopping")
31
+
32
+ subscriptions.each_key do |topic|
33
+ unregister_subscription_to(topic)
34
+ end
35
+
36
+ self.state = :stopped
37
+ end
38
+
39
+ def subscribe_to(topic)
40
+ subscribed_topics.add(topic)
41
+
42
+ return if stopped?
43
+
44
+ register_subscription_to(topic)
45
+ end
46
+
47
+ def unsubscribe_from(topic)
48
+ subscribed_topics.delete(topic)
49
+
50
+ return if stopped?
51
+
52
+ unregister_subscription_to(topic)
53
+ end
54
+
55
+ def subscribed_to
56
+ subscribed_topics.to_a
57
+ end
58
+
59
+ def started?
60
+ state == :started
61
+ end
62
+
63
+ def stopped?
64
+ state == :stopped
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :subscribed_topics, :subscriptions
70
+ attr_accessor :state
71
+
72
+ def register_subscription_to(topic)
73
+ return if subscriptions.include?(topic)
74
+
75
+ logger.debug("[#{name}] Registering subscription to topic: #{topic.inspect}")
76
+
77
+ subscriptions[topic] = ActiveSupport::Notifications.subscribe(topic) do |*args|
78
+ name.constantize.new(ActiveSupport::Notifications::Event.new(*args)).process
79
+ end
80
+ end
81
+
82
+ def unregister_subscription_to(topic)
83
+ return if subscriptions.exclude?(topic)
84
+
85
+ logger.debug("[#{name}] Unregistering subscription to topic: #{topic.inspect}")
86
+
87
+ ActiveSupport::Notifications.unsubscribe(subscriptions.delete(topic))
88
+ end
89
+
90
+ def logger(value = nil)
91
+ @logger = value if value.present?
92
+
93
+ @logger || AetherObservatory.config.logger
94
+ end
95
+ end
96
+
97
+ attr_accessor :event
98
+
99
+ def initialize(event)
100
+ self.event = event
101
+ end
102
+
103
+ delegate :name, to: :event, prefix: true
104
+ delegate :payload, to: :event, prefix: true
105
+ end
106
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AetherObservatory
4
+ VERSION = "0.0.1pre4"
5
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+ require "aether_observatory/configuration"
5
+
6
+ module AetherObservatory
7
+ mattr_accessor :configuration, default: Configuration
8
+
9
+ class << self
10
+ delegate :configure, :config, to: :configuration
11
+ end
12
+ end
13
+
14
+ require "aether_observatory/event_base"
15
+ require "aether_observatory/observer_base"
data/mkdocs.yml ADDED
@@ -0,0 +1,6 @@
1
+ site_name: AetherObservatory
2
+ nav:
3
+ - "Home": "README.md"
4
+ - "Changelog": "CHANGELOG.md"
5
+ plugins:
6
+ - techdocs-core