Skip to content

Commit 72541ce

Browse files
authored
Add session ID ratio based sampler (#262)
* Add session ID ratio based sampler * Remove redundant configuration option. * Rename sampling config option and keep boolean field internally. * Update session sampling option readme * Remove parentBased around sampler * profiler: Update documentation
1 parent 7944dd2 commit 72541ce

File tree

5 files changed

+202
-2
lines changed

5 files changed

+202
-2
lines changed

Diff for: README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,9 @@ when initializing your instance of the SplunkRum API:
256256
- `limitDiskUsageMegabytes(int)` :
257257
When disk buffering is enabled, this can be used to adjust the maximum amount of storage
258258
that will be used. Default = 25MB.
259-
259+
- `enableSessionBasedSampling(double)` :
260+
Enable session ID based sampling and set its sampling ratio. The ratio is a probability of a
261+
session being included between between 0.0 (all dropped) and 1.0 (all included).
260262

261263
#### APIs provided by the `SplunkRum` instance:
262264

Diff for: splunk-otel-android/src/main/java/com/splunk/rum/Config.java

+50
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public class Config {
5151
private final Duration slowRenderingDetectionPollInterval;
5252
private final boolean diskBufferingEnabled;
5353
private final int maxUsageMegabytes;
54+
private final boolean sessionBasedSamplerEnabled;
55+
private final double sessionBasedSamplerRatio;
5456

5557
private Config(Builder builder) {
5658
this.beaconEndpoint = builder.beaconEndpoint;
@@ -66,6 +68,8 @@ private Config(Builder builder) {
6668
this.spanFilterExporterDecorator = builder.spanFilterBuilder.build();
6769
this.diskBufferingEnabled = builder.diskBufferingEnabled;
6870
this.maxUsageMegabytes = builder.maxUsageMegabytes;
71+
this.sessionBasedSamplerEnabled = builder.sessionBasedSamplerEnabled;
72+
this.sessionBasedSamplerRatio = builder.sessionBasedSamplerRatio;
6973
}
7074

7175
private Attributes addDeploymentEnvironment(Builder builder) {
@@ -167,6 +171,20 @@ public int getMaxUsageMegabytes() {
167171
return maxUsageMegabytes;
168172
}
169173

174+
/**
175+
* Is session-based sampling of traces enabled or not.
176+
*/
177+
public boolean isSessionBasedSamplerEnabled() {
178+
return sessionBasedSamplerEnabled;
179+
}
180+
181+
/**
182+
* Get ratio of sessions that get sampled (0.0 - 1.0, where 1 is all sessions).
183+
*/
184+
public double getSessionBasedSamplerRatio() {
185+
return sessionBasedSamplerRatio;
186+
}
187+
170188
/**
171189
* Create a new instance of the {@link Builder} class. All default configuration options will be pre-populated.
172190
*/
@@ -214,6 +232,8 @@ public static class Builder {
214232
private String realm;
215233
private Duration slowRenderingDetectionPollInterval = DEFAULT_SLOW_RENDERING_DETECTION_POLL_INTERVAL;
216234
private int maxUsageMegabytes = DEFAULT_MAX_STORAGE_USE_MB;
235+
private boolean sessionBasedSamplerEnabled = false;
236+
private double sessionBasedSamplerRatio = 1.0;
217237

218238
/**
219239
* Create a new instance of {@link Config} from the options provided.
@@ -401,5 +421,35 @@ public Builder limitDiskUsageMegabytes(int maxUsageMegabytes) {
401421
this.maxUsageMegabytes = maxUsageMegabytes;
402422
return this;
403423
}
424+
425+
/**
426+
* Enable/disable session-based sampling of traces. Disabled by default.
427+
*
428+
* @return {@code this}.
429+
*/
430+
public Builder sessionBasedSamplingEnabled(boolean enabled) {
431+
this.sessionBasedSamplerEnabled = enabled;
432+
return this;
433+
}
434+
435+
/**
436+
* Set ratio of sessions that get sampled (0.0 - 1.0, where 1 is all sessions). Default is
437+
* 1.0.
438+
*
439+
* @return {@code this}.
440+
*/
441+
public Builder enableSessionBasedSampling(double ratio) {
442+
if (ratio < 0.0) {
443+
Log.e(SplunkRum.LOG_TAG, "invalid sessionBasedSamplingRatio: " + ratio + " must not be negative");
444+
return this;
445+
} else if (ratio > 1.0) {
446+
Log.e(SplunkRum.LOG_TAG, "invalid sessionBasedSamplingRatio: " + ratio + " must not be greater than 1.0");
447+
return this;
448+
}
449+
450+
this.sessionBasedSamplerEnabled = true;
451+
this.sessionBasedSamplerRatio = ratio;
452+
return this;
453+
}
404454
}
405455
}

Diff for: splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import java.util.ArrayList;
3434
import java.util.Collection;
3535
import java.util.List;
36-
import java.util.Locale;
3736
import java.util.concurrent.Executors;
3837
import java.util.concurrent.ScheduledExecutorService;
3938
import java.util.concurrent.ScheduledFuture;
@@ -238,6 +237,10 @@ private SdkTracerProvider buildTracerProvider(
238237
.setResource(resource);
239238
initializationEvents.add(new RumInitializer.InitializationEvent("tracerProviderBuilderInitialized", timingClock.now()));
240239

240+
if (config.isSessionBasedSamplerEnabled()) {
241+
tracerProviderBuilder.setSampler(new SessionIdRatioBasedSampler(config.getSessionBasedSamplerRatio(), sessionId));
242+
}
243+
241244
if (config.isDebugEnabled()) {
242245
tracerProviderBuilder.addSpanProcessor(
243246
SimpleSpanProcessor.create(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright Splunk Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.splunk.rum;
18+
19+
import java.util.List;
20+
21+
import io.opentelemetry.api.common.Attributes;
22+
import io.opentelemetry.api.trace.SpanKind;
23+
import io.opentelemetry.context.Context;
24+
import io.opentelemetry.sdk.trace.data.LinkData;
25+
import io.opentelemetry.sdk.trace.samplers.Sampler;
26+
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
27+
28+
/**
29+
* Session ID ratio based sampler. Uses {@link Sampler#traceIdRatioBased(double)} sampler
30+
* internally, but passes sessionId instead of traceId to the underlying sampler in order to use the
31+
* same ratio logic but on sessionId instead. This is valid as {@link SessionId} uses
32+
* {@link io.opentelemetry.api.trace.TraceId#fromLongs(long, long)} internally to generate random
33+
* session IDs.
34+
*/
35+
class SessionIdRatioBasedSampler implements Sampler {
36+
private final SessionId sessionId;
37+
private final Sampler ratioBasedSampler;
38+
39+
SessionIdRatioBasedSampler(double ratio, SessionId sessionId) {
40+
this.sessionId = sessionId;
41+
// SessionId uses the same format as TraceId, so we can reuse trace ID ratio sampler.
42+
this.ratioBasedSampler = Sampler.traceIdRatioBased(ratio);
43+
}
44+
45+
@Override
46+
public SamplingResult shouldSample(Context parentContext, String traceId, String name, SpanKind spanKind, Attributes attributes, List<LinkData> parentLinks) {
47+
// Replace traceId with sessionId
48+
return ratioBasedSampler.shouldSample(parentContext, sessionId.getSessionId(), name, spanKind, attributes, parentLinks);
49+
}
50+
51+
@Override
52+
public String getDescription() {
53+
return String.format("SessionIdRatioBased{traceIdRatioBased:%s}", this.ratioBasedSampler.getDescription());
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright Splunk Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.splunk.rum;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
22+
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
import org.mockito.junit.MockitoJUnitRunner;
26+
27+
import java.util.Arrays;
28+
import java.util.Collections;
29+
import java.util.List;
30+
31+
import io.opentelemetry.api.common.Attributes;
32+
import io.opentelemetry.api.trace.Span;
33+
import io.opentelemetry.api.trace.SpanContext;
34+
import io.opentelemetry.api.trace.SpanKind;
35+
import io.opentelemetry.context.Context;
36+
import io.opentelemetry.sdk.trace.IdGenerator;
37+
import io.opentelemetry.sdk.trace.data.LinkData;
38+
import io.opentelemetry.sdk.trace.samplers.Sampler;
39+
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
40+
41+
@RunWith(MockitoJUnitRunner.class)
42+
public class SessionIdRatioBasedSamplerTest {
43+
private static final String HIGH_ID = "00000000000000008fffffffffffffff";
44+
private static final String LOW_ID = "00000000000000000000000000000000";
45+
private static final IdGenerator idsGenerator = IdGenerator.random();
46+
47+
private final String traceId = idsGenerator.generateTraceId();
48+
private final Context parentContext = Context.root().with(Span.getInvalid());
49+
private final List<LinkData> parentLinks = Collections.singletonList(LinkData.create(SpanContext.getInvalid()));
50+
51+
@Test
52+
public void samplerUsesSessionId() {
53+
SessionId sessionId = mock(SessionId.class);
54+
SessionIdRatioBasedSampler sampler = new SessionIdRatioBasedSampler(0.5, sessionId);
55+
56+
// Sampler drops if TraceIdRatioBasedSampler would drop this sessionId
57+
when(sessionId.getSessionId()).thenReturn(HIGH_ID);
58+
assertEquals(shouldSample(sampler), SamplingDecision.DROP);
59+
60+
// Sampler accepts if TraceIdRatioBasedSampler would accept this sessionId
61+
when(sessionId.getSessionId()).thenReturn(LOW_ID);
62+
assertEquals(shouldSample(sampler), SamplingDecision.RECORD_AND_SAMPLE);
63+
}
64+
65+
@Test
66+
public void zeroRatioDropsAll() {
67+
SessionId sessionId = mock(SessionId.class);
68+
SessionIdRatioBasedSampler sampler = new SessionIdRatioBasedSampler(0.0, sessionId);
69+
70+
for (String id : Arrays.asList(HIGH_ID, LOW_ID)) {
71+
when(sessionId.getSessionId()).thenReturn(id);
72+
assertEquals(shouldSample(sampler), SamplingDecision.DROP);
73+
}
74+
}
75+
76+
@Test
77+
public void oneRatioAcceptsAll() {
78+
SessionId sessionId = mock(SessionId.class);
79+
SessionIdRatioBasedSampler sampler = new SessionIdRatioBasedSampler(1.0, sessionId);
80+
81+
for (String id : Arrays.asList(HIGH_ID, LOW_ID)) {
82+
when(sessionId.getSessionId()).thenReturn(id);
83+
assertEquals(shouldSample(sampler), SamplingDecision.RECORD_AND_SAMPLE);
84+
}
85+
}
86+
87+
private SamplingDecision shouldSample(Sampler sampler) {
88+
return sampler.shouldSample(parentContext, traceId, "name", SpanKind.INTERNAL, Attributes.empty(), parentLinks).getDecision();
89+
}
90+
}

0 commit comments

Comments
 (0)