Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Add AlwaysRecordSampler
([#7877](https://github.com/open-telemetry/opentelemetry-java/pull/7877))

## Version 1.56.0 (2025-11-07)

### API
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

// Includes work from:
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package io.opentelemetry.sdk.trace.internal;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the correct package? How are you planning for this to be consumed by users if it's internal?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Experimental SDK components is the place we've been least consistent about in terms of packaging, since sometimes the concept needs to be bundled into the SDK internals to work (i.e. ExceptionAttributeResolver). But for components which are SDK extension plugin interfaces (span processor, sampler, etc), we've been keeping them in opentelemetry-sdk-extension-incubator. Here's the relevant package for samplers: https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/trace/samplers


import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.TraceState;
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.SamplingDecision;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
import java.util.List;
import javax.annotation.concurrent.Immutable;

/**
* This sampler will return the sampling result of the provided {@link #rootSampler}, unless the
* sampling result contains the sampling decision {@link SamplingDecision#DROP}, in which case, a
* new sampling result will be returned that is functionally equivalent to the original, except that
* it contains the sampling decision {@link SamplingDecision#RECORD_ONLY}. This ensures that all
* spans are recorded, with no change to sampling.
*
* <p>An intended use case of this sampler is to provide a means of sending all spans to a processor
* without having an impact on the sampling rate. This may be desirable if a user wishes to count or
* otherwise measure all spans produced in a service, without incurring the cost of 100% sampling.
*
* <p>This class is internal and experimental. Its APIs are unstable and can change at any time. Its
* APIs (or a version of them) may be promoted to the public stable API in the future, but no
* guarantees are made.
*/
@Immutable
public final class AlwaysRecordSampler implements Sampler {

private final Sampler rootSampler;

public static AlwaysRecordSampler create(Sampler rootSampler) {
return new AlwaysRecordSampler(rootSampler);
}

private AlwaysRecordSampler(Sampler rootSampler) {
this.rootSampler = rootSampler;
}

@Override
public SamplingResult shouldSample(
Context parentContext,
String traceId,
String name,
SpanKind spanKind,
Attributes attributes,
List<LinkData> parentLinks) {
SamplingResult result =
rootSampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks);
if (result.getDecision() == SamplingDecision.DROP) {
result = wrapResultWithRecordOnlyResult(result);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no need to fall through. I think it reads marginally better to return early.

Suggested change
result = wrapResultWithRecordOnlyResult(result);
return wrapResultWithRecordOnlyResult(result);

}

return result;
}

@Override
public String getDescription() {
return "AlwaysRecordSampler{" + rootSampler.getDescription() + "}";
}

private static SamplingResult wrapResultWithRecordOnlyResult(SamplingResult result) {
return new SamplingResult() {
@Override
public SamplingDecision getDecision() {
return SamplingDecision.RECORD_ONLY;
}

@Override
public Attributes getAttributes() {
return result.getAttributes();
}

@Override
public TraceState getUpdatedTraceState(TraceState parentTraceState) {
return result.getUpdatedTraceState(parentTraceState);
}
};
}
Comment on lines +61 to +90
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use a concrete class instead of an anonymous class:

Suggested change
if (result.getDecision() == SamplingDecision.DROP) {
result = wrapResultWithRecordOnlyResult(result);
}
return result;
}
@Override
public String getDescription() {
return "AlwaysRecordSampler{" + rootSampler.getDescription() + "}";
}
private static SamplingResult wrapResultWithRecordOnlyResult(SamplingResult result) {
return new SamplingResult() {
@Override
public SamplingDecision getDecision() {
return SamplingDecision.RECORD_ONLY;
}
@Override
public Attributes getAttributes() {
return result.getAttributes();
}
@Override
public TraceState getUpdatedTraceState(TraceState parentTraceState) {
return result.getUpdatedTraceState(parentTraceState);
}
};
}
if (result.getDecision() != SamplingDecision.DROP) {
return result;
}
return new RecordOnlyDelegateSamplingResult(result);
}
@Override
public String getDescription() {
return "AlwaysRecordSampler{" + rootSampler.getDescription() + "}";
}
private static class RecordOnlyDelegateSamplingResult implements SamplingResult {
private final SamplingResult delegate;
private RecordOnlyDelegateSamplingResult(SamplingResult delegate) {this.delegate = delegate;}
@Override
public SamplingDecision getDecision() {
return SamplingDecision.RECORD_ONLY;
}
@Override
public Attributes getAttributes() {
return delegate.getAttributes();
}
@Override
public TraceState getUpdatedTraceState(TraceState parentTraceState) {
return delegate.getUpdatedTraceState(parentTraceState);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

// Includes work from:
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package io.opentelemetry.sdk.trace.internal;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.TraceId;
import io.opentelemetry.api.trace.TraceState;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/** Unit tests for {@link AlwaysRecordSampler}. */
class AlwaysRecordSamplerTest {

// Mocks
private Sampler mockSampler;

private AlwaysRecordSampler sampler;

@BeforeEach
void setUpSamplers() {
mockSampler = mock(Sampler.class);
sampler = AlwaysRecordSampler.create(mockSampler);
}

@Test
void testGetDescription() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remote test prefix on all these

when(mockSampler.getDescription()).thenReturn("mockDescription");
assertThat(sampler.getDescription()).isEqualTo("AlwaysRecordSampler{mockDescription}");
}

@Test
void testRecordAndSampleSamplingDecision() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use a org.junit.jupiter.params.ParameterizedTest test to improve readability.

validateShouldSample(SamplingDecision.RECORD_AND_SAMPLE, SamplingDecision.RECORD_AND_SAMPLE);
}

@Test
void testRecordOnlySamplingDecision() {
validateShouldSample(SamplingDecision.RECORD_ONLY, SamplingDecision.RECORD_ONLY);
}

@Test
void testDropSamplingDecision() {
validateShouldSample(SamplingDecision.DROP, SamplingDecision.RECORD_ONLY);
}

private void validateShouldSample(
SamplingDecision rootDecision, SamplingDecision expectedDecision) {
SamplingResult rootResult = buildRootSamplingResult(rootDecision);
when(mockSampler.shouldSample(any(), anyString(), anyString(), any(), any(), any()))
.thenReturn(rootResult);
SamplingResult actualResult =
sampler.shouldSample(
Context.current(),
TraceId.fromLongs(1, 2),
"name",
SpanKind.CLIENT,
Attributes.empty(),
Collections.emptyList());

if (rootDecision.equals(expectedDecision)) {
assertThat(actualResult).isEqualTo(rootResult);
assertThat(actualResult.getDecision()).isEqualTo(rootDecision);
} else {
assertThat(actualResult).isNotEqualTo(rootResult);
assertThat(actualResult.getDecision()).isEqualTo(expectedDecision);
}
Comment on lines +80 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think this makes the tests slightly harder to follow, rather than having some minor duplication in the test methods. Not a huge problem.


assertThat(actualResult.getAttributes()).isEqualTo(rootResult.getAttributes());
TraceState traceState = TraceState.builder().build();
assertThat(actualResult.getUpdatedTraceState(traceState))
.isEqualTo(rootResult.getUpdatedTraceState(traceState));
}

private static SamplingResult buildRootSamplingResult(SamplingDecision samplingDecision) {
return new SamplingResult() {
@Override
public SamplingDecision getDecision() {
return samplingDecision;
}

@Override
public Attributes getAttributes() {
return Attributes.of(AttributeKey.stringKey("key"), samplingDecision.name());
}

@Override
public TraceState getUpdatedTraceState(TraceState parentTraceState) {
return TraceState.builder().put("key", samplingDecision.name()).build();
}
};
}
}
Loading