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;

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);
}

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);
}
};
}
}
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() {
when(mockSampler.getDescription()).thenReturn("mockDescription");
assertThat(sampler.getDescription()).isEqualTo("AlwaysRecordSampler{mockDescription}");
}

@Test
void testRecordAndSampleSamplingDecision() {
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);
}

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