Skip to content

Commit

Permalink
Typed stats similar to Java library + zero default for empty stats
Browse files Browse the repository at this point in the history
Followed convention use in Java library for stats whereby a Hash is not returned by instead a typed object is returned.

Where sparse stats (i.e. no key / value pair when zero) are returned from the API, a value of zero is returned
  • Loading branch information
mattheworiordan committed Mar 26, 2015
1 parent d44025f commit cec0349
Show file tree
Hide file tree
Showing 4 changed files with 386 additions and 31 deletions.
53 changes: 48 additions & 5 deletions lib/ably/models/stat.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'ably/models/stat_types'

module Ably::Models
# Convert stat argument to a {Stat} object
#
Expand Down Expand Up @@ -115,14 +117,55 @@ def expected_length(format)
#
def initialize(hash_object)
@raw_hash_object = hash_object

set_hash_object hash_object
end

%w( all inbound outbound persisted connections channels api_requests token_requests ).each do |attribute|
define_method attribute do
hash[attribute.to_sym]
end
# Aggregates inbound and outbound messages
# return {@StatTypes::MessageTypes}
def all
@all ||= StatTypes::MessageTypes.new(hash[:all])
end

# All inbound messages i.e. received by Ably from clients
# @return {StatTypes::MessageTraffic}
def inbound
@inbound ||= StatTypes::MessageTraffic.new(hash[:inbound])
end

# All outbound messages i.e. sent from Ably to clients
# @return {StatTypes::MessageTraffic}
def outbound
@outbound ||= StatTypes::MessageTraffic.new(hash[:outbound])
end

# Messages persisted for later retrieval via the history API
# @return {StatTypes::MessageTypes}
def persisted
@persisted ||= StatTypes::MessageTypes.new(hash[:persisted])
end

# Breakdown of connection stats data for different (TLS vs non-TLS) connection types
# @return {StatTypes::ConnectionTypes}
def connections
@connections ||= StatTypes::ConnectionTypes.new(hash[:connections])
end

# Breakdown of channels stats
# @return {StatTypes::ResourceCount}
def channels
@channels ||= StatTypes::ResourceCount.new(hash[:channels])
end

# Breakdown of API requests received via the REST API
# @return {StatTypes::RequestCount}
def api_requests
@api_requests ||= StatTypes::RequestCount.new(hash[:api_requests])
end

# Breakdown of Token requests received via the REST API
# @return {StatTypes::RequestCount}
def token_requests
@token_requests ||= StatTypes::RequestCount.new(hash[:token_requests])
end

# @!attribute [r] interval_id
Expand Down
131 changes: 131 additions & 0 deletions lib/ably/models/stat_types.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
module Ably::Models
class StatTypes
# StatStruct is a basic Struct like class that allows methods to be defined
# on the class that will be retuned co-erced objects from the underlying hash used to
# initialize the object.
#
# This class provides a concise way to create classes that have fixed attributes and types
#
# @example
# class MessageCount < StatStruct
# coerce_attributes :count, :data, into: Integer
# end
#
# @api private
#
class StatStruct
class << self
def coerce_attributes(*attributes)
options = attributes.pop
raise ArgumentError, 'Expected attribute into: within options hash' unless options.kind_of?(Hash) && options[:into]

@type_klass = options[:into]
setup_attribute_methods attributes
end

def type_klass
@type_klass
end

private
def setup_attribute_methods(attributes)
attributes.each do |attr|
define_method(attr) do
# Lazy load the co-erced value only when accessed
unless instance_variable_defined?("@#{attr}")
instance_variable_set "@#{attr}", self.class.type_klass.new(hash[attr.to_sym])
end
instance_variable_get("@#{attr}")
end
end
end
end

attr_reader :hash

def initialize(hash)
@hash = hash || {}
end
end

# IntegerDefaultZero will always return an Integer object and will default to value 0 unless truthy
#
# @api private
#
class IntegerDefaultZero
def self.new(value)
(value && value.to_i) || 0
end
end

