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 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "action_cable"
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: []