/* * 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 com.google.mobiledatadownload.LogProto.StableSamplingInfo; 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 StableSamplingInfo that should be attached to the log events. * *

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 * StableSamplingInfo should be attached to the log event. */ public ListenableFuture> shouldLog( long sampleInterval, Optional 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 StableSamplingInfo with * stable_sampling_used = false. Otherwise, returns an empty Optional. */ private ListenableFuture> shouldLogPerEvent(long sampleInterval) { if (shouldSamplePerEvent(sampleInterval)) { return immediateFuture( Optional.of( StableSamplingInfo.newBuilder().setStableSamplingUsed(false).build())); } 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 StableSamplingInfo with * stable_sampling_used = true and all other fields populated. Otherwise, returns an empty * Optional. */ private ListenableFuture> 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.of( StableSamplingInfo.newBuilder() .setStableSamplingUsed(true) .setStableSamplingFirstEnabledTimestampMs( TimestampsUtil.toMillis( samplingInfo.getLogSamplingSaltSetTimestamp())) .setPartOfAlwaysLoggingGroup( isPartOfSample( samplingInfo.getStableLogSamplingSalt(), /* sampleInterval= */ 100)) .setInvalidSamplingRateUsed(invalidSamplingRateUsed) .build()); }, 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; } }