Skip to content

Commit

Permalink
implement client sampling
Browse files Browse the repository at this point in the history
  • Loading branch information
galdor committed Feb 8, 2017
1 parent d7b31ae commit 91f4c20
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 4 deletions.
12 changes: 12 additions & 0 deletions docs/GettingStarted
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ to trace requests to the home page:
[Span] A span tracks a unit of work in a service, like querying a database or rendering a template. Spans are associated
with a service and optionally a resource. Spans have names, start times, durations and optional tags.

== Sampling

_ddtrace_ can perform trace sampling. While the trace agent already samples
traces to reduce bandwidth usage, client sampling reduces performance
overhead.

`Datadog::RateSampler` samples a ratio of the traces. For example:

# Sample rate is between 0 (nothing sampled) to 1 (everything sampled).
sampler = Datadog::RateSampler.new(0.5) # sample 50% of the traces
Datadog.tracer.configure(sampler: sampler)

== Integrations

=== Ruby on \Rails
Expand Down
48 changes: 48 additions & 0 deletions lib/ddtrace/sampler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

module Datadog
# \Sampler performs client-side trace sampling.
class Sampler
def sample(span); end
end

# \AllSampler samples all the traces.
class AllSampler < Sampler
def sample(span)
span.sampled = true
end
end

# \RateSampler is based on a sample rate.
class RateSampler < Sampler
KNUTH_FACTOR = 1111111111111111111
SAMPLE_RATE_METRIC_KEY = '_sample_rate'.freeze()

attr_reader :sample_rate

# Initialize a \RateSampler.
# This sampler keeps a random subset of the traces. Its main purpose is to
# reduce the instrumentation footprint.
#
# * +sample_rate+: the sample rate as a \Float between 0.0 and 1.0. 0.0
# means that no trace will be sampled; 1.0 means that all traces will be
# sampled.
def initialize(sample_rate = 1.0)
unless sample_rate > 0.0 && sample_rate <= 1.0
raise ArgumentError,
'sampling rate must be greater than 0.0 and lower or equal to 1.0'
end

self.sample_rate = sample_rate
end

def sample_rate=(sample_rate)
@sample_rate = sample_rate
@sampling_id_threshold = sample_rate * Span::MAX_ID
end

def sample(span)
span.sampled = ((span.trace_id * KNUTH_FACTOR) % Datadog::Span::MAX_ID) <= @sampling_id_threshold
span.set_metric(SAMPLE_RATE_METRIC_KEY, @sample_rate)
end
end
end
24 changes: 22 additions & 2 deletions lib/ddtrace/span.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Span
attr_accessor :name, :service, :resource, :span_type,
:start_time, :end_time,
:span_id, :trace_id, :parent_id,
:status, :parent
:status, :parent, :sampled

# Create a new span linked to the given tracer. Call the <tt>finish()</tt> method once the
# tracer operation is over or use the <tt>finish_at(time)</tt> helper to close the span with the
Expand All @@ -40,9 +40,11 @@ def initialize(tracer, name, options = {})
@trace_id = options.fetch(:trace_id, @span_id)

@meta = {}
@metrics = {}
@status = 0

@parent = nil
@sampled = true

@start_time = Time.now.utc
@end_time = nil
Expand All @@ -63,6 +65,17 @@ def get_tag(key)
@meta[key]
end

# Set the given key / value metric pair on the span. Keys must be string.
# Values must be floating point numbers.
def set_metric(key, value)
@metrics[key] = value
end

# Return the metric wth the given key, nil if it doesn't exist.
def get_metric(key)
@metrics[key]
end

# Mark the span with the given error.
def set_error(e)
return if e.nil?
Expand Down Expand Up @@ -122,6 +135,7 @@ def to_hash
resource: @resource,
type: @span_type,
meta: @meta,
metrics: @metrics,
error: @status
}

Expand Down Expand Up @@ -151,12 +165,18 @@ def pretty_print(q)
q.text "Start: #{start_time}\n"
q.text "End: #{end_time}\n"
q.text "Duration: #{duration}\n"
q.group(2, 'Tags: [', ']') do
q.group(2, 'Tags: [', "]\n") do
q.breakable
q.seplist @meta.each do |key, value|
q.text "#{key} => #{value}"
end
end
q.group(2, 'Metrics: [', ']') do
q.breakable
q.seplist @metrics.each do |key, value|
q.text "#{key} => #{value}"
end
end
end
end
end
Expand Down
16 changes: 14 additions & 2 deletions lib/ddtrace/tracer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'ddtrace/span'
require 'ddtrace/buffer'
require 'ddtrace/writer'
require 'ddtrace/sampler'

