Skip to content

Commit

Permalink
ability to fetch subscription stats
Browse files Browse the repository at this point in the history
  • Loading branch information
prog-supdex committed Sep 13, 2023
1 parent 2f3217e commit b6a0696
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 0 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,59 @@ As in AnyCable there is no place to store subscription data in-memory, it should
=> 52ee8d65-275e-4d22-94af-313129116388
```
## Stats
You can grab Redis subscription statistics by calling
```ruby
GraphQL::AnyCable.stats
```
It will return a total of the amount of the key with the following prefixes
```
graphql-subscription
graphql-fingerprints
graphql-subscriptions
graphql-channel
```
The response will look like this
```json
{
"total": {
"subscription":22646,
"fingerprints":3200,
"subscriptions":20101,
"channel": 4900
}
}
```
You can also grab the number of subscribers grouped by subscriptions
```ruby
GraphQL::AnyCable.stats(grab_sb_stats: true)
```
It will return the response that contains `grouped_subscription_stats`
```json
{
"total": {
"subscription":22646,
"fingerprints":3200,
"subscriptions":20101,
"channel": 4900
},
"grouped_subscription_stats": {
"productCreated": 11323,
"productUpdated": 11323
}
}
```
## Testing applications which use `graphql-anycable`
You can pass custom redis-server URL to AnyCable using ENV variable.
Expand Down
5 changes: 5 additions & 0 deletions lib/graphql-anycable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require_relative "graphql/anycable/cleaner"
require_relative "graphql/anycable/config"
require_relative "graphql/anycable/railtie" if defined?(Rails)
require_relative "graphql/anycable/stats"
require_relative "graphql/subscriptions/anycable_subscriptions"

module GraphQL
Expand All @@ -20,6 +21,10 @@ def self.use(schema, **options)
schema.use GraphQL::Subscriptions::AnyCableSubscriptions, **options
end

def self.stats(grab_sb_stats: false)
AnyCable::Stats.new(redis: redis, grab_sb_stats: grab_sb_stats).collect
end

module_function

def redis
Expand Down
85 changes: 85 additions & 0 deletions lib/graphql/anycable/stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

module GraphQL
module AnyCable
# Calculates amount of Graphql Redis keys
# (graphql-subscription, graphql-fingerprints, graphql-subscriptions, graphql-channel)
# Also, calculate the number of subscribers grouped by subscriptions
class Stats
SCAN_COUNT_RECORDS_AMOUNT = 1_000
SCAN_FINGERPRINTS_AMOUNT = 100

attr_reader :redis, :list_prefixes_keys, :grab_subscription_stats

def initialize(redis:, grab_sb_stats: false)
@redis = redis
@grab_subscription_stats = grab_sb_stats
@list_prefix_keys = list_prefixes_keys
end

def collect
total_subscriptions_result = {total: {}}

list_prefixes_keys.each_with_object([]) do |(name, prefix)|
total_subscriptions_result[:total][name] = count_by_scan(match: "#{prefix}*")
end

if grab_subscription_stats
total_subscriptions_result[:grouped_subscription_stats] = group_subscription_stats
end

total_subscriptions_result.to_json
end

private

# Counting all keys, that match the pattern with iterating by count
def count_by_scan(match:, count: SCAN_COUNT_RECORDS_AMOUNT)
sb_amount = 0
cursor = '0'

loop do
cursor, result = redis.scan(cursor, match: match, count: count)
sb_amount += result.count

break if cursor == '0'
end

sb_amount
end

# Calculate subscribes, grouped by subscriptions
def group_subscription_stats
subscription_groups = {}

redis.scan_each(match: "#{list_prefixes_keys[:fingerprints]}*", count: SCAN_FINGERPRINTS_AMOUNT) do |fingerprint_key|
subscription_name = fingerprint_key.gsub(/#{list_prefixes_keys[:fingerprints]}|:/, "")
subscription_groups[subscription_name] = 0

redis.zscan_each(fingerprint_key) do |data|
redis.sscan_each("#{list_prefixes_keys[:subscriptions]}#{data[0]}") do |subscription_key|
next unless redis.exists?("#{list_prefixes_keys[:subscription]}#{subscription_key}")

subscription_groups[subscription_name] += 1
end
end
end

subscription_groups
end

def adapter
GraphQL::Subscriptions::AnyCableSubscriptions
end

def list_prefixes_keys
{
subscription: adapter::SUBSCRIPTION_PREFIX,
fingerprints: adapter::FINGERPRINTS_PREFIX,
subscriptions: adapter::SUBSCRIPTIONS_PREFIX,
channel: adapter::CHANNEL_PREFIX
}
end
end
end
end
8 changes: 8 additions & 0 deletions spec/graphql/anycable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,12 @@
)
end
end

describe ".stats" do
it "calls Graphql::AnyCable::Stats" do
allow_any_instance_of(GraphQL::AnyCable::Stats).to receive(:collect)

described_class.stats
end
end
end
65 changes: 65 additions & 0 deletions spec/graphql/stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

RSpec.describe GraphQL::AnyCable::Stats do
describe "#collect" do
let(:query) do
<<~GRAPHQL
subscription SomeSubscription {
productCreated { id title }
productUpdated { id }
}
GRAPHQL
end

let(:channel) do
socket = double("Socket", istate: AnyCable::Socket::State.new({}))
connection = double("Connection", anycable_socket: socket)
double("Channel", id: "legacy_id", params: { "channelId" => "legacy_id" }, stream_from: nil, connection: connection)
end

let(:subscription_id) do
"some-truly-random-number"
end

before do
AnycableSchema.execute(
query: query,
context: { channel: channel, subscription_id: subscription_id },
variables: {},
operation_name: "SomeSubscription",
)
end

let(:redis) { AnycableSchema.subscriptions.redis }

context "when grab_sb_stats is false" do
subject { described_class.new(redis: redis) }

let(:expected_result) do
{total: {subscription: 1, fingerprints: 2, subscriptions: 2, channel: 1}}.to_json
end

it "returns total stat" do
expect(subject.collect).to eq(expected_result)
end
end

context "when grab_sb_stats is true" do
subject { described_class.new(redis: redis, grab_sb_stats: true) }

let(:expected_result) do
{
total: {subscription: 1, fingerprints: 2, subscriptions: 2, channel: 1},
grouped_subscription_stats: {
"productCreated"=> 1,
"productUpdated"=> 1
}
}.to_json
end

it "returns total stat with grouped subscription stats" do
expect(subject.collect).to eq(expected_result)
end
end
end
end

0 comments on commit b6a0696

Please sign in to comment.