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.immediateVoidFuture; 19 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 20 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import com.google.android.libraries.mobiledatadownload.Flags; 25 import com.google.android.libraries.mobiledatadownload.Logger; 26 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture; 27 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 28 import com.google.common.base.Optional; 29 import com.google.common.util.concurrent.AsyncCallable; 30 import com.google.common.util.concurrent.FutureCallback; 31 import com.google.common.util.concurrent.ListenableFuture; 32 import com.google.mobiledatadownload.LogEnumsProto.MddClientEvent; 33 import com.google.mobiledatadownload.LogEnumsProto.MddDownloadResult; 34 import com.google.mobiledatadownload.LogProto.AndroidClientInfo; 35 import com.google.mobiledatadownload.LogProto.DataDownloadFileGroupStats; 36 import com.google.mobiledatadownload.LogProto.MddDeviceInfo; 37 import com.google.mobiledatadownload.LogProto.MddDownloadResultLog; 38 import com.google.mobiledatadownload.LogProto.MddLogData; 39 import com.google.mobiledatadownload.LogProto.MddStorageStats; 40 import com.google.mobiledatadownload.LogProto.StableSamplingInfo; 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.List; 44 45 /** Assembles data and logs them with underlying {@link Logger}. */ 46 public final class MddEventLogger implements EventLogger { 47 48 private static final String TAG = "MddEventLogger"; 49 50 private final Context context; 51 private final Logger logger; 52 // A process that has mdi download module loaded will get restarted if a new module version is 53 // installed. 54 private final int moduleVersion; 55 private final String hostPackageName; 56 private final Flags flags; 57 private final LogSampler logSampler; 58 59 private Optional<LoggingStateStore> loggingStateStore = Optional.absent(); 60 MddEventLogger( Context context, Logger logger, int moduleVersion, LogSampler logSampler, Flags flags)61 public MddEventLogger( 62 Context context, Logger logger, int moduleVersion, LogSampler logSampler, Flags flags) { 63 this.context = context; 64 this.logger = logger; 65 this.moduleVersion = moduleVersion; 66 this.hostPackageName = context.getPackageName(); 67 this.logSampler = logSampler; 68 this.flags = flags; 69 } 70 71 /** 72 * This should be called before MddEventLogger is used. If it is not called before MddEventLogger 73 * is used, stable sampling will not be used. 74 * 75 * <p>Note(rohitsat): this is required because LoggingStateStore is constructed with a PDS in the 76 * MainMddLibModule. MddEventLogger is required to construct the MainMddLibModule. 77 * 78 * @param loggingStateStore the LoggingStateStore that contains the persisted random number for 79 * stable sampling. 80 */ setLoggingStateStore(LoggingStateStore loggingStateStore)81 public void setLoggingStateStore(LoggingStateStore loggingStateStore) { 82 this.loggingStateStore = Optional.of(loggingStateStore); 83 } 84 85 @Override logEventSampled(MddClientEvent.Code eventCode)86 public void logEventSampled(MddClientEvent.Code eventCode) { 87 sampleAndSendLogEvent(eventCode, MddLogData.newBuilder(), flags.mddDefaultSampleInterval()); 88 } 89 90 @Override logEventSampled( MddClientEvent.Code eventCode, String fileGroupName, int fileGroupVersionNumber, long buildId, String variantId)91 public void logEventSampled( 92 MddClientEvent.Code eventCode, 93 String fileGroupName, 94 int fileGroupVersionNumber, 95 long buildId, 96 String variantId) { 97 98 DataDownloadFileGroupStats dataDownloadFileGroupStats = 99 DataDownloadFileGroupStats.newBuilder() 100 .setFileGroupName(fileGroupName) 101 .setFileGroupVersionNumber(fileGroupVersionNumber) 102 .setBuildId(buildId) 103 .setVariantId(variantId) 104 .build(); 105 106 sampleAndSendLogEvent( 107 eventCode, 108 MddLogData.newBuilder().setDataDownloadFileGroupStats(dataDownloadFileGroupStats), 109 flags.mddDefaultSampleInterval()); 110 } 111 112 @Override logEventAfterSample(MddClientEvent.Code eventCode, int sampleInterval)113 public void logEventAfterSample(MddClientEvent.Code eventCode, int sampleInterval) { 114 // TODO(b/138392640): delete this method once the pds migration is complete. If it's necessary 115 // for other use cases, we can establish a pattern where this class is still responsible for 116 // sampling. 117 MddLogData.Builder logData = MddLogData.newBuilder(); 118 processAndSendEventWithoutStableSampling(eventCode, logData, sampleInterval); 119 } 120 121 @Override logMddApiCallStats(DataDownloadFileGroupStats fileGroupDetails, Void apiCallStats)122 public void logMddApiCallStats(DataDownloadFileGroupStats fileGroupDetails, Void apiCallStats) { 123 // TODO(b/144684763): update this to use stable sampling. Leaving it as is for now since it is 124 // fairly high volume. 125 long sampleInterval = flags.apiLoggingSampleInterval(); 126 if (!LogUtil.shouldSampleInterval(sampleInterval)) { 127 return; 128 } 129 MddLogData.Builder logData = 130 MddLogData.newBuilder().setDataDownloadFileGroupStats(fileGroupDetails); 131 processAndSendEventWithoutStableSampling( 132 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, logData, sampleInterval); 133 } 134 135 @Override logMddLibApiResultLog(Void mddLibApiResultLog)136 public void logMddLibApiResultLog(Void mddLibApiResultLog) { 137 MddLogData.Builder logData = MddLogData.newBuilder(); 138 139 sampleAndSendLogEvent( 140 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, logData, flags.apiLoggingSampleInterval()); 141 } 142 143 @Override logMddFileGroupStats( AsyncCallable<List<EventLogger.FileGroupStatusWithDetails>> buildFileGroupStats)144 public ListenableFuture<Void> logMddFileGroupStats( 145 AsyncCallable<List<EventLogger.FileGroupStatusWithDetails>> buildFileGroupStats) { 146 return lazySampleAndSendLogEvent( 147 MddClientEvent.Code.DATA_DOWNLOAD_FILE_GROUP_STATUS, 148 () -> 149 PropagatedFutures.transform( 150 buildFileGroupStats.call(), 151 fileGroupStatusAndDetailsList -> { 152 List<MddLogData> allMddLogData = new ArrayList<>(); 153 154 for (FileGroupStatusWithDetails fileGroupStatusAndDetails : 155 fileGroupStatusAndDetailsList) { 156 allMddLogData.add( 157 MddLogData.newBuilder() 158 .setMddFileGroupStatus(fileGroupStatusAndDetails.fileGroupStatus()) 159 .setDataDownloadFileGroupStats( 160 fileGroupStatusAndDetails.fileGroupDetails()) 161 .build()); 162 } 163 return allMddLogData; 164 }, 165 directExecutor()), 166 flags.groupStatsLoggingSampleInterval()); 167 } 168 169 @Override logMddStorageStats( AsyncCallable<MddStorageStats> buildStorageStats)170 public ListenableFuture<Void> logMddStorageStats( 171 AsyncCallable<MddStorageStats> buildStorageStats) { 172 return lazySampleAndSendLogEvent( 173 MddClientEvent.Code.DATA_DOWNLOAD_STORAGE_STATS, 174 () -> 175 PropagatedFutures.transform( 176 buildStorageStats.call(), 177 storageStats -> 178 Arrays.asList(MddLogData.newBuilder().setMddStorageStats(storageStats).build()), 179 directExecutor()), 180 flags.storageStatsLoggingSampleInterval()); 181 } 182 183 @Override logMddNetworkStats(AsyncCallable<Void> buildNetworkStats)184 public ListenableFuture<Void> logMddNetworkStats(AsyncCallable<Void> buildNetworkStats) { 185 return lazySampleAndSendLogEvent( 186 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, 187 () -> 188 PropagatedFutures.transform( 189 buildNetworkStats.call(), networkStats -> Arrays.asList(), directExecutor()), 190 flags.networkStatsLoggingSampleInterval()); 191 } 192 193 @Override logMddDataDownloadFileExpirationEvent(int eventCode, int count)194 public void logMddDataDownloadFileExpirationEvent(int eventCode, int count) { 195 MddLogData.Builder logData = MddLogData.newBuilder(); 196 sampleAndSendLogEvent( 197 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, logData, flags.mddDefaultSampleInterval()); 198 } 199 200 @Override logMddNetworkSavings( DataDownloadFileGroupStats fileGroupDetails, int code, long fullFileSize, long downloadedFileSize, String fileId, int deltaIndex)201 public void logMddNetworkSavings( 202 DataDownloadFileGroupStats fileGroupDetails, 203 int code, 204 long fullFileSize, 205 long downloadedFileSize, 206 String fileId, 207 int deltaIndex) { 208 MddLogData.Builder logData = MddLogData.newBuilder(); 209 210 sampleAndSendLogEvent( 211 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, logData, flags.mddDefaultSampleInterval()); 212 } 213 214 @Override logMddQueryStats(DataDownloadFileGroupStats fileGroupDetails)215 public void logMddQueryStats(DataDownloadFileGroupStats fileGroupDetails) { 216 MddLogData.Builder logData = MddLogData.newBuilder(); 217 218 sampleAndSendLogEvent( 219 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, logData, flags.mddDefaultSampleInterval()); 220 } 221 222 @Override logMddDownloadLatency( DataDownloadFileGroupStats fileGroupDetails, Void downloadLatency)223 public void logMddDownloadLatency( 224 DataDownloadFileGroupStats fileGroupDetails, Void downloadLatency) { 225 MddLogData.Builder logData = 226 MddLogData.newBuilder().setDataDownloadFileGroupStats(fileGroupDetails); 227 228 sampleAndSendLogEvent( 229 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, logData, flags.mddDefaultSampleInterval()); 230 } 231 232 @Override logMddDownloadResult( MddDownloadResult.Code code, DataDownloadFileGroupStats fileGroupDetails)233 public void logMddDownloadResult( 234 MddDownloadResult.Code code, DataDownloadFileGroupStats fileGroupDetails) { 235 MddLogData.Builder logData = 236 MddLogData.newBuilder() 237 .setMddDownloadResultLog( 238 MddDownloadResultLog.newBuilder() 239 .setResult(code) 240 .setDataDownloadFileGroupStats(fileGroupDetails)); 241 242 sampleAndSendLogEvent( 243 MddClientEvent.Code.DATA_DOWNLOAD_RESULT_LOG, logData, flags.mddDefaultSampleInterval()); 244 } 245 246 @Override logMddAndroidSharingLog(Void event)247 public void logMddAndroidSharingLog(Void event) { 248 // TODO(b/144684763): consider moving this to stable sampling depending on frequency of events. 249 long sampleInterval = flags.mddAndroidSharingSampleInterval(); 250 if (!LogUtil.shouldSampleInterval(sampleInterval)) { 251 return; 252 } 253 MddLogData.Builder logData = MddLogData.newBuilder(); 254 processAndSendEventWithoutStableSampling( 255 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, logData, sampleInterval); 256 } 257 258 @Override logMddUsageEvent(DataDownloadFileGroupStats fileGroupDetails, Void usageEventLog)259 public void logMddUsageEvent(DataDownloadFileGroupStats fileGroupDetails, Void usageEventLog) { 260 MddLogData.Builder logData = 261 MddLogData.newBuilder().setDataDownloadFileGroupStats(fileGroupDetails); 262 263 sampleAndSendLogEvent( 264 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, logData, flags.mddDefaultSampleInterval()); 265 } 266 267 @Override logNewConfigReceived( DataDownloadFileGroupStats fileGroupDetails, Void newConfigReceivedInfo)268 public void logNewConfigReceived( 269 DataDownloadFileGroupStats fileGroupDetails, Void newConfigReceivedInfo) { 270 MddLogData.Builder logData = 271 MddLogData.newBuilder().setDataDownloadFileGroupStats(fileGroupDetails); 272 sampleAndSendLogEvent( 273 MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, logData, flags.mddDefaultSampleInterval()); 274 } 275 276 /** 277 * Determines whether the log event will be a part of the sample, and if so calls {@code 278 * buildStats} to construct the log event. This is like {@link sampleAndSendLogEvent} but 279 * constructs the log event lazy. This is useful if constructing the log event is expensive. 280 */ lazySampleAndSendLogEvent( MddClientEvent.Code eventCode, AsyncCallable<List<MddLogData>> buildStats, int sampleInterval)281 private ListenableFuture<Void> lazySampleAndSendLogEvent( 282 MddClientEvent.Code eventCode, 283 AsyncCallable<List<MddLogData>> buildStats, 284 int sampleInterval) { 285 return PropagatedFutures.transformAsync( 286 logSampler.shouldLog(sampleInterval, loggingStateStore), 287 samplingInfoOptional -> { 288 if (!samplingInfoOptional.isPresent()) { 289 return immediateVoidFuture(); 290 } 291 292 return PropagatedFluentFuture.from(buildStats.call()) 293 .transform( 294 icingLogDataList -> { 295 if (icingLogDataList != null) { 296 for (MddLogData icingLogData : icingLogDataList) { 297 processAndSendEvent( 298 eventCode, 299 icingLogData.toBuilder(), 300 sampleInterval, 301 samplingInfoOptional.get()); 302 } 303 } 304 return null; 305 }, 306 directExecutor()); 307 }, 308 directExecutor()); 309 } 310 311 private void sampleAndSendLogEvent( 312 MddClientEvent.Code eventCode, MddLogData.Builder logData, long sampleInterval) { 313 // NOTE: When using a single-threaded executor, logging may be delayed since other 314 // work will come before the log sampler check. 315 PropagatedFutures.addCallback( 316 logSampler.shouldLog(sampleInterval, loggingStateStore), 317 new FutureCallback<Optional<StableSamplingInfo>>() { 318 @Override 319 public void onSuccess(Optional<StableSamplingInfo> stableSamplingInfo) { 320 if (stableSamplingInfo.isPresent()) { 321 processAndSendEvent(eventCode, logData, sampleInterval, stableSamplingInfo.get()); 322 } 323 } 324 325 @Override 326 public void onFailure(Throwable t) { 327 LogUtil.e(t, "%s: failure when sampling log!", TAG); 328 } 329 }, 330 directExecutor()); 331 } 332 333 /** Adds all transforms common to all logs and sends the event to Logger. */ 334 private void processAndSendEventWithoutStableSampling( 335 MddClientEvent.Code eventCode, MddLogData.Builder logData, long sampleInterval) { 336 processAndSendEvent( 337 eventCode, 338 logData, 339 sampleInterval, 340 StableSamplingInfo.newBuilder().setStableSamplingUsed(false).build()); 341 } 342 343 /** Adds all transforms common to all logs and sends the event to Logger. */ 344 private void processAndSendEvent( 345 MddClientEvent.Code eventCode, 346 MddLogData.Builder logData, 347 long sampleInterval, 348 StableSamplingInfo stableSamplingInfo) { 349 if (eventCode.equals(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED)) { 350 LogUtil.e("%s: unspecified code used, skipping event log", TAG); 351 // return early for unspecified codes. 352 return; 353 } 354 logData 355 .setSamplingInterval(sampleInterval) 356 .setDeviceInfo(MddDeviceInfo.newBuilder().setDeviceStorageLow(isDeviceStorageLow(context))) 357 .setAndroidClientInfo( 358 AndroidClientInfo.newBuilder() 359 .setHostPackageName(hostPackageName) 360 .setModuleVersion(moduleVersion)) 361 .setStableSamplingInfo(stableSamplingInfo); 362 logger.log(logData.build(), eventCode.getNumber()); 363 } 364 365 /** Returns whether the device is in low storage state. */ 366 private static boolean isDeviceStorageLow(Context context) { 367 // Check if the system says storage is low, by reading the sticky intent. 368 return context.registerReceiver(null, new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW)) 369 != null; 370 } 371 } 372