• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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