# MessageCount contains aggregate counts for messages and data transferred
# @!attribute [r] count
# @return [Integer] count of all messages
# @!attribute [r] data
# @return [Integer] total data transferred for all messages in bytes
class MessageCount < StatStruct
coerce_attributes :count, :data, into: IntegerDefaultZero
end

# RequestCount contains aggregate counts for requests made
# @!attribute [r] succeeded
# @return [Integer] requests succeeded
# @!attribute [r] failed
# @return [Integer] requests failed
# @!attribute [r] refused
# @return [Integer] requests refused typically as a result of permissions or a limit being exceeded
class RequestCount < StatStruct
coerce_attributes :succeeded, :failed, :refused, into: IntegerDefaultZero
end

# ResourceCount contains aggregate data for usage of a resource in a specific scope
# @!attribute [r] opened
# @return [Integer] total resources of this type opened
# @!attribute [r] peak
# @return [Integer] peak resources of this type used for this period
# @!attribute [r] mean
# @return [Integer] average resources of this type used for this period
# @!attribute [r] min
# @return [Integer] minimum total resources of this type used for this period
# @!attribute [r] refused
# @return [Integer] resource requests refused within this period
class ResourceCount < StatStruct
coerce_attributes :opened, :peak, :mean, :min, :refused, into: IntegerDefaultZero
end

# ConnectionTypes contains a breakdown of summary stats data for different (TLS vs non-TLS) connection types
# @!attribute [r] tls
# @return [ResourceCount] TLS connection count
# @!attribute [r] plain
# @return [ResourceCount] non-TLS connection count (unencrypted)
# @!attribute [r] all
# @return [ResourceCount] all connection count (includes both TLS & non-TLS connections)
class ConnectionTypes < StatStruct
coerce_attributes :tls, :plain, :all, into: ResourceCount
end

# MessageTypes contains a breakdown of summary stats data for different (message vs presence) message types
# @!attribute [r] messages
# @return [MessageCount] count of channel messages
# @!attribute [r] presence
# @return [MessageCount] count of presence messages
# @!attribute [r] all
# @return [MessageCount] all messages count (includes both presence & messages)
class MessageTypes < StatStruct
coerce_attributes :messages, :presence, :all, into: MessageCount
end

# MessageTraffic contains a breakdown of summary stats data for traffic over various transport types
# @!attribute [r] realtime
# @return [MessageTypes] count of messages transferred over a real-time transport such as WebSockets
# @!attribute [r] rest
# @return [MessageTypes] count of messages transferred using REST
# @!attribute [r] webhook
# @return [MessageTypes] count of messages delivered using WebHooks
# @!attribute [r] all
# @return [MessageTypes] all messages count (includes realtime, rest and webhook messages)
class MessageTraffic < StatStruct
coerce_attributes :realtime, :rest, :webhook, :all, into: MessageTypes
end
end
end
53 changes: 29 additions & 24 deletions spec/acceptance/rest/stats_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,49 +51,54 @@
expect(subject.count).to eql(1)
end

it 'returns zero value for any missing metrics' do
expect(stat.channels.refused).to eql(0)
expect(stat.outbound.webhook.all.count).to eql(0)
end

it 'returns all aggregated message data' do
expect(stat.all[:messages][:count]).to eql(70 + 40) # inbound + outbound
expect(stat.all[:messages][:data]).to eql(7000 + 4000) # inbound + outbound
expect(stat.all.messages.count).to eql(70 + 40) # inbound + outbound
expect(stat.all.messages.data).to eql(7000 + 4000) # inbound + outbound
end

it 'returns inbound realtime all data' do
expect(stat.inbound[:realtime][:all][:count]).to eql(70)
expect(stat.inbound[:realtime][:all][:data]).to eql(7000)
expect(stat.inbound.realtime.all.count).to eql(70)
expect(stat.inbound.realtime.all.data).to eql(7000)
end

