Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

route_sender: replace min/max with TDigests #351

Merged
merged 1 commit into from
Oct 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions airbrake-ruby.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ DESC

s.required_ruby_version = '>= 2.0'

s.add_dependency 'tdigest', '= 0.1.1'

s.add_development_dependency 'rspec', '~> 3'
s.add_development_dependency 'rake', '~> 10'
s.add_development_dependency 'pry', '~> 0'
Expand Down
2 changes: 1 addition & 1 deletion lib/airbrake-ruby/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def self.parse(response, logger)

begin
case code
when 200
when 200, 204
logger.debug("#{LOG_LABEL} #{name} (#{code}): #{body}")
{ response.msg => response.body }
when 201
Expand Down
69 changes: 62 additions & 7 deletions lib/airbrake-ruby/route_sender.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,76 @@
require 'tdigest'
require 'base64'

module Airbrake
# RouteSender aggregates information about requests and periodically sends
# collected data to Airbrake.
# @since v2.13.0
class RouteSender
# Monkey-patch https://github.com/castle/tdigest to pack with Big Endian
# (instead of Little Endian) since our backend wants it.
#
# @see https://github.com/castle/tdigest/blob/master/lib/tdigest/tdigest.rb
# @since v2.13.0
# @api private
module TDigestBigEndianness
refine TDigest::TDigest do
# rubocop:disable Metrics/AbcSize
def as_small_bytes
size = @centroids.size
output = [self.class::SMALL_ENCODING, compression, size]
x = 0
# delta encoding allows saving 4-bytes floats
mean_arr = @centroids.map do |_, c|
val = c.mean - x
x = c.mean
val
end
output += mean_arr
# Variable length encoding of numbers
c_arr = @centroids.each_with_object([]) do |(_, c), arr|
k = 0
n = c.n
while n < 0 || n > 0x7f
b = 0x80 | (0x7f & n)
arr << b
n = n >> 7
k += 1
raise 'Unreasonable large number' if k > 6
end
arr << n
end
output += c_arr
output.pack("NGNg#{size}C#{size}")
end
# rubocop:enable Metrics/AbcSize
end
end

using TDigestBigEndianness

# The key that represents a route.
RouteKey = Struct.new(:method, :route, :statusCode, :time)

# RouteStat holds data that describes a route's performance.
RouteStat = Struct.new(:count, :sum, :sumsq, :min, :max) do
RouteStat = Struct.new(:count, :sum, :sumsq, :tdigest) do
# @param [Integer] count The number of requests
# @param [Float] sum The sum of request duration in milliseconds
# @param [Float] sumsq The squared sum of request duration in milliseconds
# @param [Float] min The minimal request duration
# @param [Float] max The maximum request duration
def initialize(count: 0, sum: 0.0, sumsq: 0.0, min: 0.0, max: 0.0)
super(count, sum, sumsq, min, max)
# @param [TDigest::TDigest] tdigest
def initialize(count: 0, sum: 0.0, sumsq: 0.0, tdigest: TDigest::TDigest.new)
super(count, sum, sumsq, tdigest)
end

# @return [Hash{String=>Object}] the route stat as a hash with compressed
# and serialized as binary base64 tdigest
def to_h
tdigest.compress!
{
'count' => count,
'sum' => sum,
'sumsq' => sumsq,
'tDigest' => Base64.strict_encode64(tdigest.as_small_bytes)
}
end
end

Expand Down Expand Up @@ -66,8 +122,7 @@ def increment_stats(stat, dur)
stat.sum += ms
stat.sumsq += ms * ms

stat.min = ms if ms < stat.min || stat.min == 0
stat.max = ms if ms > stat.max
stat.tdigest.push(ms)
end

def schedule_flush(promise)
Expand Down
2 changes: 1 addition & 1 deletion spec/response_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
let(:out) { StringIO.new }
let(:logger) { Logger.new(out) }

[200, 201].each do |code|
[200, 201, 204].each do |code|
context "when response code is #{code}" do
it "logs response body" do
described_class.parse(OpenStruct.new(code: code, body: '{}'), logger)
Expand Down
8 changes: 4 additions & 4 deletions spec/route_sender_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@
{"routes":\[
{"method":"GET","route":"/foo","statusCode":200,
"time":"2018-01-01T00:00:00\+00:00","count":1,"sum":24.0,
"sumsq":576.0,"min":24.0,"max":24.0},
"sumsq":576.0,"tDigest":"AAAAAkBZAAAAAAAAAAAAAUHAAAAB"},
{"method":"GET","route":"/foo","statusCode":200,
"time":"2018-01-01T00:01:00\+00:00","count":1,"sum":10.0,
"sumsq":100.0,"min":10.0,"max":10.0}\]}
"sumsq":100.0,"tDigest":"AAAAAkBZAAAAAAAAAAAAAUEgAAAB"}\]}
\z|x
)
).to have_been_made
Expand All @@ -67,10 +67,10 @@
{"routes":\[
{"method":"GET","route":"/foo","statusCode":200,
"time":"2018-01-01T00:00:00\+00:00","count":1,"sum":24.0,
"sumsq":576.0,"min":24.0,"max":24.0},
"sumsq":576.0,"tDigest":"AAAAAkBZAAAAAAAAAAAAAUHAAAAB"},
{"method":"POST","route":"/foo","statusCode":200,
"time":"2018-01-01T00:00:00\+00:00","count":1,"sum":10.0,
"sumsq":100.0,"min":10.0,"max":10.0}\]}
"sumsq":100.0,"tDigest":"AAAAAkBZAAAAAAAAAAAAAUEgAAAB"}\]}
\z|x
)
).to have_been_made
Expand Down