Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ private SamplingConfiguration randomSamplingConfiguration() {
return new SamplingConfiguration(
randomDoubleBetween(0.1, 1.0, true),
randomBoolean() ? randomIntBetween(1, SamplingConfiguration.MAX_SAMPLES_LIMIT) : null,
randomBoolean() ? ByteSizeValue.ofGb(randomLongBetween(1, SamplingConfiguration.MAX_SIZE_LIMIT_GIGABYTES)) : null,
randomBoolean() ? ByteSizeValue.ofMb(randomLongBetween(1, 100)) : null,
randomBoolean() ? randomValidTimeValue() : null,
randomBoolean() ? randomAlphaOfLengthBetween(5, 30) : null
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.monitor.jvm.JvmInfo;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ObjectParser;
import org.elasticsearch.xcontent.ParseField;
Expand Down Expand Up @@ -57,18 +58,20 @@ public record SamplingConfiguration(

// Constants for validation and defaults
public static final int MAX_SAMPLES_LIMIT = 10_000;
public static final long MAX_SIZE_LIMIT_GIGABYTES = 5;
public static final double MAX_SIZE_HEAP_PERCENTAGE_LIMIT = 0.01;
public static final ByteSizeValue DEFAULT_MAX_SIZE_FLOOR = ByteSizeValue.ofKb(100);
public static final long MAX_TIME_TO_LIVE_DAYS = 30;
public static final int DEFAULT_MAX_SAMPLES = 100;
public static final long DEFAULT_MAX_SIZE_GIGABYTES = 1;
public static final long DEFAULT_TIME_TO_LIVE_DAYS = 10;

// Error messages
public static final String INVALID_RATE_MESSAGE = "rate must be greater than 0 and less than or equal to 1";
public static final String INVALID_MAX_SAMPLES_MIN_MESSAGE = "maxSamples must be greater than 0";
public static final String INVALID_MAX_SAMPLES_MAX_MESSAGE = "maxSamples must be less than or equal to " + MAX_SAMPLES_LIMIT;
public static final String INVALID_MAX_SIZE_MIN_MESSAGE = "maxSize must be greater than 0";
public static final String INVALID_MAX_SIZE_MAX_MESSAGE = "maxSize must be less than or equal to " + MAX_SIZE_LIMIT_GIGABYTES + "GB";
public static final String INVALID_MAX_SIZE_MAX_MESSAGE = "maxSize must be less than or equal to "
+ (int) (MAX_SIZE_HEAP_PERCENTAGE_LIMIT * 100)
+ "% of heap size";
public static final String INVALID_TIME_TO_LIVE_MIN_MESSAGE = "timeToLive must be greater than 0";
public static final String INVALID_TIME_TO_LIVE_MAX_MESSAGE = "timeToLive must be less than or equal to "
+ MAX_TIME_TO_LIVE_DAYS
Expand Down Expand Up @@ -152,7 +155,7 @@ public record SamplingConfiguration(
*
* @param rate The fraction of documents to sample (must be between 0 and 1)
* @param maxSamples The maximum number of documents to sample (optional, defaults to {@link #DEFAULT_MAX_SAMPLES})
* @param maxSize The maximum total size of sampled documents (optional, defaults to {@link #DEFAULT_MAX_SIZE_GIGABYTES} GB)
* @param maxSize The maximum total size of sampled documents (optional, defaults to {@link #MAX_SIZE_HEAP_PERCENTAGE_LIMIT} of heap)
* @param timeToLive The duration for which the sampled documents
* should be retained (optional, defaults to {@link #DEFAULT_TIME_TO_LIVE_DAYS} days)
* @param condition An optional condition script that sampled documents must satisfy (optional, can be null)
Expand All @@ -168,12 +171,32 @@ public SamplingConfiguration(
) {
this.rate = rate;
this.maxSamples = maxSamples == null ? DEFAULT_MAX_SAMPLES : maxSamples;
this.maxSize = maxSize == null ? ByteSizeValue.ofGb(DEFAULT_MAX_SIZE_GIGABYTES) : maxSize;
this.maxSize = maxSize == null ? calculateDefaultMaxSize() : maxSize;
this.timeToLive = timeToLive == null ? TimeValue.timeValueDays(DEFAULT_TIME_TO_LIVE_DAYS) : timeToLive;
this.condition = condition;
this.creationTime = creationTime == null ? Instant.now().toEpochMilli() : creationTime;
}

/**
* Calculates the default max size as a percentage of the configured heap size,
* with a minimum floor value.
*
* @return The default max size value
*/
private static ByteSizeValue calculateDefaultMaxSize() {
long heapBasedSize = (long) (MAX_SIZE_HEAP_PERCENTAGE_LIMIT * JvmInfo.jvmInfo().getConfiguredMaxHeapSize());
return ByteSizeValue.ofBytes(Math.max(heapBasedSize, DEFAULT_MAX_SIZE_FLOOR.getBytes()));
}

/**
* Calculates the maximum allowed max size as a percentage of the configured heap size.
*
* @return The maximum allowed max size value
*/
private static ByteSizeValue calculateMaxAllowedSize() {
return ByteSizeValue.ofBytes((long) (MAX_SIZE_HEAP_PERCENTAGE_LIMIT * JvmInfo.jvmInfo().getConfiguredMaxHeapSize()));
}

// Convenience constructor without creationTime
public SamplingConfiguration(double rate, Integer maxSamples, ByteSizeValue maxSize, TimeValue timeToLive, String condition) {
this(rate, maxSamples, maxSize, timeToLive, condition, null);
Expand Down Expand Up @@ -266,7 +289,7 @@ private static void validateInputs(double rate, Integer maxSamples, ByteSizeValu
if (maxSize.compareTo(ByteSizeValue.ZERO) <= 0) {
throw new IllegalArgumentException(INVALID_MAX_SIZE_MIN_MESSAGE);
}
ByteSizeValue maxLimit = ByteSizeValue.ofGb(MAX_SIZE_LIMIT_GIGABYTES);
ByteSizeValue maxLimit = calculateMaxAllowedSize();
if (maxSize.compareTo(maxLimit) > 0) {
throw new IllegalArgumentException(INVALID_MAX_SIZE_MAX_MESSAGE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.monitor.jvm.JvmInfo;
import org.elasticsearch.test.AbstractXContentSerializingTestCase;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentParser;
Expand Down Expand Up @@ -40,17 +41,21 @@ protected Writeable.Reader<SamplingConfiguration> instanceReader() {

@Override
protected SamplingConfiguration createTestInstance() {
long maxHeap = JvmInfo.jvmInfo().getConfiguredMaxHeapSize();
long maxSizeLimit = (long) (SamplingConfiguration.MAX_SIZE_HEAP_PERCENTAGE_LIMIT * maxHeap);
return new SamplingConfiguration(
randomDoubleBetween(0.0, 1.0, true),
randomBoolean() ? null : randomIntBetween(1, SamplingConfiguration.MAX_SAMPLES_LIMIT),
randomBoolean() ? null : ByteSizeValue.ofGb(randomLongBetween(1, SamplingConfiguration.MAX_SIZE_LIMIT_GIGABYTES)),
randomBoolean() ? null : ByteSizeValue.ofBytes(randomLongBetween(1, maxSizeLimit)),
randomBoolean() ? null : TimeValue.timeValueDays(randomLongBetween(1, SamplingConfiguration.MAX_TIME_TO_LIVE_DAYS)),
randomBoolean() ? null : randomAlphaOfLength(10)
);
}

@Override
protected SamplingConfiguration mutateInstance(SamplingConfiguration instance) {
long maxHeap = JvmInfo.jvmInfo().getConfiguredMaxHeapSize();
long maxSizeLimit = (long) (SamplingConfiguration.MAX_SIZE_HEAP_PERCENTAGE_LIMIT * maxHeap);
return switch (randomIntBetween(0, 4)) {
case 0 -> new SamplingConfiguration(
randomValueOtherThan(instance.rate(), () -> randomDoubleBetween(0.0, 1.0, true)),
Expand All @@ -71,7 +76,7 @@ protected SamplingConfiguration mutateInstance(SamplingConfiguration instance) {
instance.maxSamples(),
randomValueOtherThan(
instance.maxSize(),
() -> ByteSizeValue.ofGb(randomLongBetween(1, SamplingConfiguration.MAX_SIZE_LIMIT_GIGABYTES))
() -> ByteSizeValue.ofBytes(randomLongBetween(1, maxSizeLimit))
),
instance.timeToLive(),
instance.condition()
Expand Down Expand Up @@ -101,7 +106,11 @@ public void testDefaults() {
SamplingConfiguration config = new SamplingConfiguration(0.5, null, null, null, null);
assertThat(config.rate(), equalTo(0.5));
assertThat(config.maxSamples(), equalTo(SamplingConfiguration.DEFAULT_MAX_SAMPLES));
assertThat(config.maxSize(), equalTo(ByteSizeValue.ofGb(SamplingConfiguration.DEFAULT_MAX_SIZE_GIGABYTES)));
long expectedDefaultMaxSize = Math.max(
(long) (SamplingConfiguration.MAX_SIZE_HEAP_PERCENTAGE_LIMIT * JvmInfo.jvmInfo().getConfiguredMaxHeapSize()),
SamplingConfiguration.DEFAULT_MAX_SIZE_FLOOR.getBytes()
);
assertThat(config.maxSize(), equalTo(ByteSizeValue.ofBytes(expectedDefaultMaxSize)));
assertThat(config.timeToLive(), equalTo(TimeValue.timeValueDays(SamplingConfiguration.DEFAULT_TIME_TO_LIVE_DAYS)));
assertThat(config.condition(), nullValue());
}
Expand Down Expand Up @@ -157,12 +166,15 @@ public void testValidation() throws IOException {
}
""", SamplingConfiguration.INVALID_MAX_SIZE_MIN_MESSAGE);

// Test max size exceeding heap-based limit
long maxHeap = JvmInfo.jvmInfo().getConfiguredMaxHeapSize();
long maxSizeLimit = (long) (SamplingConfiguration.MAX_SIZE_HEAP_PERCENTAGE_LIMIT * maxHeap);
assertValidationError(String.format(Locale.ROOT, """
{
"rate": 0.5,
"max_size": "%dgb"
"max_size_in_bytes": %d
}
""", SamplingConfiguration.MAX_SIZE_LIMIT_GIGABYTES + 1), SamplingConfiguration.INVALID_MAX_SIZE_MAX_MESSAGE);
""", maxSizeLimit + 1), SamplingConfiguration.INVALID_MAX_SIZE_MAX_MESSAGE);

// Test invalid timeToLive
assertValidationError("""
Expand Down Expand Up @@ -234,6 +246,8 @@ public void testValidInputs() throws IOException {
assertThat(config.maxSamples(), equalTo(1));

// Test boundary conditions - maximum values
long maxHeap = JvmInfo.jvmInfo().getConfiguredMaxHeapSize();
long maxSizeLimit = (long) (SamplingConfiguration.MAX_SIZE_HEAP_PERCENTAGE_LIMIT * maxHeap);
parser = createParser(
JsonXContent.jsonXContent,
String.format(
Expand All @@ -242,13 +256,13 @@ public void testValidInputs() throws IOException {
{
"rate": 1.0,
"max_samples": %d,
"max_size": "%dgb",
"max_size_in_bytes": %d,
"time_to_live": "%dd",
"if": "test_condition"
}
""",
SamplingConfiguration.MAX_SAMPLES_LIMIT,
SamplingConfiguration.MAX_SIZE_LIMIT_GIGABYTES,
maxSizeLimit,
SamplingConfiguration.MAX_TIME_TO_LIVE_DAYS
)
);
Expand Down Expand Up @@ -332,4 +346,21 @@ public void testCreationTimeUserDataRestrictionRaw() throws IOException {
assertNotNull("Expected IllegalArgumentException with creation_time_in_millis message", cause);
assertThat(cause.getMessage(), equalTo("Creation time cannot be set by user (field: creation_time_in_millis)"));
}

public void testMinimumDefaultMaxSize() {
// Test that the minimum default max size is enforced
SamplingConfiguration config = new SamplingConfiguration(0.5, null, null, null, null);

// Calculate what the heap percentage would give us
long heapBasedSize = (long) (SamplingConfiguration.MAX_SIZE_HEAP_PERCENTAGE_LIMIT
* JvmInfo.jvmInfo().getConfiguredMaxHeapSize());
long minSize = SamplingConfiguration.DEFAULT_MAX_SIZE_FLOOR.getBytes();

// The actual default should be the larger of the two
long expectedSize = Math.max(heapBasedSize, minSize);
assertThat(config.maxSize().getBytes(), equalTo(expectedSize));

// Verify it's at least the minimum
assertThat(config.maxSize().getBytes(), greaterThanOrEqualTo(minSize));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ private SamplingConfiguration createRandomSamplingConfiguration() {
return new SamplingConfiguration(
randomDoubleBetween(0.0, 1.0, true),
randomBoolean() ? null : randomIntBetween(1, SamplingConfiguration.MAX_SAMPLES_LIMIT),
randomBoolean() ? null : ByteSizeValue.ofGb(randomLongBetween(1, SamplingConfiguration.MAX_SIZE_LIMIT_GIGABYTES)),
randomBoolean() ? null : ByteSizeValue.ofMb(randomLongBetween(1, 100)),
randomBoolean() ? null : TimeValue.timeValueDays(randomLongBetween(1, SamplingConfiguration.MAX_TIME_TO_LIVE_DAYS)),
randomBoolean() ? null : randomAlphaOfLength(10)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ private SamplingConfiguration createRandomSamplingConfiguration() {
return new SamplingConfiguration(
randomDoubleBetween(0.0, 1.0, true),
randomBoolean() ? null : randomIntBetween(1, SamplingConfiguration.MAX_SAMPLES_LIMIT),
randomBoolean() ? null : ByteSizeValue.ofGb(randomLongBetween(1, SamplingConfiguration.MAX_SIZE_LIMIT_GIGABYTES)),
randomBoolean() ? null : ByteSizeValue.ofMb(randomLongBetween(1, 100)),
randomBoolean() ? null : TimeValue.timeValueDays(randomLongBetween(1, SamplingConfiguration.MAX_TIME_TO_LIVE_DAYS)),
randomBoolean() ? null : randomAlphaOfLength(10)
);
Expand Down