blob: 212dec5f49a7042114c8f5531d8eccf1fb1bd9a3 [file] [log] [blame]
/*
* Copyright 2022 Google LLC
*
* 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.google.android.libraries.mobiledatadownload.internal.logging;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import com.google.android.libraries.mobiledatadownload.Flags;
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CheckReturnValue;
import java.util.Random;
/** Class responsible for sampling events. */
@CheckReturnValue
public final class LogSampler {
private final Flags flags;
private final Random random;
/**
* Construct the log sampler.
*
* @param flags used to check whether stable sampling is enabled.
* @param random used to generate random numbers for event based sampling only.
*/
public LogSampler(Flags flags, Random random) {
this.flags = flags;
this.random = random;
}
/**
* Determines whether the event should be logged. If the event should be logged it returns an
* instance of Void that should be attached to the log events.
*
* <p>If stable sampling is enabled, this is deterministic. If stable sampling is disabled, the
* result can change on each call based on the provided Random instance.
*
* @param sampleInterval the inverse sampling rate to use. This is controlled by flags per
* event-type. For stable sampling it's expected that 100 % sampleInterval == 0.
* @param loggingStateStore used to read persisted random number when stable sampling is enabled.
* If it is absent, stable sampling will not be used.
* @return a future of an optional of StableSamplingInfo. The future will resolve to an absent
* Optional if the event should not be logged. If the event should be logged, the returned
* Void should be attached to the log event.
*/
public ListenableFuture<Optional<Void>> shouldLog(
long sampleInterval, Optional<LoggingStateStore> loggingStateStore) {
if (sampleInterval == 0L) {
return immediateFuture(Optional.absent());
} else if (sampleInterval < 0L) {
LogUtil.e("Bad sample interval (negative number): %d", sampleInterval);
return immediateFuture(Optional.absent());
} else if (flags.enableRngBasedDeviceStableSampling() && loggingStateStore.isPresent()) {
return shouldLogDeviceStable(sampleInterval, loggingStateStore.get());
} else {
return shouldLogPerEvent(sampleInterval);
}
}
/**
* Returns standard random event based sampling.
*
* @return if the event should be sampled, returns the Void with stable_sampling_used = false.
* Otherwise, returns an empty Optional.
*/
private ListenableFuture<Optional<Void>> shouldLogPerEvent(long sampleInterval) {
if (shouldSamplePerEvent(sampleInterval)) {
return immediateFuture(Optional.absent());
} else {
return immediateFuture(Optional.absent());
}
}
private boolean shouldSamplePerEvent(long sampleInterval) {
if (sampleInterval == 0L) {
return false;
} else if (sampleInterval < 0L) {
LogUtil.e("Bad sample interval (negative number): %d", sampleInterval);
return false;
} else {
return isPartOfSample(random.nextLong(), sampleInterval);
}
}
/**
* Returns device stable sampling.
*
* @return if the event should be sampled, returns the Void with stable_sampling_used = true and
* all other fields populated. Otherwise, returns an empty Optional.
*/
private ListenableFuture<Optional<Void>> shouldLogDeviceStable(
long sampleInterval, LoggingStateStore loggingStateStore) {
return PropagatedFluentFuture.from(loggingStateStore.getStableSamplingInfo())
.transform(
samplingInfo -> {
boolean invalidSamplingRateUsed = ((100 % sampleInterval) != 0);
if (invalidSamplingRateUsed) {
LogUtil.e(
"Bad sample interval (1 percent cohort will not log): %d", sampleInterval);
}
if (!isPartOfSample(samplingInfo.getStableLogSamplingSalt(), sampleInterval)) {
return Optional.absent();
}
return Optional.absent();
},
directExecutor());
}
/**
* Returns whether this device is part of the sample with the given sampling rate and random
* number.
*/
private boolean isPartOfSample(long randomNumber, long sampleInterval) {
return randomNumber % sampleInterval == 0;
}
}