Skip to content

Commit

Permalink
Change session-based probabilistic sampling computation (#698)
Browse files Browse the repository at this point in the history
* new sampler impl

* add the UInt32QuadXorTraceIdRatioSampler to better align with the experience on web and ios.

* add comment

* typo
  • Loading branch information
breedx-splk authored Dec 6, 2023
1 parent cfe58a1 commit ceed9cf
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.splunk.rum.internal.UInt32QuadXorTraceIdRatioSampler;
import io.opentelemetry.android.GlobalAttributesSpanAppender;
import io.opentelemetry.android.OpenTelemetryRum;
import io.opentelemetry.android.OpenTelemetryRumBuilder;
import io.opentelemetry.android.RuntimeDetailsExtractor;
import io.opentelemetry.android.SessionIdRatioBasedSampler;
import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker;
import io.opentelemetry.android.instrumentation.anr.AnrDetector;
import io.opentelemetry.android.instrumentation.crash.CrashReporter;
Expand All @@ -57,8 +57,10 @@
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import java.time.Duration;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
Expand Down Expand Up @@ -149,13 +151,20 @@ SplunkRum initialize(
.build()));

// Set up the sampler, if enabled
// TODO: Make this better...
// This holder is required because we cannot reasonably get the session id until after
// OpenTelemetryRum has been created. So this is spackled into place below.
AtomicReference<Supplier<String>> sessionSupplierHolder = new AtomicReference<>(() -> null);
if (builder.sessionBasedSamplerEnabled) {
otelRumBuilder.addTracerProviderCustomizer(
(tracerProviderBuilder, app) -> {
SessionIdRatioBasedSampler sampler =
new SessionIdRatioBasedSampler(
Sampler sampler =
UInt32QuadXorTraceIdRatioSampler.create(
builder.sessionBasedSamplerRatio,
otelRumBuilder.getSessionId());
() -> {
Supplier<String> supplier = sessionSupplierHolder.get();
return supplier == null ? null : supplier.get();
});
return tracerProviderBuilder.setSampler(sampler);
});
}
Expand Down Expand Up @@ -210,6 +219,8 @@ SplunkRum initialize(

OpenTelemetryRum openTelemetryRum = otelRumBuilder.build();

sessionSupplierHolder.set(openTelemetryRum::getRumSessionId);

initializationEvents.recordInitializationSpans(
builder.getConfigFlags(),
openTelemetryRum.getOpenTelemetry().getTracer(RUM_TRACER_NAME));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.rum.internal;

import android.util.Log;

public class SessionUtils {

/** Performs an unsigned 32-bit conversion of the hex session id to a long. */
static long convertToUInt32(String sessionId) {
long acc = 0L;
for (int i = 0; i < sessionId.length(); i += 8) {
long chunk = 0;
try {
String chunkString = sessionId.substring(i, i + 8);
chunk = Long.parseUnsignedLong(chunkString, 16);
} catch (NumberFormatException e) {
Log.w("SplunkRum", "Error parsing session id into long: " + sessionId);
}
acc = acc ^ chunk;
}
return acc;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.rum.internal;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.data.LinkData;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
import java.util.List;
import java.util.Locale;
import java.util.function.Supplier;

/**
* This class is very similar to the SessionIdRatioBasedSampler from upstream, but exists in order
* to perform a trace id into a long calculation in a way that is more consistent with iOS and js.
*
* <p>This class should be considered a stop-gap measure until this problem is correctly spec'd in
* otel.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public class UInt32QuadXorTraceIdRatioSampler implements Sampler {
static final SamplingResult POSITIVE_SAMPLING_RESULT = SamplingResult.recordAndSample();

static final SamplingResult NEGATIVE_SAMPLING_RESULT = SamplingResult.drop();
private final long idUpperBound;
private final String description;
private final Supplier<String> sessionIdSupplier;
private final Object lock = new Object();
private String lastSeenSessionId = "";
private SamplingResult lastSamplingResult = NEGATIVE_SAMPLING_RESULT;

public static Sampler create(double ratio, Supplier<String> sessionIdSupplier) {
// Taken directly mostly from the TraceIdRatioBasedSampler in upstream, with a modification
// to the upper bound to make it within UInt32.
if (ratio < 0.0 || ratio > 1.0) {
throw new IllegalArgumentException("ratio must be in range [0.0, 1.0]");
}
long idUpperBound;
// Special case the limits, to avoid any possible issues with lack of precision across
// double/long boundaries. For probability == 0.0, we use Long.MIN_VALUE as this guarantees
// that we will never sample a trace, even in the case where the id == Long.MIN_VALUE, since
// Math.Abs(Long.MIN_VALUE) == Long.MIN_VALUE.
if (ratio == 0.0) {
idUpperBound = Long.MIN_VALUE;
} else if (ratio == 1.0) {
idUpperBound = Long.MAX_VALUE;
} else {
// ratio * UInt32 max value
idUpperBound = (long) (ratio * 0xFFFFFFFFL);
}
String description =
String.format(
Locale.getDefault(), "UInt32QuadXorTraceIdRatioSampler{ratio:%f}", ratio);
return new UInt32QuadXorTraceIdRatioSampler(idUpperBound, sessionIdSupplier, description);
}

private UInt32QuadXorTraceIdRatioSampler(
long idUpperBound, Supplier<String> sessionIdSupplier, String description) {
this.idUpperBound = idUpperBound;
this.sessionIdSupplier = sessionIdSupplier;
this.description = description;
}

@Override
public SamplingResult shouldSample(
Context parentContext,
String traceId,
String name,
SpanKind spanKind,
Attributes attributes,
List<LinkData> parentLinks) {
String sessionId = sessionIdSupplier.get();
if (sessionId == null) {
return POSITIVE_SAMPLING_RESULT; // Have to return true because we may not have a
// session yet
}
synchronized (lock) {
if (lastSeenSessionId.equals(sessionId)) {
return lastSamplingResult;
}
lastSeenSessionId = sessionId;
lastSamplingResult =
SessionUtils.convertToUInt32(sessionId) < idUpperBound
? POSITIVE_SAMPLING_RESULT
: NEGATIVE_SAMPLING_RESULT;
return lastSamplingResult;
}
}

@Override
public String getDescription() {
return description;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.rum.internal;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

class SessionUtilsTest {

@Test
void testConvert() {
long result = SessionUtils.convertToUInt32("c06947ed1f53b1a69be3c6899bc11a3e");
assertEquals(3742903036L, result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.splunk.rum.internal;

import static com.splunk.rum.internal.UInt32QuadXorTraceIdRatioSampler.NEGATIVE_SAMPLING_RESULT;
import static com.splunk.rum.internal.UInt32QuadXorTraceIdRatioSampler.POSITIVE_SAMPLING_RESULT;
import static org.junit.jupiter.api.Assertions.assertEquals;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
import java.util.Collections;
import org.junit.jupiter.api.Test;

class UInt32QuadXorTraceIdRatioSamplerTest {

private final Context parentContext = Context.root().with(Span.getInvalid());

@Test
void sampleInclude() {
Sampler sampler =
UInt32QuadXorTraceIdRatioSampler.create(
0.5, () -> "4777abcd3f7777abcdefc6899bc11a3e");
SamplingResult result =
sampler.shouldSample(
parentContext,
null,
null,
null,
Attributes.empty(),
Collections.emptyList());
assertEquals(POSITIVE_SAMPLING_RESULT.getDecision(), result.getDecision());
}

@Test
void sampleDrop() {
Sampler sampler =
UInt32QuadXorTraceIdRatioSampler.create(
0.5, () -> "9777abcd3f7777abcdefc6899bc11a3e");
SamplingResult result =
sampler.shouldSample(
parentContext,
null,
null,
null,
Attributes.empty(),
Collections.emptyList());
assertEquals(NEGATIVE_SAMPLING_RESULT.getDecision(), result.getDecision());
}

@Test
void nullSessionMeansAlwaysPositive() {
Sampler sampler = UInt32QuadXorTraceIdRatioSampler.create(0.00000001, () -> null);
SamplingResult result =
sampler.shouldSample(
parentContext,
null,
null,
null,
Attributes.empty(),
Collections.emptyList());
assertEquals(POSITIVE_SAMPLING_RESULT.getDecision(), result.getDecision());
}
}

0 comments on commit ceed9cf

Please sign in to comment.