Skip to content

Commit 5b0fc25

Browse files
committed
Add ability to configure sampling rate based on transaction span name
1 parent 4cc5e9f commit 5b0fc25

File tree

6 files changed

+184
-8
lines changed

6 files changed

+184
-8
lines changed

docs/reference/configuration.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,18 @@ Limits the amount of spans that are recorded per transaction. This is helpful in
677677
By default, the agent will sample every transaction (e.g. request to your service). To reduce overhead and storage requirements, you can set the sample rate to a value between `0.0` and `1.0`. We still record overall time and the result for unsampled transactions, but no context information, tags, or spans. The sample rate will be rounded to 4 digits of precision.
678678
679679
680+
### `transaction_sample_rate_by_name` [config-transaction-sample-rate-by-name]
681+
682+
| | | | |
683+
| --- | --- | --- | --- |
684+
| Environment | `Config` key | Default | Example |
685+
| `ELASTIC_APM_TRANSACTION_SAMPLE_RATE_BY_NAME` | `transaction_sample_rate_by_name` | `{}` | `{"UsersController#index" => 1.0, "HealthController#ping" => 0.0}` |
686+
687+
Configure specific sampling rates for transactions with particular names. This option takes a hash where the keys are transaction names and the values are sampling rates between `0.0` and `1.0`.
688+
689+
This is useful when you want to ensure critical transactions are always sampled while reducing sampling for high-volume, less important transactions. When a transaction name matches an entry in this hash, the specified sampling rate takes precedence over the global `transaction_sample_rate`.
690+
691+
680692
### `verify_server_cert` [config-verify-server-cert]
681693
682694
| | | |
@@ -731,4 +743,3 @@ Elastic APM patches `Kernel#require` to auto-detect and instrument supported thi
731743
To get around this patch, set the environment variable `ELASTIC_APM_SKIP_REQUIRE_PATCH` to `"1"`.
732744
733745
The agent might need some additional tweaking to make sure the third-party libraries are picked up and instrumented. Make sure you require the agent *after* you require your other dependencies.
734-

lib/elastic_apm/config.rb

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
require 'elastic_apm/config/log_level_map'
2323
require 'elastic_apm/config/options'
2424
require 'elastic_apm/config/round_float'
25+
require 'elastic_apm/config/round_float_hash_value'
2526
require 'elastic_apm/config/regexp_list'
2627
require 'elastic_apm/config/wildcard_pattern_list'
2728
require 'elastic_apm/deprecations'
@@ -98,6 +99,7 @@ class Config
9899
option :transaction_ignore_urls, type: :list, default: [], converter: WildcardPatternList.new
99100
option :transaction_max_spans, type: :int, default: 500
100101
option :transaction_sample_rate, type: :float, default: 1.0, converter: RoundFloat.new
102+
option :transaction_sample_rate_by_name, type: :hash, default: {}, converter: RoundFloatHashValue.new
101103
option :use_elastic_traceparent_header, type: :bool, default: true
102104
option :verify_server_cert, type: :bool, default: true
103105

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Licensed to Elasticsearch B.V. under one or more contributor
2+
# license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright
4+
# ownership. Elasticsearch B.V. licenses this file to you under
5+
# the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
# frozen_string_literal: true
19+
20+
module ElasticAPM
21+
class Config
22+
# @api private
23+
class RoundFloatHashValue
24+
def initialize
25+
@float_converter = RoundFloat.new
26+
end
27+
28+
def call(hash)
29+
return {} unless hash
30+
31+
hash.transform_values { |value| @float_converter.call(value) }
32+
end
33+
end
34+
end
35+
end

lib/elastic_apm/instrumenter.rb

