mixboard 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.
- checksums.yaml +7 -0
- data/LICENSE.md +25 -0
- data/README.md +180 -0
- data/Rakefile +34 -0
- data/app/assets/config/mixboard_manifest.js +1 -0
- data/app/assets/stylesheets/mixboard/application.css +15 -0
- data/app/controllers/mixboard/application_controller.rb +8 -0
- data/app/helpers/mixboard/application_helper.rb +7 -0
- data/app/jobs/mixboard/application_job.rb +7 -0
- data/app/mixer/mixboard/channel.rb +44 -0
- data/app/mixer/mixboard/configuration.rb +60 -0
- data/app/mixer/mixboard/dynamic_channel_store.rb +34 -0
- data/app/mixer/mixboard/errors/abstract_not_implemented_error.rb +14 -0
- data/app/mixer/mixboard/errors/type_mismatch_error.rb +14 -0
- data/app/mixer/mixboard/filter.rb +27 -0
- data/app/mixer/mixboard/filterable.rb +45 -0
- data/app/mixer/mixboard/in_memory_dynamic_channel_store.rb +32 -0
- data/app/mixer/mixboard/mixer.rb +77 -0
- data/app/mixer/mixboard/signal.rb +7 -0
- data/app/mixer/mixboard/signal_processor.rb +35 -0
- data/app/mixer/mixboard/sink.rb +21 -0
- data/app/mixer/mixboard/source.rb +17 -0
- data/app/mixer/mixboard/utility_functions.rb +28 -0
- data/app/views/layouts/mixboard/application.html.erb +15 -0
- data/config/routes.rb +4 -0
- data/lib/mixboard.rb +7 -0
- data/lib/mixboard/engine.rb +14 -0
- data/lib/mixboard/version.rb +5 -0
- data/lib/tasks/mixboard_tasks.rake +5 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0d0a1f66401e36fe1c9dfcac7514a27d9808a92b2416b8ca37a10bb738ab408f
|
4
|
+
data.tar.gz: 9b404a6b5b57186e759aa27314ed81a45d054f5cb3bb96fbfbd08ffa5118ed3a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 92128dccbba4f852a99662525589a38ac169e9ef14e3086570737f9e8e55bc109dba73c7200dab090b2fbfcb5809fb47835dff60c42a9a60b90629f646fee85f
|
7
|
+
data.tar.gz: a1c9d465c7259ca915c1d680ca8caa6f55fdf56fc3f1d0d2f4f0da82d60ec96ca020d6a40a7b2df13b912859d955e6847c64ca96ab8cbb1996c296603ae751ee
|
data/LICENSE.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
=====================
|
3
|
+
|
4
|
+
Copyright © `2020` `Braze, Inc.`
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person
|
7
|
+
obtaining a copy of this software and associated documentation
|
8
|
+
files (the “Software”), to deal in the Software without
|
9
|
+
restriction, including without limitation the rights to use,
|
10
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the
|
12
|
+
Software is furnished to do so, subject to the following
|
13
|
+
conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be
|
16
|
+
included in all copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
20
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
22
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
23
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
24
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
25
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
# Mixboard
|
2
|
+
|
3
|
+
Mixboard is a framework for routing signals from sources to sinks. Signals can be metrics,
|
4
|
+
log messages, errors, or any other similar piece of information one might want to route
|
5
|
+
from a source to a sink. Conditional routing is available via filters, and transformation
|
6
|
+
of signals is possible via signal processors. A specific end-to-end route is called a channel.
|
7
|
+
|
8
|
+
It's a common pattern to spend a non-trivial amount of time enabling and disabling
|
9
|
+
logs, metrics, or other types of debugging information on a per-customer or a
|
10
|
+
per-business-object basis. Instead, this framework provides the tooling to do this type of
|
11
|
+
observability work ahead of time (i.e. inserting the proper signalling) and be able to
|
12
|
+
enable/disable it selectively at runtime or via configuration.
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
Mixboard is designed as a Rails Engine, so usage is straightforward. A simple example
|
17
|
+
is below.
|
18
|
+
|
19
|
+
### Signals
|
20
|
+
|
21
|
+
A signal is a payload containing information that can be routed through the mixer.
|
22
|
+
|
23
|
+
First, create a signal by extending `Mixboard::Signal` if you aren't using a generic
|
24
|
+
signal type, like so:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class DummySignal < Mixboard::Signal
|
28
|
+
attr_accessor :my_attribute
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
#### Signal Levels
|
33
|
+
|
34
|
+
Signals have levels associated with them, similar to log messages. Using levels,
|
35
|
+
it's easy to subcategorize or delineate the granularity of signals. The provided
|
36
|
+
levels are: `verbose`, `debug`, `info`, `warn`, and `error`, similar to most
|
37
|
+
logging frameworks. These are often helpful when using filters.
|
38
|
+
|
39
|
+
### Sources
|
40
|
+
|
41
|
+
A source is anything that emits signals of a certain type.
|
42
|
+
|
43
|
+
Create a source by extending `Mixboard::Source`, making sure the methods you
|
44
|
+
plan on using on your source are calling the `emit` method with a signal instance,
|
45
|
+
like so:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
class DummySource < Mixboard::Source
|
49
|
+
def signal_class
|
50
|
+
DummySignal
|
51
|
+
end
|
52
|
+
|
53
|
+
def my_logging_method
|
54
|
+
signal = DummySignal.new
|
55
|
+
signal.my_attribute = 'FizzBuzz'
|
56
|
+
emit(signal)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
### Sinks
|
62
|
+
|
63
|
+
A sink is anything that accepts signals of a certain type.
|
64
|
+
|
65
|
+
Create a sink by extending `Mixboard::Sink` if you aren't using a generic sink,
|
66
|
+
making sure to provide an `accept` method, like so:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class DummySink < Mixboard::Sink
|
70
|
+
def signal_class
|
71
|
+
DummySignal
|
72
|
+
end
|
73
|
+
|
74
|
+
def accept(signal)
|
75
|
+
MyExternalLoggingService.post(signal.my_attribute)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### Configuration
|
81
|
+
|
82
|
+
To statically configure channels, create a `mixboard.rb` initializer and
|
83
|
+
configure a channel, like so:
|
84
|
+
```ruby
|
85
|
+
Mixboard::Mixer.configure do |c|
|
86
|
+
channel = Mixboard::Channel.new
|
87
|
+
.add_source(DummySource)
|
88
|
+
.add_sink(DummySink.new(any_config_options))
|
89
|
+
|
90
|
+
c.add_channel(channel)
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
And you should be all set. You can instantiate your `DummySource` anywhere, use
|
95
|
+
`my_logging_method`, and Mixboard will connect the two components!
|
96
|
+
|
97
|
+
To dynamically configure channels... TODO!
|
98
|
+
|
99
|
+
### Signal Processors
|
100
|
+
|
101
|
+
Signal processors accept signals of a certain type and emit signals of a certain type.
|
102
|
+
The are used for transforming signals from one type to another.
|
103
|
+
|
104
|
+
Imagine wanting to send an error message to something like Sentry, but also wanting to
|
105
|
+
track it as a metric in StatsD. You would set up two channels:
|
106
|
+
|
107
|
+
- Source: Logger; Sink: SentrySink
|
108
|
+
- Source: Logger; Sink: StatsDCountMetricSink
|
109
|
+
|
110
|
+
For the first channel, the signal types match (assuming they are both something simple,
|
111
|
+
such as `MessageSignal`, so you don't need to associate a signal processor. For the
|
112
|
+
second channel, you have a source type of `MessageSignal`, and a sink type of `CountMetric`.
|
113
|
+
You would need to add a signal processor that accepts `MessageSignal` (or a superclass),
|
114
|
+
and emits `CountMetric`. Your channels would look like:
|
115
|
+
|
116
|
+
- Source: Logger; Sink: SentrySink
|
117
|
+
- Source: Logger; Sink: StatsDCountMetricSink, SignalProcessors: MessageSignalCounter
|
118
|
+
|
119
|
+
Your signal processor might look like:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
class MessageSignalCounter < Mixboard::SignalProcessor
|
123
|
+
def initialize(metric_name:)
|
124
|
+
@metric_name = metric_name
|
125
|
+
end
|
126
|
+
|
127
|
+
def input_signal_class
|
128
|
+
MessageSignal
|
129
|
+
end
|
130
|
+
|
131
|
+
def output_signal_class
|
132
|
+
CountMetric
|
133
|
+
end
|
134
|
+
|
135
|
+
def transform(signal)
|
136
|
+
return CountMetric.new(@metric_name, 1)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
### Filters
|
142
|
+
|
143
|
+
Filters interrupt the flow of signals through the system. You can add filters before or
|
144
|
+
after channels and signal processors. Here's a simple example:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class DummyFilter < Mixboard::Filter
|
148
|
+
def signal_class
|
149
|
+
DummySignal
|
150
|
+
end
|
151
|
+
|
152
|
+
def filter(signal)
|
153
|
+
signal
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
Filters' `filter` method should only return the original signal to continue processing, or
|
159
|
+
return nil to indicate to stop processing the signal further.
|
160
|
+
|
161
|
+
|
162
|
+
## Installation
|
163
|
+
Add this line to your application's Gemfile:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
gem 'mixboard'
|
167
|
+
```
|
168
|
+
|
169
|
+
Or install it yourself as:
|
170
|
+
```bash
|
171
|
+
$ gem install mixboard
|
172
|
+
```
|
173
|
+
|
174
|
+
To take advantage of the dynamic "mixing panel" to configure channels... TODO
|
175
|
+
|
176
|
+
## Contributing
|
177
|
+
Contribution directions go here. TODO
|
178
|
+
|
179
|
+
## License
|
180
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rdoc/task'
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = 'rdoc'
|
13
|
+
rdoc.title = 'Mixboard'
|
14
|
+
rdoc.options << '--line-numbers'
|
15
|
+
rdoc.rdoc_files.include('README.md')
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
17
|
+
end
|
18
|
+
|
19
|
+
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
20
|
+
load 'rails/tasks/engine.rake'
|
21
|
+
|
22
|
+
load 'rails/tasks/statistics.rake'
|
23
|
+
|
24
|
+
require 'bundler/gem_tasks'
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'rspec/core/rake_task'
|
28
|
+
|
29
|
+
RSpec::Core::RakeTask.new(:spec)
|
30
|
+
|
31
|
+
task default: :spec
|
32
|
+
rescue LoadError
|
33
|
+
# no rspec available
|
34
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/mixboard .css
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# A specific end-to-end route from a source to a sink.
|
5
|
+
class Channel
|
6
|
+
include Mixboard::Filterable
|
7
|
+
include Mixboard::UtilityFunctions
|
8
|
+
|
9
|
+
attr_reader :source_class
|
10
|
+
|
11
|
+
def add_source(source_class)
|
12
|
+
assert_non_nil_of_type(source_class, Class)
|
13
|
+
@source_class = source_class
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_sink(sink)
|
18
|
+
assert_non_nil_of_type(sink, Sink)
|
19
|
+
@sink = sink
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_signal_processor(signal_processor)
|
24
|
+
assert_non_nil_of_type(signal_processor, SignalProcessor)
|
25
|
+
signal_processors.append(signal_processor)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def accept(signal)
|
30
|
+
signal = with_filters(signal) do
|
31
|
+
signal_processors.reduce(signal) do |current_signal, signal_processor|
|
32
|
+
signal_processor.do_transform(current_signal)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@sink.do_accept(signal) unless signal.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def signal_processors
|
41
|
+
@signal_processors ||= []
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# A mixer's configuration, usually set via an initializer
|
5
|
+
class Configuration
|
6
|
+
include Mixboard::UtilityFunctions
|
7
|
+
|
8
|
+
attr_reader :channel_map
|
9
|
+
|
10
|
+
def add_source(source_class)
|
11
|
+
assert_non_nil_of_type(source_class, Class)
|
12
|
+
sources.add(source_class)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_sink(sink)
|
17
|
+
assert_non_nil_of_type(sink, Mixboard::Sink)
|
18
|
+
sinks.add(sink)
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_channel(channel)
|
23
|
+
assert_non_nil_of_type(channel, Channel)
|
24
|
+
channels.append(channel)
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def dynamic_channel_store=(dynamic_channel_store)
|
29
|
+
assert_non_nil_of_type(dynamic_channel_store, Mixboard::DynamicChannelStore)
|
30
|
+
@dynamic_channel_store = dynamic_channel_store
|
31
|
+
end
|
32
|
+
|
33
|
+
def dynamic_channel_store
|
34
|
+
@dynamic_channel_store ||= Mixboard::InMemoryDynamicChannelStore.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def setup_channel_map
|
38
|
+
@channel_map = {}
|
39
|
+
channels.each do |channel|
|
40
|
+
@channel_map[channel.source_class] ||= []
|
41
|
+
@channel_map[channel.source_class].append(channel)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def sources
|
48
|
+
@sources ||= Set.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def sinks
|
52
|
+
@sinks ||= Set.new
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return Array[Channel]
|
56
|
+
def channels
|
57
|
+
@channels ||= []
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# A DynamicChannelStore is an interface for a dynamically populated set of channels. The canonical implementation
|
5
|
+
# is InMemoryDynamicChannelStore, but you could implement this with another backing store, such as Redis or Memcached.
|
6
|
+
class DynamicChannelStore
|
7
|
+
include Mixboard::UtilityFunctions
|
8
|
+
|
9
|
+
def add_channel(_id, _channel, _expiry = nil)
|
10
|
+
declare_abstract_method_body
|
11
|
+
end
|
12
|
+
|
13
|
+
def remove_channel_by_id(_id)
|
14
|
+
declare_abstract_method_body
|
15
|
+
end
|
16
|
+
|
17
|
+
def channels
|
18
|
+
declare_abstract_method_body
|
19
|
+
end
|
20
|
+
|
21
|
+
def active_channel_map
|
22
|
+
map = {}
|
23
|
+
channels.each do |id, channel, expiry|
|
24
|
+
if !expiry.nil? && Time.now.to_i > expiry
|
25
|
+
remove_channel_by_id(id)
|
26
|
+
next
|
27
|
+
end
|
28
|
+
map[channel.source_class] ||= []
|
29
|
+
map[channel.source_class].append(channel)
|
30
|
+
end
|
31
|
+
map
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
module Errors
|
5
|
+
# An error meant to indicate that a user tried to call a superclass method that the subclass did not implement.
|
6
|
+
class AbstractNotImplementedError < StandardError
|
7
|
+
def initialize(class_name:, entity_name:)
|
8
|
+
super("#{entity_name} not implemented in abstract class #{class_name}")
|
9
|
+
@class_name = class_name
|
10
|
+
@entity_name = entity_name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
module Errors
|
5
|
+
# An error indicating that a signal was provided with an incorrect type.
|
6
|
+
class TypeMismatchError < StandardError
|
7
|
+
def initialize(expected_type:, given_type:)
|
8
|
+
super("Expected signal of type #{expected_type}, but given signal of type #{given_type}")
|
9
|
+
@expected_type = expected_type
|
10
|
+
@given_type = given_type
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# Filters interrupt the flow of signals through the system. You can add filters before or
|
5
|
+
# after channels and signal processors
|
6
|
+
class Filter
|
7
|
+
include Mixboard::UtilityFunctions
|
8
|
+
|
9
|
+
# @return [Class] the Signal subclass that this filter applies to
|
10
|
+
def signal_class
|
11
|
+
declare_abstract_method_body
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [Signal]
|
15
|
+
# @return [Signal,nil] the filtered signal or nil
|
16
|
+
def filter(_signal)
|
17
|
+
declare_abstract_method_body
|
18
|
+
end
|
19
|
+
|
20
|
+
def do_filter(signal)
|
21
|
+
assert_non_nil_of_type(signal, signal_class)
|
22
|
+
signal = filter(signal)
|
23
|
+
assert_type(signal, signal_class)
|
24
|
+
signal
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# A mixin for anything that can have a filter added before/after it.
|
5
|
+
module Filterable
|
6
|
+
include Mixboard::UtilityFunctions
|
7
|
+
|
8
|
+
def add_before_filter(filter)
|
9
|
+
before_filters.append(filter)
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_after_filter(filter)
|
14
|
+
after_filters.append(filter)
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_filters(signal, &block)
|
19
|
+
signal = run_filters(signal, before_filters)
|
20
|
+
return nil if signal.nil?
|
21
|
+
|
22
|
+
block.call(signal)
|
23
|
+
run_filters(signal, after_filters)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def run_filters(signal, filters)
|
29
|
+
filters.reduce(signal) do |current_signal, filter|
|
30
|
+
return nil if current_signal.nil?
|
31
|
+
|
32
|
+
assert_non_nil_of_type(current_signal, filter.signal_class)
|
33
|
+
filter.do_filter(current_signal)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def before_filters
|
38
|
+
@before_filters ||= []
|
39
|
+
end
|
40
|
+
|
41
|
+
def after_filters
|
42
|
+
@after_filters ||= []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# An in-memory store of dynamic channel assignments. You probably shouldn't use this because
|
5
|
+
# it won't work right with multiple processes. Consider implementing a DynamicChannelStore with
|
6
|
+
# a fast key-value store, like Redis or Memcached.
|
7
|
+
class InMemoryDynamicChannelStore < Mixboard::DynamicChannelStore
|
8
|
+
include Mixboard::UtilityFunctions
|
9
|
+
|
10
|
+
def add_channel(id, channel, expiry = nil)
|
11
|
+
assert_non_nil_of_type(channel, Mixboard::Channel)
|
12
|
+
assert_type(expiry, Integer)
|
13
|
+
channels_by_id[id] = [channel, expiry]
|
14
|
+
end
|
15
|
+
|
16
|
+
def remove_channel_by_id(id)
|
17
|
+
channels_by_id.delete(id)
|
18
|
+
end
|
19
|
+
|
20
|
+
def channels
|
21
|
+
channels_by_id.map do |id, tuple|
|
22
|
+
[id, tuple[0], tuple[1]]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def channels_by_id
|
29
|
+
@channels_by_id ||= {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# The Mixer is a singleton class that actually does the routing of signals.
|
5
|
+
class Mixer
|
6
|
+
include ::Singleton
|
7
|
+
include Mixboard::UtilityFunctions
|
8
|
+
|
9
|
+
CONFIGURATION_MUTEX = Mutex.new
|
10
|
+
|
11
|
+
attr_writer :map_refresh_timeout
|
12
|
+
|
13
|
+
# Accept a signal from a source to route it to the correct channels
|
14
|
+
# @param [Signal] signal
|
15
|
+
# @param [Source] source
|
16
|
+
def accept(signal, source)
|
17
|
+
assert_non_nil_of_type(signal, Signal)
|
18
|
+
assert_non_nil_of_type(source, Source)
|
19
|
+
(channel_map[source.class] || []).each do |channel|
|
20
|
+
channel.accept(signal)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def configure(&block)
|
25
|
+
CONFIGURATION_MUTEX.synchronize do
|
26
|
+
return unless @configuration.nil?
|
27
|
+
|
28
|
+
c = Mixboard::Configuration.new
|
29
|
+
block.call(c) if block_given?
|
30
|
+
c.setup_channel_map
|
31
|
+
@last_map_refresh = 0
|
32
|
+
@configuration = c
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear_configuration
|
37
|
+
@configuration = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def map_refresh_timeout
|
41
|
+
@map_refresh_timeout ||= 30
|
42
|
+
end
|
43
|
+
|
44
|
+
def dynamic_channel_store
|
45
|
+
configuration.dynamic_channel_store
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def channel_map
|
51
|
+
refresh_channel_map if Time.now.to_i > last_map_refresh + map_refresh_timeout
|
52
|
+
@channel_map
|
53
|
+
end
|
54
|
+
|
55
|
+
def refresh_channel_map
|
56
|
+
@last_map_refresh = Time.now.to_i
|
57
|
+
map = configuration.channel_map.dup
|
58
|
+
|
59
|
+
configuration.dynamic_channel_store.active_channel_map.each do |source, channels|
|
60
|
+
map[source] ||= []
|
61
|
+
map[source].concat(channels)
|
62
|
+
end
|
63
|
+
|
64
|
+
@channel_map = map
|
65
|
+
end
|
66
|
+
|
67
|
+
def last_map_refresh
|
68
|
+
@last_map_refresh ||= 0
|
69
|
+
end
|
70
|
+
|
71
|
+
def configuration
|
72
|
+
raise 'Mixer not configured yet!' if @configuration.nil?
|
73
|
+
|
74
|
+
@configuration
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# Signal processors accept signals of a certain type and emit signals of a certain type.
|
5
|
+
# The are used for transforming signals from one type to another.
|
6
|
+
class SignalProcessor
|
7
|
+
include Mixboard::Filterable
|
8
|
+
include Mixboard::UtilityFunctions
|
9
|
+
|
10
|
+
# @return [Class] the Signal subclass that this filter applies to
|
11
|
+
def input_signal_class
|
12
|
+
declare_abstract_method_body
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Class] the Signal subclass that this filter applies to
|
16
|
+
def output_signal_class
|
17
|
+
declare_abstract_method_body
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [Signal]
|
21
|
+
# @return [Signal,nil] the filtered signal or nil
|
22
|
+
def transform(_signal)
|
23
|
+
declare_abstract_method_body
|
24
|
+
end
|
25
|
+
|
26
|
+
def do_transform(signal)
|
27
|
+
assert_non_nil_of_type(signal, input_signal_class)
|
28
|
+
signal = with_filters(signal) do
|
29
|
+
transform(signal)
|
30
|
+
end
|
31
|
+
assert_type(signal, output_signal_class)
|
32
|
+
signal
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# A sink is anything that accepts signals of a certain type.
|
5
|
+
class Sink
|
6
|
+
include Mixboard::UtilityFunctions
|
7
|
+
|
8
|
+
def signal_class
|
9
|
+
declare_abstract_method_body
|
10
|
+
end
|
11
|
+
|
12
|
+
def accept(_signal)
|
13
|
+
declare_abstract_method_body
|
14
|
+
end
|
15
|
+
|
16
|
+
def do_accept(signal)
|
17
|
+
assert_non_nil_of_type(signal, signal_class)
|
18
|
+
accept(signal)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# A source is anything that emits signals of a certain type.
|
5
|
+
class Source
|
6
|
+
include Mixboard::UtilityFunctions
|
7
|
+
|
8
|
+
def signal_class
|
9
|
+
declare_abstract_method_body
|
10
|
+
end
|
11
|
+
|
12
|
+
def emit(signal)
|
13
|
+
assert_non_nil_of_type(signal, signal_class)
|
14
|
+
Mixer.instance.accept(signal, self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# Various utility functions
|
5
|
+
module UtilityFunctions
|
6
|
+
def method_not_implemented_error(name)
|
7
|
+
Mixboard::Errors::AbstractNotImplementedError.new(
|
8
|
+
class_name: self.class.to_s, entity_name: "method: #{name}"
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def declare_abstract_method_body
|
13
|
+
raise method_not_implemented_error(caller_locations(1, 1)[0].label)
|
14
|
+
end
|
15
|
+
|
16
|
+
def assert_type(obj, typ)
|
17
|
+
return unless !obj.nil? && !obj.is_a?(typ)
|
18
|
+
|
19
|
+
raise Mixboard::Errors::TypeMismatchError.new(expected_type: typ, given_type: obj.class)
|
20
|
+
end
|
21
|
+
|
22
|
+
def assert_non_nil_of_type(obj, typ)
|
23
|
+
raise Mixboard::Errors::TypeMismatchError.new(expected_type: typ, given_type: obj.class) if obj.nil?
|
24
|
+
|
25
|
+
assert_type(obj, typ)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/config/routes.rb
ADDED
data/lib/mixboard.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mixboard
|
4
|
+
# The Rails engine for Mixboard
|
5
|
+
class Engine < ::Rails::Engine
|
6
|
+
isolate_namespace Mixboard
|
7
|
+
|
8
|
+
config.generators do |g|
|
9
|
+
g.test_framework :rspec
|
10
|
+
g.fixture_replacement :factory_bot
|
11
|
+
g.factory_bot dir: 'spec/factories'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mixboard
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Zach McCormick
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: factory_bot_rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: |
|
70
|
+
Mixboard is a framework for routing signals from sources to sinks. Signals can be metrics,
|
71
|
+
log messages, errors, or any other similar piece of information one might want to route
|
72
|
+
from a source to a sink. Conditional routing is available via filters, and transformation
|
73
|
+
of signals is possible via signal processors.
|
74
|
+
email:
|
75
|
+
- zach.mccormick@braze.com
|
76
|
+
executables: []
|
77
|
+
extensions: []
|
78
|
+
extra_rdoc_files: []
|
79
|
+
files:
|
80
|
+
- LICENSE.md
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- app/assets/config/mixboard_manifest.js
|
84
|
+
- app/assets/stylesheets/mixboard/application.css
|
85
|
+
- app/controllers/mixboard/application_controller.rb
|
86
|
+
- app/helpers/mixboard/application_helper.rb
|
87
|
+
- app/jobs/mixboard/application_job.rb
|
88
|
+
- app/mixer/mixboard/channel.rb
|
89
|
+
- app/mixer/mixboard/configuration.rb
|
90
|
+
- app/mixer/mixboard/dynamic_channel_store.rb
|
91
|
+
- app/mixer/mixboard/errors/abstract_not_implemented_error.rb
|
92
|
+
- app/mixer/mixboard/errors/type_mismatch_error.rb
|
93
|
+
- app/mixer/mixboard/filter.rb
|
94
|
+
- app/mixer/mixboard/filterable.rb
|
95
|
+
- app/mixer/mixboard/in_memory_dynamic_channel_store.rb
|
96
|
+
- app/mixer/mixboard/mixer.rb
|
97
|
+
- app/mixer/mixboard/signal.rb
|
98
|
+
- app/mixer/mixboard/signal_processor.rb
|
99
|
+
- app/mixer/mixboard/sink.rb
|
100
|
+
- app/mixer/mixboard/source.rb
|
101
|
+
- app/mixer/mixboard/utility_functions.rb
|
102
|
+
- app/views/layouts/mixboard/application.html.erb
|
103
|
+
- config/routes.rb
|
104
|
+
- lib/mixboard.rb
|
105
|
+
- lib/mixboard/engine.rb
|
106
|
+
- lib/mixboard/version.rb
|
107
|
+
- lib/tasks/mixboard_tasks.rake
|
108
|
+
homepage: https://github.com/braze-inc/mixboard
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">"
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '2.6'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubygems_version: 3.0.3
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: Mixboard is a framework for routing signals from sources to sinks.
|
131
|
+
test_files: []
|