Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change session-based probabilistic sampling computation #698

Merged
merged 4 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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{radio:%f}", ratio);
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
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());
}
}