+25-3
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def start_transaction(
123123
sampled = trace_context.recorded?
124124
sample_rate = trace_context.tracestate.sample_rate
125125
else
126-
sampled = random_sample?(config)
126+
sampled = random_sample?(config.transaction_sample_rate)
127127
sample_rate = sampled ? config.transaction_sample_rate : 0
128128
end
129129

@@ -195,6 +195,21 @@ def start_span(
195195
current_transaction
196196
end
197197
return unless transaction
198+
199+
unless trace_context
200+
span_sample_rate = transaction_sample_rate_for_name(name, transaction.config)
201+
# if the span sample rate is different from the transaction sample rate,
202+
# we need to check if the span should be sampled
203+
# and update the transaction's sample rate accordingly
204+
if transaction.started_spans == 0 && span_sample_rate && span_sample_rate != transaction.sample_rate
205+
span_sampled = random_sample?(span_sample_rate)
206+
if transaction.sampled? != span_sampled
207+
transaction.sampled = span_sampled
208+
transaction.sample_rate = span_sample_rate
209+
end
210+
end
211+
end
212+
198213
return unless transaction.sampled?
199214
return unless transaction.inc_started_spans!
200215

@@ -278,10 +293,17 @@ def inspect
278293
'>'
279294
end
280295

296+
def transaction_sample_rate_for_name(name, config)
297+
return if !name || config.transaction_sample_rate_by_name.empty?
298+
return unless config.transaction_sample_rate_by_name.key?(name)
299+
300+
config.transaction_sample_rate_by_name[name]
301+
end
302+
281303
private
282304

283-
def random_sample?(config)
284-
rand <= config.transaction_sample_rate
305+
def random_sample?(sample_rate)
306+
rand <= sample_rate
285307
end
286308

287309
def update_transaction_metrics(transaction)

lib/elastic_apm/transaction.rb

+11-4
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,15 @@ def initialize(
8787
end
8888
# rubocop:enable Metrics/ParameterLists
8989

90-
attr_accessor :name, :type, :result, :outcome
90+
attr_accessor(
91+
:name,
92+
:type,
93+
:result,
94+
:outcome,
95+
:sampled,
96+
:sample_rate,
97+
:started_spans
98+
)
9199

92100
attr_reader(
93101
:breakdown_metrics,
@@ -98,12 +106,11 @@ def initialize(
98106
:framework_name,
99107
:notifications,
100108
:self_time,
101-
:sample_rate,
102109
:span_frames_min_duration,
103-
:started_spans,
104110
:timestamp,
105111
:trace_context,
106-
:transaction_max_spans
112+
:transaction_max_spans,
113+
:config
107114
)
108115

109116
alias :collect_metrics? :collect_metrics

spec/elastic_apm/instrumenter_spec.rb

+99
Original file line numberDiff line numberDiff line change
@@ -469,5 +469,104 @@ module ElasticAPM
469469
end
470470
end
471471
end
472+
473+
describe '#transaction_sample_rate_for_name' do
474+
let(:default_rate) { 0.5 }
475+
let(:special_rate) { 1.0 }
476+
477+
context 'with nil name' do
478+
it 'returns nil' do
479+
config = Config.new(transaction_sample_rate: default_rate)
480+
expect(subject.transaction_sample_rate_for_name(nil, config)).to be_nil
481+
end
482+
end
483+
484+
context 'with empty transaction_sample_rate_by_name' do
485+
it 'returns nil' do
486+
config = Config.new(transaction_sample_rate_by_name: {})
487+
expect(subject.transaction_sample_rate_for_name('Something', config)).to be_nil
488+
end
489+
end
490+
491+
context 'with matching name in transaction_sample_rate_by_name' do
492+
it 'returns matching sample rate' do
493+
config = Config.new(
494+
transaction_sample_rate: default_rate,
495+
transaction_sample_rate_by_name: { 'Something' => special_rate }
496+
)
497+
expect(subject.transaction_sample_rate_for_name('Something', config)).to eq(special_rate)
498+
end
499+
end
500+
501+
context 'with non-matching name in transaction_sample_rate_by_name' do
502+
it 'returns default sample rate' do
503+
config = Config.new(
504+
transaction_sample_rate_by_name: { 'SomethingElse' => special_rate }
505+
)
506+
expect(subject.transaction_sample_rate_for_name('Something', config)).to be_nil
507+
end
508+
end
509+
end
510+
511+
describe 'span-based sampling' do
512+
context 'when starting a span with a name that has a different sampling rate' do
513+
it 'can change the sampling decision of the transaction' do
514+
config = Config.new(
515+
transaction_sample_rate: 0.0,
516+
transaction_sample_rate_by_name: { 'ImportantOperation' => 1.0 }
517+
)
518+
519+
# Force predictable random sampling
520+
allow(subject).to receive(:rand).and_return(0.5)
521+
522+
transaction = subject.start_transaction('Test', config: config)
523+
expect(transaction).not_to be_sampled
524+
525+
# Verify sampling is updated when first span is created
526+
allow(subject).to receive(:random_sample?).and_return(true)
527+
span = subject.start_span('ImportantOperation')
528+
529+
# The span should exist (since the transaction is now sampled)
530+
expect(span).not_to be_nil
531+
expect(transaction).to be_sampled
532+
end
533+
534+
it 'only considers the first span for changing the sampling decision' do
535+
config = Config.new(
536+
transaction_sample_rate: 0.0,
537+
transaction_sample_rate_by_name: {
538+
'FirstSpan' => 1.0,
539+
'SecondSpan' => 0.0,
540+
'ThirdSpan' => 0.5
541+
}
542+
)
543+
544+
# Force predictable random sampling
545+
allow(subject).to receive(:rand).and_return(0.5)
546+
547+
transaction = subject.start_transaction('Test', config: config)
548+
expect(transaction).not_to be_sampled
549+
550+
# First span changes sampling because it's the first span
551+
first_span = subject.start_span('FirstSpan')
552+
expect(first_span).not_to be_nil
553+
expect(transaction).to be_sampled
554+
expect(transaction.sample_rate).to eq(1.0)
555+
556+
# Initial sampling rate stored
557+
original_sample_rate = transaction.sample_rate
558+
559+
# Second span should not change sampling even though it has a different rate
560+
second_span = subject.start_span('SecondSpan')
561+
expect(second_span).not_to be_nil
562+
expect(transaction.sample_rate).to eq(original_sample_rate)
563+
564+
# Third span should not change sampling either
565+
third_span = subject.start_span('ThirdSpan')
566+
expect(third_span).not_to be_nil
567+
expect(transaction.sample_rate).to eq(original_sample_rate)
568+
end
569+
end
570+
end
472571
end
473572
end

0 commit comments

Comments
 (0)