Class: Flare::Sampler

Inherits:
Object
  • Object
show all
Defined in:
lib/flare/sampler.rb

Overview

Path 1 trace sampler. At span start, iterates active rules; returns RECORD_AND_SAMPLE when one matches and the deterministic trace_id_ratio falls under the rule’s rate. Otherwise RECORD_ONLY – the span still records so MetricSpanProcessor sees it; the trace export decision for web spans is deferred to Path 2 via Flare::Marker.

Used as the ‘root` sampler inside an OTel ParentBased sampler so root spans go through this logic but child spans inherit upstream decisions. The `local_parent_not_sampled` slot of the ParentBased should point at Flare::ALWAYS_RECORD_ONLY – the default ALWAYS_OFF would drop children of an unsampled local parent, making them NoOp spans the processors never see.

Rules are pushed in via update_rules from Flare::RuleManager; the swap is atomic, and malformed rule entries are dropped with a counter so a bad server payload can’t crash the tracing path.

Defined Under Namespace

Classes: Rule

Constant Summary collapse

Decision =
OpenTelemetry::SDK::Trace::Samplers::Decision
Result =
OpenTelemetry::SDK::Trace::Samplers::Result
RULE_ID_ATTRIBUTE =
"flare.rule_id"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSampler

Returns a new instance of Sampler.



33
34
35
36
# File 'lib/flare/sampler.rb', line 33

def initialize
  @rules_ref = Concurrent::AtomicReference.new([].freeze)
  @dropped_rule_count = Concurrent::AtomicFixnum.new(0)
end

Instance Attribute Details

#dropped_rule_countObject (readonly)

Returns the value of attribute dropped_rule_count.



31
32
33
# File 'lib/flare/sampler.rb', line 31

def dropped_rule_count
  @dropped_rule_count
end

Instance Method Details

#descriptionObject



65
66
67
# File 'lib/flare/sampler.rb', line 65

def description
  "Flare::Sampler"
end

#rulesObject



47
48
49
# File 'lib/flare/sampler.rb', line 47

def rules
  @rules_ref.get
end

#should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:) ⇒ Boolean

Returns:

  • (Boolean)


51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/flare/sampler.rb', line 51

def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
  tracestate = tracestate_from(parent_context)

  rules.each do |rule|
    next unless matches?(rule, attributes)
    next unless trace_id_ratio(trace_id) < rule.rate

    merged = (attributes || {}).merge(RULE_ID_ATTRIBUTE => rule.id)
    return Result.new(decision: Decision::RECORD_AND_SAMPLE, attributes: merged, tracestate: tracestate)
  end

  Result.new(decision: Decision::RECORD_ONLY, tracestate: tracestate)
end

#trace_id_ratio(trace_id) ⇒ Object

Cross-language formula: last 8 bytes of the 16-byte raw trace_id as uint64-big-endian, divided by 2^64. Same in every Flare SDK so the server can reproduce the decision if it ever needs to.



72
73
74
75
76
77
78
# File 'lib/flare/sampler.rb', line 72

def trace_id_ratio(trace_id)
  bytes = trace_id.is_a?(String) ? trace_id.bytes : Array(trace_id)
  tail = bytes.last(8)
  n = 0
  tail.each { |b| n = (n << 8) | b }
  n.to_f / (1 << 64)
end

#update_rules(new_rules) ⇒ Object

new_rules: an array of rule hashes from GET /api/rules, e.g.

[{ "id" => 1, "match_attributes" => {...}, "rate" => 0.5 }, ...]

Entries that don’t validate are skipped (counted in dropped_rule_count).



41
42
43
44
45
# File 'lib/flare/sampler.rb', line 41

def update_rules(new_rules)
  validated = (new_rules || []).filter_map { |r| validate(r) }
  @dropped_rule_count.increment((new_rules || []).length - validated.length)
  @rules_ref.set(validated.freeze)
end