it 'returns inbound realtime message data' do
expect(stat.inbound[:realtime][:messages][:count]).to eql(70)
expect(stat.inbound[:realtime][:messages][:data]).to eql(7000)
expect(stat.inbound.realtime.messages.count).to eql(70)
expect(stat.inbound.realtime.messages.data).to eql(7000)
end

it 'returns outbound realtime all data' do
expect(stat.outbound[:realtime][:all][:count]).to eql(40)
expect(stat.outbound[:realtime][:all][:data]).to eql(4000)
expect(stat.outbound.realtime.all.count).to eql(40)
expect(stat.outbound.realtime.all.data).to eql(4000)
end

it 'returns persisted presence all data' do
expect(stat.persisted[:all][:count]).to eql(20)
expect(stat.persisted[:all][:data]).to eql(2000)
expect(stat.persisted.all.count).to eql(20)
expect(stat.persisted.all.data).to eql(2000)
end

it 'returns connections all data' do
expect(stat.connections[:tls][:peak]).to eql(20)
expect(stat.connections[:tls][:opened]).to eql(10)
expect(stat.connections.tls.peak).to eql(20)
expect(stat.connections.tls.opened).to eql(10)
end

it 'returns channels all data' do
expect(stat.channels[:peak]).to eql(50)
expect(stat.channels[:opened]).to eql(30)
expect(stat.channels.peak).to eql(50)
expect(stat.channels.opened).to eql(30)
end

it 'returns api_requests data' do
expect(stat.api_requests[:succeeded]).to eql(50)
expect(stat.api_requests[:failed]).to eql(10)
expect(stat.api_requests.succeeded).to eql(50)
expect(stat.api_requests.failed).to eql(10)
end

it 'returns token_requests data' do
expect(stat.token_requests[:succeeded]).to eql(60)
expect(stat.token_requests[:failed]).to eql(20)
expect(stat.token_requests.succeeded).to eql(60)
expect(stat.token_requests.failed).to eql(20)
end

it 'returns stat objects with #interval_granularity equal to :minute' do
Expand All @@ -115,15 +120,15 @@
let(:stat) { subject.first}

it 'returns the first interval stats as stats are provided forwards from :start' do
expect(stat.inbound[:realtime][:all][:count]).to eql(first_inbound_realtime_count)
expect(stat.inbound.realtime.all.count).to eql(first_inbound_realtime_count)
end

it 'returns 3 pages of stats' do
expect(subject).to be_first_page
expect(subject).to_not be_last_page
page3 = subject.next_page.next_page
expect(page3).to be_last_page
expect(page3.first.inbound[:realtime][:all][:count]).to eql(last_inbound_realtime_count)
expect(page3.first.inbound.realtime.all.count).to eql(last_inbound_realtime_count)
end
end

Expand All @@ -132,14 +137,14 @@
let(:stat) { subject.first}

it 'returns the 3rd interval stats first as stats are provided backwards from :end' do
expect(stat.inbound[:realtime][:all][:count]).to eql(last_inbound_realtime_count)
expect(stat.inbound.realtime.all.count).to eql(last_inbound_realtime_count)
end

it 'returns 3 pages of stats' do
expect(subject).to be_first_page
expect(subject).to_not be_last_page
page3 = subject.next_page.next_page
expect(page3.first.inbound[:realtime][:all][:count]).to eql(first_inbound_realtime_count)
expect(page3.first.inbound.realtime.all.count).to eql(first_inbound_realtime_count)
end
end
end
Expand All @@ -162,8 +167,8 @@
it 'should aggregate the stats for that period' do
expect(subject.count).to eql(1)

expect(stat.all[:messages][:count]).to eql(aggregate_messages_count)
expect(stat.all[:messages][:data]).to eql(aggregate_messages_data)
expect(stat.all.messages.count).to eql(aggregate_messages_count)
expect(stat.all.messages.data).to eql(aggregate_messages_data)
end
end
end
Expand Down
Loading

0 comments on commit cec0349

Please sign in to comment.