# \Datadog global namespace that includes all tracing functionality for Tracer and Span classes.
module Datadog
Expand All @@ -13,7 +14,7 @@ module Datadog
# Even though the request may require multiple resources and machines to handle the request, all
# of these function calls and sub-requests would be encapsulated within a single trace.
class Tracer
attr_reader :writer, :services
attr_reader :writer, :sampler, :services
attr_accessor :enabled

# Global, memoized, lazy initialized instance of a logger that is used within the the Datadog
Expand Down Expand Up @@ -44,6 +45,8 @@ def self.debug_logging
def initialize(options = {})
@enabled = options.fetch(:enabled, true)
@writer = options.fetch(:writer, Datadog::Writer.new)
@sampler = options.fetch(:sampler, Datadog::AllSampler.new)

@buffer = Datadog::SpanBuffer.new()

@mutex = Mutex.new
Expand All @@ -66,10 +69,12 @@ def configure(options = {})
enabled = options.fetch(:enabled, nil)
hostname = options.fetch(:hostname, nil)
port = options.fetch(:port, nil)
sampler = options.fetch(:sampler, nil)

@enabled = enabled unless enabled.nil?
@writer.transport.hostname = hostname unless hostname.nil?
@writer.transport.port = port unless port.nil?
@sampler = sampler unless sampler.nil?
end

# Set the information about the given service. A valid example is:
Expand Down Expand Up @@ -121,6 +126,13 @@ def trace(name, options = {})
span.set_parent(parent)
@buffer.set(span)

# sampling
if parent.nil?
@sampler.sample(span)
else
span.sampled = span.parent.sampled
end

# call the finish only if a block is given; this ensures
# that a call to tracer.trace() without a block, returns
# a span that should be manually finished.
Expand Down Expand Up @@ -155,7 +167,7 @@ def record(span)
@spans = []
end

return if spans.empty?
return if spans.empty? || !span.sampled
write(spans)
end

Expand Down
85 changes: 85 additions & 0 deletions test/sampler_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@

require 'helper'
require 'ddtrace/sampler'

class SamplerTest < Minitest::Test
def test_all_sampler
spans = [Datadog::Span.new(nil, '', trace_id: 1),
Datadog::Span.new(nil, '', trace_id: 2),
Datadog::Span.new(nil, '', trace_id: 3)]

sampler = Datadog::AllSampler.new()

spans.each do |span|
sampler.sample(span)
assert_equal(true, span.sampled)
end
end

def test_rate_sampler_invalid
assert_raises(ArgumentError) { Datadog::RateSampler.new(-1.0) }
assert_raises(ArgumentError) { Datadog::RateSampler.new(0.0) }
assert_raises(ArgumentError) { Datadog::RateSampler.new(1.5) }
end

def test_rate_sampler_1_0
spans = [Datadog::Span.new(nil, '', trace_id: 1),
Datadog::Span.new(nil, '', trace_id: 2),
Datadog::Span.new(nil, '', trace_id: 3)]

sampler = Datadog::RateSampler.new(1.0)

spans.each do |span|
sampler.sample(span)
assert_equal(true, span.sampled)
assert_equal(1.0, span.get_metric(Datadog::RateSampler::SAMPLE_RATE_METRIC_KEY))
end
end

def test_rate_sampler
prng = Random.new(123)

[0.1, 0.25, 0.5, 0.9].each do |sample_rate|
nb_spans = 1000
spans = Array.new(nb_spans) do
Datadog::Span.new(nil, '', trace_id: prng.rand(Datadog::Span::MAX_ID))
end

sampler = Datadog::RateSampler.new(sample_rate)

spans.each do |span|
sampler.sample(span)

if span.sampled
assert_equal(sample_rate,
span.get_metric(Datadog::RateSampler::SAMPLE_RATE_METRIC_KEY))
end
end

sampled_spans = spans.select { |span| span.sampled }
expected = nb_spans * sample_rate
assert_in_delta(sampled_spans.length, expected, 0.1 * expected)
end
end

def test_tracer_with_rate_sampler
prng = Random.new(123)

tracer = get_test_tracer()
tracer.configure(sampler: Datadog::RateSampler.new(0.5))

nb_spans = 1000
nb_spans.times do
span = tracer.trace('test', trace_id: prng.rand(Datadog::Span::MAX_ID))
span.finish()
end

spans = tracer.writer.spans()
expected = nb_spans * 0.5
assert_in_delta(spans.length, expected, 0.1 * expected)

spans.each do |span|
assert_equal(0.5, span.get_metric(Datadog::RateSampler::SAMPLE_RATE_METRIC_KEY))
end
end
end

0 comments on commit 91f4c20

Please sign in to comment.