1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.internal.logging; 17 18 import static com.google.common.util.concurrent.Futures.immediateFuture; 19 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 20 21 import com.google.android.libraries.mobiledatadownload.Flags; 22 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture; 23 import com.google.common.base.Optional; 24 import com.google.common.util.concurrent.ListenableFuture; 25 import com.google.errorprone.annotations.CheckReturnValue; 26 import com.google.mobiledatadownload.LogProto.StableSamplingInfo; 27 28 import java.util.Random; 29 30 /** Class responsible for sampling events. */ 31 @CheckReturnValue 32 public final class LogSampler { 33 34 private final Flags flags; 35 private final Random random; 36 37 /** 38 * Construct the log sampler. 39 * 40 * @param flags used to check whether stable sampling is enabled. 41 * @param random used to generate random numbers for event based sampling only. 42 */ LogSampler(Flags flags, Random random)43 public LogSampler(Flags flags, Random random) { 44 this.flags = flags; 45 this.random = random; 46 } 47 48 /** 49 * Determines whether the event should be logged. If the event should be logged it returns an 50 * instance of StableSamplingInfo that should be attached to the log events. 51 * 52 * <p>If stable sampling is enabled, this is deterministic. If stable sampling is disabled, the 53 * result can change on each call based on the provided Random instance. 54 * 55 * @param sampleInterval the inverse sampling rate to use. This is controlled by flags per 56 * event-type. For stable sampling it's expected that 100 % 57 * sampleInterval == 0. 58 * @param loggingStateStore used to read persisted random number when stable sampling is 59 * enabled. 60 * If it is absent, stable sampling will not be used. 61 * @return a future of an optional of StableSamplingInfo. The future will resolve to an absent 62 * Optional if the event should not be logged. If the event should be logged, the returned 63 * StableSamplingInfo should be attached to the log event. 64 */ shouldLog( long sampleInterval, Optional<LoggingStateStore> loggingStateStore)65 public ListenableFuture<Optional<StableSamplingInfo>> shouldLog( 66 long sampleInterval, Optional<LoggingStateStore> loggingStateStore) { 67 if (sampleInterval == 0L) { 68 return immediateFuture(Optional.absent()); 69 } else if (sampleInterval < 0L) { 70 LogUtil.e("Bad sample interval (negative number): %d", sampleInterval); 71 return immediateFuture(Optional.absent()); 72 } else if (flags.enableRngBasedDeviceStableSampling() && loggingStateStore.isPresent()) { 73 return shouldLogDeviceStable(sampleInterval, loggingStateStore.get()); 74 } else { 75 return shouldLogPerEvent(sampleInterval); 76 } 77 } 78 79 /** 80 * Returns standard random event based sampling. 81 * 82 * @return if the event should be sampled, returns the StableSamplingInfo with 83 * stable_sampling_used = false. Otherwise, returns an empty Optional. 84 */ shouldLogPerEvent(long sampleInterval)85 private ListenableFuture<Optional<StableSamplingInfo>> shouldLogPerEvent(long sampleInterval) { 86 if (shouldSamplePerEvent(sampleInterval)) { 87 return immediateFuture( 88 Optional.of( 89 StableSamplingInfo.newBuilder().setStableSamplingUsed(false).build())); 90 } else { 91 return immediateFuture(Optional.absent()); 92 } 93 } 94 shouldSamplePerEvent(long sampleInterval)95 private boolean shouldSamplePerEvent(long sampleInterval) { 96 if (sampleInterval == 0L) { 97 return false; 98 } else if (sampleInterval < 0L) { 99 LogUtil.e("Bad sample interval (negative number): %d", sampleInterval); 100 return false; 101 } else { 102 return isPartOfSample(random.nextLong(), sampleInterval); 103 } 104 } 105 106 /** 107 * Returns device stable sampling. 108 * 109 * @return if the event should be sampled, returns the StableSamplingInfo with 110 * stable_sampling_used = true and all other fields populated. Otherwise, returns an empty 111 * Optional. 112 */ shouldLogDeviceStable( long sampleInterval, LoggingStateStore loggingStateStore)113 private ListenableFuture<Optional<StableSamplingInfo>> shouldLogDeviceStable( 114 long sampleInterval, LoggingStateStore loggingStateStore) { 115 return PropagatedFluentFuture.from(loggingStateStore.getStableSamplingInfo()) 116 .transform( 117 samplingInfo -> { 118 boolean invalidSamplingRateUsed = ((100 % sampleInterval) != 0); 119 if (invalidSamplingRateUsed) { 120 LogUtil.e( 121 "Bad sample interval (1 percent cohort will not log): %d", 122 sampleInterval); 123 } 124 125 if (!isPartOfSample(samplingInfo.getStableLogSamplingSalt(), 126 sampleInterval)) { 127 return Optional.absent(); 128 } 129 130 return Optional.of( 131 StableSamplingInfo.newBuilder() 132 .setStableSamplingUsed(true) 133 .setStableSamplingFirstEnabledTimestampMs( 134 TimestampsUtil.toMillis( 135 samplingInfo.getLogSamplingSaltSetTimestamp())) 136 .setPartOfAlwaysLoggingGroup( 137 isPartOfSample( 138 samplingInfo.getStableLogSamplingSalt(), /* sampleInterval= */ 139 100)) 140 .setInvalidSamplingRateUsed(invalidSamplingRateUsed) 141 .build()); 142 }, 143 directExecutor()); 144 } 145 146 /** 147 * Returns whether this device is part of the sample with the given sampling rate and random 148 * number. 149 */ isPartOfSample(long randomNumber, long sampleInterval)150 private boolean isPartOfSample(long randomNumber, long sampleInterval) { 151 return randomNumber % sampleInterval == 0; 152 } 153 } 154