yabeda-actioncable 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.txt +21 -0
- data/Rakefile +12 -0
- data/lib/yabeda/action_cable/channel_concern.rb +22 -0
- data/lib/yabeda/action_cable/config.rb +55 -0
- data/lib/yabeda/action_cable/measurment_collector.rb +73 -0
- data/lib/yabeda/action_cable/railtie.rb +15 -0
- data/lib/yabeda/action_cable/version.rb +17 -0
- data/lib/yabeda/action_cable.rb +204 -0
- data/lib/yabeda/actioncable.rb +3 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 831ed6899ba655794b01af416ac4d66e7955c392d037882916221726202036c6
|
4
|
+
data.tar.gz: 7c0776e6e5503908e1c0801a20c66608d2384838b5e49d52b6cf67888c907f99
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3a07f5c277eff792bac3274a6928d77aa3a7bafd464dfea2a256e60ecda4a2a4ef2623d889758e0d32e35d1bedd3b82c0519060c0c3d5cf36f6a5c2e9a81e4ef
|
7
|
+
data.tar.gz: bf9837f3b0a068bff310f6c55fe9737137e20d4072443cbac80cf5ffb2fa315da9645567650492ae53f4ebad9c1eee1556b7e92735d3cede9b56cae81975a1fa
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Stanko K.R. <hey@stanko.io>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yabeda
|
4
|
+
module ActionCable
|
5
|
+
# :nodoc:
|
6
|
+
module ChannelConcern
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
after_subscribe do
|
11
|
+
stream_from(
|
12
|
+
Yabeda::ActionCable.stream_name,
|
13
|
+
proc do |json|
|
14
|
+
payload = ActiveSupport::JSON.decode(json)
|
15
|
+
Yabeda::ActionCable.collect_measurment(payload)
|
16
|
+
end
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/numeric/time"
|
4
|
+
require "set"
|
5
|
+
|
6
|
+
module Yabeda
|
7
|
+
module ActionCable
|
8
|
+
class Config
|
9
|
+
DEFAULT_BUCKETS = [ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 ].freeze
|
10
|
+
DEFAULT_STREAM_NAME = "yabeda.action_cable.metrics"
|
11
|
+
DEFAULT_COLLECTION_PERIOD = 60.seconds.freeze
|
12
|
+
DEFAULT_CHANNEL_CLASS_NAME = "ApplicationCable::Channel"
|
13
|
+
|
14
|
+
attr_accessor :default_buckets,
|
15
|
+
:buckets,
|
16
|
+
:default_tags,
|
17
|
+
:tags,
|
18
|
+
:stream_name,
|
19
|
+
:collection_period,
|
20
|
+
:channel_class_name
|
21
|
+
attr_writer :collection_cooldown_period
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
reset!
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset!
|
28
|
+
@default_buckets = DEFAULT_BUCKETS.dup
|
29
|
+
@buckets = {}
|
30
|
+
@default_tags = {}
|
31
|
+
@tags = {}
|
32
|
+
@stream_name = DEFAULT_STREAM_NAME
|
33
|
+
@collection_period = DEFAULT_COLLECTION_PERIOD.dup
|
34
|
+
@collection_cooldown_period = nil
|
35
|
+
@channel_class_name = DEFAULT_CHANNEL_CLASS_NAME
|
36
|
+
end
|
37
|
+
|
38
|
+
def collection_cooldown_period
|
39
|
+
if @collection_cooldown_period.nil?
|
40
|
+
collection_period / 2.0
|
41
|
+
else
|
42
|
+
@collection_cooldown_period
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def buckets_for(metric)
|
47
|
+
buckets[metric] || default_buckets
|
48
|
+
end
|
49
|
+
|
50
|
+
def tags_for(metric)
|
51
|
+
tags[metric] || default_tags
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/numeric/time"
|
4
|
+
|
5
|
+
module Yabeda
|
6
|
+
module ActionCable
|
7
|
+
class MeasurmentCollector
|
8
|
+
attr_reader :config
|
9
|
+
attr_accessor :last_collected_at
|
10
|
+
|
11
|
+
def initialize(config:)
|
12
|
+
@config = config
|
13
|
+
@mutex = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def measure
|
17
|
+
::ActionCable.server.broadcast(config.stream_name, measurment_payload)
|
18
|
+
end
|
19
|
+
|
20
|
+
def collect_measurment(payload)
|
21
|
+
return if on_cooldown?
|
22
|
+
|
23
|
+
run_unless_already_running do
|
24
|
+
self.last_collected_at = Time.now
|
25
|
+
|
26
|
+
measure_connection_count
|
27
|
+
measure_pubsub_latency(payload)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :mutex
|
34
|
+
|
35
|
+
def measurment_payload
|
36
|
+
{
|
37
|
+
sent_at: Time.now.to_f
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def on_cooldown?
|
42
|
+
last_collected_at&.after?(config.collection_cooldown_period.ago)
|
43
|
+
end
|
44
|
+
|
45
|
+
def run_unless_already_running
|
46
|
+
lock_ackquired = mutex.try_lock
|
47
|
+
return unless lock_ackquired
|
48
|
+
|
49
|
+
yield
|
50
|
+
ensure
|
51
|
+
mutex.unlock if lock_ackquired
|
52
|
+
end
|
53
|
+
|
54
|
+
def measure_connection_count
|
55
|
+
Yabeda.actioncable.connection_count.set(
|
56
|
+
Yabeda::ActionCable.config.tags_for(:connection_count),
|
57
|
+
::ActionCable.server.connections.length
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def measure_pubsub_latency(payload)
|
62
|
+
sent_at = payload["sent_at"] || raise(ArgumentError, "Payload must contain 'sent_at' key")
|
63
|
+
pubsub_latency = Time.now.to_f - sent_at
|
64
|
+
return unless pubsub_latency.positive? || pubsub_latency.zero?
|
65
|
+
|
66
|
+
Yabeda.actioncable.pubsub_latency.measure(
|
67
|
+
Yabeda::ActionCable.config.tags_for(:pubsub_latency),
|
68
|
+
pubsub_latency
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/railtie"
|
4
|
+
require "yabeda/railtie"
|
5
|
+
|
6
|
+
module Yabeda
|
7
|
+
module ActionCable
|
8
|
+
# :nodoc:
|
9
|
+
class Railtie < ::Rails::Railtie
|
10
|
+
initializer "yabeda-actioncable.metrics" do
|
11
|
+
::Yabeda::ActionCable.install!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yabeda
|
4
|
+
module ActionCable
|
5
|
+
MAJOR_VERSION = 0
|
6
|
+
MINOR_VERSION = 1
|
7
|
+
PATCH_VERSION = 0
|
8
|
+
PRERELEASE_VERSION = nil
|
9
|
+
|
10
|
+
VERSION = [
|
11
|
+
MAJOR_VERSION,
|
12
|
+
MINOR_VERSION,
|
13
|
+
PATCH_VERSION,
|
14
|
+
PRERELEASE_VERSION
|
15
|
+
].reject(&:nil?).join(".").freeze
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "action_cable/version"
|
4
|
+
|
5
|
+
require "yabeda"
|
6
|
+
require "action_cable"
|
7
|
+
require "active_support/notifications"
|
8
|
+
|
9
|
+
require "yabeda/action_cable/config"
|
10
|
+
require "yabeda/action_cable/measurment_collector"
|
11
|
+
require "yabeda/action_cable/channel_concern"
|
12
|
+
require "yabeda/action_cable/railtie"
|
13
|
+
|
14
|
+
module Yabeda
|
15
|
+
module ActionCable
|
16
|
+
class Error < StandardError; end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
MUTEX = Mutex.new
|
20
|
+
|
21
|
+
def installed?
|
22
|
+
yabeda_configured? && subscirbed? && channel_concern_included?
|
23
|
+
end
|
24
|
+
|
25
|
+
def install!
|
26
|
+
configure_yabeda!
|
27
|
+
subscribe!
|
28
|
+
include_channel_concern!
|
29
|
+
end
|
30
|
+
|
31
|
+
def stream_name
|
32
|
+
config.stream_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def measure
|
36
|
+
measurment_collector.measure
|
37
|
+
end
|
38
|
+
|
39
|
+
def collect_measurment(payload)
|
40
|
+
measurment_collector.collect_measurment(payload)
|
41
|
+
end
|
42
|
+
|
43
|
+
def configure(&block)
|
44
|
+
raise ArgumentError, "Block is required" unless block_given?
|
45
|
+
|
46
|
+
config.tap do |cfg|
|
47
|
+
block.call(cfg)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def config
|
52
|
+
return @config if defined?(@config)
|
53
|
+
|
54
|
+
MUTEX.synchronize do
|
55
|
+
@config ||= Config.new
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def reset!
|
60
|
+
unsubscribe!
|
61
|
+
config.reset!
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_accessor :subscribers
|
67
|
+
|
68
|
+
def configure_yabeda!
|
69
|
+
Yabeda.group :actioncable do
|
70
|
+
Yabeda.configure do
|
71
|
+
histogram :pubsub_latency do
|
72
|
+
comment "The time it takes for a message to go through the " \
|
73
|
+
"PubSub backend (e.g. Redis, SolidQueue, Postgres)"
|
74
|
+
unit :seconds
|
75
|
+
buckets Yabeda::ActionCable.config.buckets_for(:pubsub_latency)
|
76
|
+
end
|
77
|
+
|
78
|
+
histogram :broadcast_duration do
|
79
|
+
comment "The time it takes to broadcast a message to the PubSub backend"
|
80
|
+
unit :seconds
|
81
|
+
buckets Yabeda::ActionCable.config.buckets_for(:broadcast_duration)
|
82
|
+
end
|
83
|
+
|
84
|
+
histogram :transmission_duration do
|
85
|
+
comment "The time it takes to write a message to a WebSocket"
|
86
|
+
unit :seconds
|
87
|
+
buckets Yabeda::ActionCable.config.buckets_for(:transmission_duration)
|
88
|
+
end
|
89
|
+
|
90
|
+
histogram :action_execution_duration do
|
91
|
+
comment "The time it takes to perform an invoked action"
|
92
|
+
unit :seconds
|
93
|
+
buckets Yabeda::ActionCable.config.buckets_for(:action_execution_duration)
|
94
|
+
end
|
95
|
+
|
96
|
+
counter :confirmed_subscriptions,
|
97
|
+
comment: "Total number of confirmed subscriptions"
|
98
|
+
|
99
|
+
counter :rejected_subscriptions,
|
100
|
+
comment: "Total number of rejected subscriptions"
|
101
|
+
|
102
|
+
gauge :connection_count,
|
103
|
+
comment: "Number of open WebSocket connections",
|
104
|
+
aggregation: :sum
|
105
|
+
|
106
|
+
counter :allocations_during_action,
|
107
|
+
comment: "Number of allocated objects during the invoication of an action"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def yabeda_configured?
|
113
|
+
Yabeda.groups.include?(:actioncable)
|
114
|
+
end
|
115
|
+
|
116
|
+
def subscribe!
|
117
|
+
unsubscribe!
|
118
|
+
|
119
|
+
subscribers.push(
|
120
|
+
ActiveSupport::Notifications.monotonic_subscribe("perform_action.action_cable") do |event|
|
121
|
+
tags = { channel: event.payload[:channel_class], action: event.payload[:action] }
|
122
|
+
|
123
|
+
Yabeda.actioncable.action_execution_duration.measure(
|
124
|
+
config.tags_for(:action_execution_duration).merge(tags),
|
125
|
+
event.duration / 1000.0
|
126
|
+
)
|
127
|
+
|
128
|
+
Yabeda.actioncable.allocations_during_action.increment(
|
129
|
+
config.tags_for(:allocations_during_action).merge(tags),
|
130
|
+
by: event.allocations
|
131
|
+
)
|
132
|
+
end
|
133
|
+
)
|
134
|
+
|
135
|
+
subscribers.push(
|
136
|
+
ActiveSupport::Notifications.monotonic_subscribe("transmit.action_cable") do |event|
|
137
|
+
Yabeda.actioncable.transmission_duration.measure(
|
138
|
+
config.tags_for(:transmission_duration).merge(
|
139
|
+
channel: event.payload[:channel_class]
|
140
|
+
),
|
141
|
+
event.duration / 1000.0
|
142
|
+
)
|
143
|
+
end
|
144
|
+
)
|
145
|
+
|
146
|
+
subscribers.push(
|
147
|
+
ActiveSupport::Notifications.monotonic_subscribe("transmit_subscription_confirmation.action_cable") do |event|
|
148
|
+
Yabeda.actioncable.confirmed_subscriptions.increment(
|
149
|
+
config.tags_for(:confirmed_subscriptions).merge(
|
150
|
+
channel: event.payload[:channel_class]
|
151
|
+
),
|
152
|
+
by: 1
|
153
|
+
)
|
154
|
+
end
|
155
|
+
)
|
156
|
+
|
157
|
+
subscribers.push(
|
158
|
+
ActiveSupport::Notifications.monotonic_subscribe("transmit_subscription_rejection.action_cable") do |event|
|
159
|
+
Yabeda.actioncable.rejected_subscriptions.increment(
|
160
|
+
config.tags_for(:rejected_subscriptions).merge(
|
161
|
+
channel: event.payload[:channel_class]
|
162
|
+
),
|
163
|
+
by: 1
|
164
|
+
)
|
165
|
+
end
|
166
|
+
)
|
167
|
+
|
168
|
+
subscribers.push(
|
169
|
+
ActiveSupport::Notifications.monotonic_subscribe("broadcast.action_cable") do |event|
|
170
|
+
Yabeda.actioncable.broadcast_duration.measure(
|
171
|
+
config.tags_for(:broadcast_duration),
|
172
|
+
event.duration / 1000.0
|
173
|
+
)
|
174
|
+
end
|
175
|
+
)
|
176
|
+
end
|
177
|
+
|
178
|
+
def unsubscribe!
|
179
|
+
subscribers&.each { |subscriber| ActiveSupport::Notifications.unsubscribe(subscriber) }
|
180
|
+
self.subscribers = []
|
181
|
+
end
|
182
|
+
|
183
|
+
def subscirbed?
|
184
|
+
subscribers.present?
|
185
|
+
end
|
186
|
+
|
187
|
+
def include_channel_concern!
|
188
|
+
config.channel_class_name.constantize.include(ChannelConcern)
|
189
|
+
end
|
190
|
+
|
191
|
+
def channel_concern_included?
|
192
|
+
config.channel_class_name.constantize.include?(ChannelConcern)
|
193
|
+
end
|
194
|
+
|
195
|
+
def measurment_collector
|
196
|
+
return @measurment_collector if defined?(@measurment_collector)
|
197
|
+
|
198
|
+
MUTEX.synchronize do
|
199
|
+
@measurment_collector ||= MeasurmentCollector.new(config: config)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: yabeda-actioncable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stanko K.R.
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-03-18 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: actioncable
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '7.2'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '7.2'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activesupport
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: railties
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: yabeda
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0.8'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.8'
|
68
|
+
description: ''
|
69
|
+
email:
|
70
|
+
- stanko@stanko.io
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- LICENSE.txt
|
76
|
+
- Rakefile
|
77
|
+
- lib/yabeda/action_cable.rb
|
78
|
+
- lib/yabeda/action_cable/channel_concern.rb
|
79
|
+
- lib/yabeda/action_cable/config.rb
|
80
|
+
- lib/yabeda/action_cable/measurment_collector.rb
|
81
|
+
- lib/yabeda/action_cable/railtie.rb
|
82
|
+
- lib/yabeda/action_cable/version.rb
|
83
|
+
- lib/yabeda/actioncable.rb
|
84
|
+
homepage: https://github.com/monorkin/yabeda-actioncable
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata:
|
88
|
+
allowed_push_host: https://rubygems.org
|
89
|
+
homepage_uri: https://github.com/monorkin/yabeda-actioncable
|
90
|
+
source_code_uri: https://github.com/monorkin/yabeda-actioncable
|
91
|
+
changelog_uri: https://github.com/monorkin/yabeda-actioncable/blob/main/CHANGELOG.md
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 3.1.0
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubygems_version: 3.6.2
|
107
|
+
specification_version: 4
|
108
|
+
summary: Yabeda plugin for collecting ActionCable metrics
|
109
|
+
test_files: []
|