1 /* 2 * Copyright (C) 2021 The Android Open Source Project 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 17 package com.android.server.appsearch.stats; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.appsearch.exceptions.AppSearchException; 22 import android.content.Context; 23 import android.os.Process; 24 import android.os.SystemClock; 25 import android.util.ArrayMap; 26 import android.util.Log; 27 import android.util.SparseIntArray; 28 29 import com.android.internal.annotations.GuardedBy; 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.server.appsearch.AppSearchConfig; 32 import com.android.server.appsearch.external.localstorage.AppSearchLogger; 33 import com.android.server.appsearch.external.localstorage.stats.CallStats; 34 import com.android.server.appsearch.external.localstorage.stats.InitializeStats; 35 import com.android.server.appsearch.external.localstorage.stats.OptimizeStats; 36 import com.android.server.appsearch.external.localstorage.stats.PutDocumentStats; 37 import com.android.server.appsearch.external.localstorage.stats.RemoveStats; 38 import com.android.server.appsearch.external.localstorage.stats.SearchStats; 39 import com.android.server.appsearch.util.PackageUtil; 40 41 import java.io.UnsupportedEncodingException; 42 import java.security.MessageDigest; 43 import java.security.NoSuchAlgorithmException; 44 import java.util.Map; 45 import java.util.Objects; 46 import java.util.Random; 47 48 /** 49 * Logger Implementation for pushed atoms. 50 * 51 * <p>This class is thread-safe. 52 * 53 * @hide 54 */ 55 public final class PlatformLogger implements AppSearchLogger { 56 private static final String TAG = "AppSearchPlatformLogger"; 57 58 // Context of the user we're logging for. 59 private final Context mUserContext; 60 61 // Manager holding the configuration flags 62 private final AppSearchConfig mConfig; 63 64 private final Random mRng = new Random(); 65 private final Object mLock = new Object(); 66 67 /** 68 * SparseArray to track how many stats we skipped due to 69 * {@link AppSearchConfig#getCachedMinTimeIntervalBetweenSamplesMillis()}. 70 * 71 * <p> We can have correct extrapolated number by adding those counts back when we log 72 * the same type of stats next time. E.g. the true count of an event could be estimated as: 73 * SUM(sampling_interval * (num_skipped_sample + 1)) as est_count 74 * 75 * <p>The key to the SparseArray is {@link CallStats.CallType} 76 */ 77 @GuardedBy("mLock") 78 private final SparseIntArray mSkippedSampleCountLocked = 79 new SparseIntArray(); 80 81 /** 82 * Map to cache the packageUid for each package. 83 * 84 * <p>It maps packageName to packageUid. 85 * 86 * <p>The entry will be removed whenever the app gets uninstalled 87 */ 88 @GuardedBy("mLock") 89 private final Map<String, Integer> mPackageUidCacheLocked = 90 new ArrayMap<>(); 91 92 /** 93 * Elapsed time for last stats logged from boot in millis 94 */ 95 @GuardedBy("mLock") 96 private long mLastPushTimeMillisLocked = 0; 97 98 /** 99 * Helper class to hold platform specific stats for statsd. 100 */ 101 static final class ExtraStats { 102 // UID for the calling package of the stats. 103 final int mPackageUid; 104 // sampling interval for the call type of the stats. 105 final int mSamplingInterval; 106 // number of samplings skipped before the current one for the same call type. 107 final int mSkippedSampleCount; 108 ExtraStats(int packageUid, int samplingInterval, int skippedSampleCount)109 ExtraStats(int packageUid, int samplingInterval, int skippedSampleCount) { 110 mPackageUid = packageUid; 111 mSamplingInterval = samplingInterval; 112 mSkippedSampleCount = skippedSampleCount; 113 } 114 } 115 116 /** 117 * Constructor 118 */ PlatformLogger( @onNull Context userContext, @NonNull AppSearchConfig config)119 public PlatformLogger( 120 @NonNull Context userContext, 121 @NonNull AppSearchConfig config) { 122 mUserContext = Objects.requireNonNull(userContext); 123 mConfig = Objects.requireNonNull(config); 124 } 125 126 /** Logs {@link CallStats}. */ 127 @Override logStats(@onNull CallStats stats)128 public void logStats(@NonNull CallStats stats) { 129 Objects.requireNonNull(stats); 130 synchronized (mLock) { 131 if (shouldLogForTypeLocked(stats.getCallType())) { 132 logStatsImplLocked(stats); 133 } 134 } 135 } 136 137 /** Logs {@link PutDocumentStats}. */ 138 @Override logStats(@onNull PutDocumentStats stats)139 public void logStats(@NonNull PutDocumentStats stats) { 140 Objects.requireNonNull(stats); 141 synchronized (mLock) { 142 if (shouldLogForTypeLocked(CallStats.CALL_TYPE_PUT_DOCUMENT)) { 143 logStatsImplLocked(stats); 144 } 145 } 146 } 147 148 @Override logStats(@onNull InitializeStats stats)149 public void logStats(@NonNull InitializeStats stats) { 150 Objects.requireNonNull(stats); 151 synchronized (mLock) { 152 if (shouldLogForTypeLocked(CallStats.CALL_TYPE_INITIALIZE)) { 153 logStatsImplLocked(stats); 154 } 155 } 156 } 157 158 @Override logStats(@onNull SearchStats stats)159 public void logStats(@NonNull SearchStats stats) { 160 Objects.requireNonNull(stats); 161 synchronized (mLock) { 162 if (shouldLogForTypeLocked(CallStats.CALL_TYPE_SEARCH)) { 163 logStatsImplLocked(stats); 164 } 165 } 166 } 167 168 @Override logStats(@onNull RemoveStats stats)169 public void logStats(@NonNull RemoveStats stats) { 170 // TODO(b/173532925): Log stats 171 } 172 173 @Override logStats(@onNull OptimizeStats stats)174 public void logStats(@NonNull OptimizeStats stats) { 175 Objects.requireNonNull(stats); 176 synchronized (mLock) { 177 if (shouldLogForTypeLocked(CallStats.CALL_TYPE_OPTIMIZE)) { 178 logStatsImplLocked(stats); 179 } 180 } 181 } 182 183 /** 184 * Removes cached UID for package. 185 * 186 * @return removed UID for the package, or {@code INVALID_UID} if package was not previously 187 * cached. 188 */ removeCachedUidForPackage(@onNull String packageName)189 public int removeCachedUidForPackage(@NonNull String packageName) { 190 // TODO(b/173532925) This needs to be called when we get PACKAGE_REMOVED intent 191 Objects.requireNonNull(packageName); 192 synchronized (mLock) { 193 Integer uid = mPackageUidCacheLocked.remove(packageName); 194 return uid != null ? uid : Process.INVALID_UID; 195 } 196 } 197 198 @GuardedBy("mLock") logStatsImplLocked(@onNull CallStats stats)199 private void logStatsImplLocked(@NonNull CallStats stats) { 200 mLastPushTimeMillisLocked = SystemClock.elapsedRealtime(); 201 ExtraStats extraStats = createExtraStatsLocked(stats.getPackageName(), stats.getCallType()); 202 String database = stats.getDatabase(); 203 try { 204 int hashCodeForDatabase = calculateHashCodeMd5(database); 205 AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_CALL_STATS_REPORTED, 206 extraStats.mSamplingInterval, 207 extraStats.mSkippedSampleCount, 208 extraStats.mPackageUid, 209 hashCodeForDatabase, 210 stats.getStatusCode(), 211 stats.getTotalLatencyMillis(), 212 stats.getCallType(), 213 stats.getEstimatedBinderLatencyMillis(), 214 stats.getNumOperationsSucceeded(), 215 stats.getNumOperationsFailed()); 216 } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { 217 // TODO(b/184204720) report hashing error to statsd 218 // We need to set a special value(e.g. 0xFFFFFFFF) for the hashing of the database, 219 // so in the dashboard we know there is some error for hashing. 220 // 221 // Something is wrong while calculating the hash code for database 222 // this shouldn't happen since we always use "MD5" and "UTF-8" 223 if (database != null) { 224 Log.e(TAG, "Error calculating hash code for database " + database, e); 225 } 226 } 227 } 228 229 @GuardedBy("mLock") logStatsImplLocked(@onNull PutDocumentStats stats)230 private void logStatsImplLocked(@NonNull PutDocumentStats stats) { 231 mLastPushTimeMillisLocked = SystemClock.elapsedRealtime(); 232 ExtraStats extraStats = createExtraStatsLocked( 233 stats.getPackageName(), CallStats.CALL_TYPE_PUT_DOCUMENT); 234 String database = stats.getDatabase(); 235 try { 236 int hashCodeForDatabase = calculateHashCodeMd5(database); 237 AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_PUT_DOCUMENT_STATS_REPORTED, 238 extraStats.mSamplingInterval, 239 extraStats.mSkippedSampleCount, 240 extraStats.mPackageUid, 241 hashCodeForDatabase, 242 stats.getStatusCode(), 243 stats.getTotalLatencyMillis(), 244 stats.getGenerateDocumentProtoLatencyMillis(), 245 stats.getRewriteDocumentTypesLatencyMillis(), 246 stats.getNativeLatencyMillis(), 247 stats.getNativeDocumentStoreLatencyMillis(), 248 stats.getNativeIndexLatencyMillis(), 249 stats.getNativeIndexMergeLatencyMillis(), 250 stats.getNativeDocumentSizeBytes(), 251 stats.getNativeNumTokensIndexed(), 252 stats.getNativeExceededMaxNumTokens()); 253 } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { 254 // TODO(b/184204720) report hashing error to statsd 255 // We need to set a special value(e.g. 0xFFFFFFFF) for the hashing of the database, 256 // so in the dashboard we know there is some error for hashing. 257 // 258 // Something is wrong while calculating the hash code for database 259 // this shouldn't happen since we always use "MD5" and "UTF-8" 260 if (database != null) { 261 Log.e(TAG, "Error calculating hash code for database " + database, e); 262 } 263 } 264 } 265 266 @GuardedBy("mLock") logStatsImplLocked(@onNull SearchStats stats)267 private void logStatsImplLocked(@NonNull SearchStats stats) { 268 mLastPushTimeMillisLocked = SystemClock.elapsedRealtime(); 269 ExtraStats extraStats = createExtraStatsLocked(stats.getPackageName(), 270 CallStats.CALL_TYPE_SEARCH); 271 String database = stats.getDatabase(); 272 try { 273 int hashCodeForDatabase = calculateHashCodeMd5(database); 274 AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_QUERY_STATS_REPORTED, 275 extraStats.mSamplingInterval, 276 extraStats.mSkippedSampleCount, 277 extraStats.mPackageUid, 278 hashCodeForDatabase, 279 stats.getStatusCode(), 280 stats.getTotalLatencyMillis(), 281 stats.getRewriteSearchSpecLatencyMillis(), 282 stats.getRewriteSearchResultLatencyMillis(), 283 stats.getVisibilityScope(), 284 stats.getNativeLatencyMillis(), 285 stats.getTermCount(), 286 stats.getQueryLength(), 287 stats.getFilteredNamespaceCount(), 288 stats.getFilteredSchemaTypeCount(), 289 stats.getRequestedPageSize(), 290 stats.getCurrentPageReturnedResultCount(), 291 stats.isFirstPage(), 292 stats.getParseQueryLatencyMillis(), 293 stats.getRankingStrategy(), 294 stats.getScoredDocumentCount(), 295 stats.getScoringLatencyMillis(), 296 stats.getRankingLatencyMillis(), 297 stats.getDocumentRetrievingLatencyMillis(), 298 stats.getResultWithSnippetsCount()); 299 } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { 300 // TODO(b/184204720) report hashing error to statsd 301 // We need to set a special value(e.g. 0xFFFFFFFF) for the hashing of the database, 302 // so in the dashboard we know there is some error for hashing. 303 // 304 // Something is wrong while calculating the hash code for database 305 // this shouldn't happen since we always use "MD5" and "UTF-8" 306 if (database != null) { 307 Log.e(TAG, "Error calculating hash code for database " + database, e); 308 } 309 } 310 } 311 312 @GuardedBy("mLock") logStatsImplLocked(@onNull InitializeStats stats)313 private void logStatsImplLocked(@NonNull InitializeStats stats) { 314 mLastPushTimeMillisLocked = SystemClock.elapsedRealtime(); 315 ExtraStats extraStats = createExtraStatsLocked(/*packageName=*/ null, 316 CallStats.CALL_TYPE_INITIALIZE); 317 AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_INITIALIZE_STATS_REPORTED, 318 extraStats.mSamplingInterval, 319 extraStats.mSkippedSampleCount, 320 extraStats.mPackageUid, 321 stats.getStatusCode(), 322 stats.getTotalLatencyMillis(), 323 stats.hasDeSync(), 324 stats.getPrepareSchemaAndNamespacesLatencyMillis(), 325 stats.getPrepareVisibilityStoreLatencyMillis(), 326 stats.getNativeLatencyMillis(), 327 stats.getDocumentStoreRecoveryCause(), 328 stats.getIndexRestorationCause(), 329 stats.getSchemaStoreRecoveryCause(), 330 stats.getDocumentStoreRecoveryLatencyMillis(), 331 stats.getIndexRestorationLatencyMillis(), 332 stats.getSchemaStoreRecoveryLatencyMillis(), 333 stats.getDocumentStoreDataStatus(), 334 stats.getDocumentCount(), 335 stats.getSchemaTypeCount(), 336 stats.hasReset(), 337 stats.getResetStatusCode()); 338 } 339 340 @GuardedBy("mLock") logStatsImplLocked(@onNull OptimizeStats stats)341 private void logStatsImplLocked(@NonNull OptimizeStats stats) { 342 mLastPushTimeMillisLocked = SystemClock.elapsedRealtime(); 343 ExtraStats extraStats = createExtraStatsLocked(/*packageName=*/ null, 344 CallStats.CALL_TYPE_OPTIMIZE); 345 AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_OPTIMIZE_STATS_REPORTED, 346 extraStats.mSamplingInterval, 347 extraStats.mSkippedSampleCount, 348 stats.getStatusCode(), 349 stats.getTotalLatencyMillis(), 350 stats.getNativeLatencyMillis(), 351 stats.getDocumentStoreOptimizeLatencyMillis(), 352 stats.getIndexRestorationLatencyMillis(), 353 stats.getOriginalDocumentCount(), 354 stats.getDeletedDocumentCount(), 355 stats.getExpiredDocumentCount(), 356 stats.getStorageSizeBeforeBytes(), 357 stats.getStorageSizeAfterBytes(), 358 stats.getTimeSinceLastOptimizeMillis()); 359 } 360 361 /** 362 * Calculate the hash code as an integer by returning the last four bytes of its MD5. 363 * 364 * @param str a string 365 * @return hash code as an integer. returns -1 if str is null. 366 * @throws AppSearchException if either algorithm or encoding does not exist. 367 */ 368 @VisibleForTesting 369 @NonNull calculateHashCodeMd5(@ullable String str)370 static int calculateHashCodeMd5(@Nullable String str) throws 371 NoSuchAlgorithmException, UnsupportedEncodingException { 372 if (str == null) { 373 // Just return -1 if caller doesn't have database name 374 // For some stats like globalQuery, databaseName can be null. 375 // Since in atom it is an integer, we have to return something here. 376 return -1; 377 } 378 379 MessageDigest md = MessageDigest.getInstance("MD5"); 380 md.update(str.getBytes(/*charsetName=*/ "UTF-8")); 381 byte[] digest = md.digest(); 382 383 // Since MD5 generates 16 bytes digest, we don't need to check the length here to see 384 // if it is smaller than sizeof(int)(4). 385 // 386 // We generate the same value as BigInteger(digest).intValue(). 387 // BigInteger takes bytes[] and treat it as big endian. And its intValue() would get the 388 // lower 4 bytes. So here we take the last 4 bytes and treat them as big endian. 389 return (digest[12] & 0xFF) << 24 390 | (digest[13] & 0xFF) << 16 391 | (digest[14] & 0xFF) << 8 392 | (digest[15] & 0xFF); 393 } 394 395 /** 396 * Creates {@link ExtraStats} to hold additional information generated for logging. 397 * 398 * <p>This method is called by most of logStatsImplLocked functions to reduce code 399 * duplication. 400 */ 401 // TODO(b/173532925) Once we add CTS test for logging atoms and can inspect the result, we can 402 // remove this @VisibleForTesting and directly use PlatformLogger.logStats to test sampling and 403 // rate limiting. 404 @VisibleForTesting 405 @GuardedBy("mLock") 406 @NonNull createExtraStatsLocked(@ullable String packageName, @CallStats.CallType int callType)407 ExtraStats createExtraStatsLocked(@Nullable String packageName, 408 @CallStats.CallType int callType) { 409 int packageUid = Process.INVALID_UID; 410 if (packageName != null) { 411 packageUid = getPackageUidAsUserLocked(packageName); 412 } 413 414 // The sampling ratio here might be different from the one used in 415 // shouldLogForTypeLocked if there is a config change in the middle. 416 // Since it is only one sample, we can just ignore this difference. 417 // Or we can retrieve samplingRatio at beginning and pass along 418 // as function parameter, but it will make code less cleaner with some duplication. 419 int samplingInterval = getSamplingIntervalFromConfig(callType); 420 int skippedSampleCount = mSkippedSampleCountLocked.get(callType, 421 /*valueOfKeyIfNotFound=*/ 0); 422 mSkippedSampleCountLocked.put(callType, 0); 423 424 return new ExtraStats(packageUid, samplingInterval, skippedSampleCount); 425 } 426 427 /** 428 * Checks if this stats should be logged. 429 * 430 * <p>It won't be logged if it is "sampled" out, or it is too close to the previous logged 431 * stats. 432 */ 433 @GuardedBy("mLock") 434 // TODO(b/173532925) Once we add CTS test for logging atoms and can inspect the result, we can 435 // remove this @VisibleForTesting and directly use PlatformLogger.logStats to test sampling and 436 // rate limiting. 437 @VisibleForTesting shouldLogForTypeLocked(@allStats.CallType int callType)438 boolean shouldLogForTypeLocked(@CallStats.CallType int callType) { 439 int samplingInterval = getSamplingIntervalFromConfig(callType); 440 // Sampling 441 if (!shouldSample(samplingInterval)) { 442 return false; 443 } 444 445 // Rate limiting 446 // Check the timestamp to see if it is too close to last logged sample 447 long currentTimeMillis = SystemClock.elapsedRealtime(); 448 if (mLastPushTimeMillisLocked 449 > currentTimeMillis - mConfig.getCachedMinTimeIntervalBetweenSamplesMillis()) { 450 int count = mSkippedSampleCountLocked.get(callType, /*valueOfKeyIfNotFound=*/ 0); 451 ++count; 452 mSkippedSampleCountLocked.put(callType, count); 453 return false; 454 } 455 456 return true; 457 } 458 459 /** 460 * Checks if the stats should be "sampled" 461 * 462 * @param samplingInterval sampling interval 463 * @return if the stats should be sampled 464 */ shouldSample(int samplingInterval)465 private boolean shouldSample(int samplingInterval) { 466 if (samplingInterval <= 0) { 467 return false; 468 } 469 470 return mRng.nextInt((int) samplingInterval) == 0; 471 } 472 473 /** 474 * Finds the UID of the {@code packageName}. Returns {@link Process#INVALID_UID} if unable to 475 * find the UID. 476 */ 477 @GuardedBy("mLock") getPackageUidAsUserLocked(@onNull String packageName)478 private int getPackageUidAsUserLocked(@NonNull String packageName) { 479 Integer packageUid = mPackageUidCacheLocked.get(packageName); 480 if (packageUid == null) { 481 packageUid = PackageUtil.getPackageUid(mUserContext, packageName); 482 if (packageUid != Process.INVALID_UID) { 483 mPackageUidCacheLocked.put(packageName, packageUid); 484 } 485 } 486 return packageUid; 487 } 488 489 /** Returns sampling ratio for stats type specified form {@link AppSearchConfig}. */ getSamplingIntervalFromConfig(@allStats.CallType int statsType)490 private int getSamplingIntervalFromConfig(@CallStats.CallType int statsType) { 491 switch (statsType) { 492 case CallStats.CALL_TYPE_PUT_DOCUMENTS: 493 case CallStats.CALL_TYPE_GET_DOCUMENTS: 494 case CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID: 495 case CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH: 496 return mConfig.getCachedSamplingIntervalForBatchCallStats(); 497 case CallStats.CALL_TYPE_PUT_DOCUMENT: 498 return mConfig.getCachedSamplingIntervalForPutDocumentStats(); 499 case CallStats.CALL_TYPE_INITIALIZE: 500 return mConfig.getCachedSamplingIntervalForInitializeStats(); 501 case CallStats.CALL_TYPE_SEARCH: 502 return mConfig.getCachedSamplingIntervalForSearchStats(); 503 case CallStats.CALL_TYPE_GLOBAL_SEARCH: 504 return mConfig.getCachedSamplingIntervalForGlobalSearchStats(); 505 case CallStats.CALL_TYPE_OPTIMIZE: 506 return mConfig.getCachedSamplingIntervalForOptimizeStats(); 507 case CallStats.CALL_TYPE_UNKNOWN: 508 case CallStats.CALL_TYPE_SET_SCHEMA: 509 case CallStats.CALL_TYPE_GET_DOCUMENT: 510 case CallStats.CALL_TYPE_REMOVE_DOCUMENT_BY_ID: 511 case CallStats.CALL_TYPE_FLUSH: 512 case CallStats.CALL_TYPE_REMOVE_DOCUMENT_BY_SEARCH: 513 // TODO(b/173532925) Some of them above will have dedicated sampling ratio config 514 default: 515 return mConfig.getCachedSamplingIntervalDefault(); 516 } 517 } 518 519 // 520 // Functions below are used for tests only 521 // 522 @VisibleForTesting 523 @GuardedBy("mLock") setLastPushTimeMillisLocked(long lastPushElapsedTimeMillis)524 void setLastPushTimeMillisLocked(long lastPushElapsedTimeMillis) { 525 mLastPushTimeMillisLocked = lastPushElapsedTimeMillis; 526 } 527 } 528