1 // Copyright 2023 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net.telemetry; 6 7 import static java.nio.charset.StandardCharsets.UTF_8; 8 9 import android.os.Build; 10 import android.util.Log; 11 12 import androidx.annotation.RequiresApi; 13 import androidx.annotation.VisibleForTesting; 14 15 import org.chromium.net.impl.CronetLogger; 16 17 import java.nio.ByteBuffer; 18 import java.security.MessageDigest; 19 import java.security.NoSuchAlgorithmException; 20 import java.util.concurrent.atomic.AtomicInteger; 21 22 /** Logger for logging cronet's telemetry */ 23 @RequiresApi(Build.VERSION_CODES.R) 24 public class CronetLoggerImpl extends CronetLogger { 25 private static final String TAG = CronetLoggerImpl.class.getSimpleName(); 26 27 private static final MessageDigest MD5_MESSAGE_DIGEST; 28 29 static { 30 MessageDigest messageDigest; 31 try { 32 messageDigest = MessageDigest.getInstance("MD5"); 33 } catch (NoSuchAlgorithmException e) { 34 Log.d(TAG, "Error while instantiating messageDigest", e); 35 messageDigest = null; 36 } 37 MD5_MESSAGE_DIGEST = messageDigest; 38 } 39 40 private final AtomicInteger mSamplesRateLimited = new AtomicInteger(); 41 private final RateLimiter mRateLimiter; 42 CronetLoggerImpl(int sampleRatePerSecond)43 public CronetLoggerImpl(int sampleRatePerSecond) { 44 this(new RateLimiter(sampleRatePerSecond)); 45 } 46 47 @VisibleForTesting CronetLoggerImpl(RateLimiter rateLimiter)48 public CronetLoggerImpl(RateLimiter rateLimiter) { 49 super(); 50 this.mRateLimiter = rateLimiter; 51 } 52 53 @Override logCronetEngineCreation( int cronetEngineId, CronetEngineBuilderInfo builder, CronetVersion version, CronetSource source)54 public void logCronetEngineCreation( 55 int cronetEngineId, 56 CronetEngineBuilderInfo builder, 57 CronetVersion version, 58 CronetSource source) { 59 if (builder == null || version == null || source == null) { 60 return; 61 } 62 63 writeCronetEngineCreation(cronetEngineId, builder, version, source); 64 } 65 66 @Override logCronetTrafficInfo(int cronetEngineId, CronetTrafficInfo trafficInfo)67 public void logCronetTrafficInfo(int cronetEngineId, CronetTrafficInfo trafficInfo) { 68 if (trafficInfo == null) { 69 return; 70 } 71 72 if (!mRateLimiter.tryAcquire()) { 73 mSamplesRateLimited.incrementAndGet(); 74 return; 75 } 76 77 writeCronetTrafficReported(cronetEngineId, trafficInfo, mSamplesRateLimited.getAndSet(0)); 78 } 79 80 @SuppressWarnings("CatchingUnchecked") writeCronetEngineCreation( long cronetEngineId, CronetEngineBuilderInfo builder, CronetVersion version, CronetSource source)81 public void writeCronetEngineCreation( 82 long cronetEngineId, 83 CronetEngineBuilderInfo builder, 84 CronetVersion version, 85 CronetSource source) { 86 try { 87 // Parse experimental Options 88 ExperimentalOptions experimentalOptions = 89 new ExperimentalOptions(builder.getExperimentalOptions()); 90 91 CronetStatsLog.write( 92 CronetStatsLog.CRONET_ENGINE_CREATED, 93 cronetEngineId, 94 version.getMajorVersion(), 95 version.getMinorVersion(), 96 version.getBuildVersion(), 97 version.getPatchVersion(), 98 convertToProtoCronetSource(source), 99 builder.isBrotliEnabled(), 100 builder.isHttp2Enabled(), 101 convertToProtoHttpCacheMode(builder.getHttpCacheMode()), 102 builder.isPublicKeyPinningBypassForLocalTrustAnchorsEnabled(), 103 builder.isQuicEnabled(), 104 builder.isNetworkQualityEstimatorEnabled(), 105 builder.getThreadPriority(), 106 // QUIC options 107 experimentalOptions.getConnectionOptionsOption(), 108 experimentalOptions.getStoreServerConfigsInPropertiesOption().getValue(), 109 experimentalOptions.getMaxServerConfigsStoredInPropertiesOption(), 110 experimentalOptions.getIdleConnectionTimeoutSecondsOption(), 111 experimentalOptions.getGoawaySessionsOnIpChangeOption().getValue(), 112 experimentalOptions.getCloseSessionsOnIpChangeOption().getValue(), 113 experimentalOptions.getMigrateSessionsOnNetworkChangeV2Option().getValue(), 114 experimentalOptions.getMigrateSessionsEarlyV2().getValue(), 115 experimentalOptions.getDisableBidirectionalStreamsOption().getValue(), 116 experimentalOptions.getMaxTimeBeforeCryptoHandshakeSecondsOption(), 117 experimentalOptions.getMaxIdleTimeBeforeCryptoHandshakeSecondsOption(), 118 experimentalOptions.getEnableSocketRecvOptimizationOption().getValue(), 119 // AsyncDNS 120 experimentalOptions.getAsyncDnsEnableOption().getValue(), 121 // StaleDNS 122 experimentalOptions.getStaleDnsEnableOption().getValue(), 123 experimentalOptions.getStaleDnsDelayMillisOption(), 124 experimentalOptions.getStaleDnsMaxExpiredTimeMillisOption(), 125 experimentalOptions.getStaleDnsMaxStaleUsesOption(), 126 experimentalOptions.getStaleDnsAllowOtherNetworkOption().getValue(), 127 experimentalOptions.getStaleDnsPersistToDiskOption().getValue(), 128 experimentalOptions.getStaleDnsPersistDelayMillisOption(), 129 experimentalOptions.getStaleDnsUseStaleOnNameNotResolvedOption().getValue(), 130 experimentalOptions.getDisableIpv6OnWifiOption().getValue(), 131 /* cronet_initialization_ref= */ -1); 132 } catch (Exception e) { // catching all exceptions since we don't want to crash the client 133 Log.d( 134 TAG, 135 String.format( 136 "Failed to log CronetEngine:%s creation: %s", 137 cronetEngineId, e.getMessage())); 138 } 139 } 140 141 @SuppressWarnings("CatchingUnchecked") 142 @VisibleForTesting writeCronetTrafficReported( long cronetEngineId, CronetTrafficInfo trafficInfo, int samplesRateLimitedCount)143 public void writeCronetTrafficReported( 144 long cronetEngineId, CronetTrafficInfo trafficInfo, int samplesRateLimitedCount) { 145 try { 146 CronetStatsLog.write( 147 CronetStatsLog.CRONET_TRAFFIC_REPORTED, 148 cronetEngineId, 149 SizeBuckets.calcRequestHeadersSizeBucket( 150 trafficInfo.getRequestHeaderSizeInBytes()), 151 SizeBuckets.calcRequestBodySizeBucket(trafficInfo.getRequestBodySizeInBytes()), 152 SizeBuckets.calcResponseHeadersSizeBucket( 153 trafficInfo.getResponseHeaderSizeInBytes()), 154 SizeBuckets.calcResponseBodySizeBucket( 155 trafficInfo.getResponseBodySizeInBytes()), 156 trafficInfo.getResponseStatusCode(), 157 hashNegotiatedProtocol(trafficInfo.getNegotiatedProtocol()), 158 (int) trafficInfo.getHeadersLatency().toMillis(), 159 (int) trafficInfo.getTotalLatency().toMillis(), 160 trafficInfo.wasConnectionMigrationAttempted(), 161 trafficInfo.didConnectionMigrationSucceed(), 162 samplesRateLimitedCount, 163 /* terminal_state= */ CronetStatsLog 164 .CRONET_TRAFFIC_REPORTED__TERMINAL_STATE__STATE_UNKNOWN, 165 /* user_callback_exception_count= */ -1, 166 /* total_idle_time_millis= */ -1, 167 /* total_user_executor_execute_latency_millis= */ -1, 168 /* read_count= */ -1, 169 /* on_upload_read_count= */ -1, 170 /* is_bidi_stream= */ CronetStatsLog 171 .CRONET_TRAFFIC_REPORTED__IS_BIDI_STREAM__OPTIONAL_BOOLEAN_UNSET); 172 } catch (Exception e) { 173 // using addAndGet because another thread might have modified samplesRateLimited's value 174 mSamplesRateLimited.addAndGet(samplesRateLimitedCount); 175 Log.d( 176 TAG, 177 String.format( 178 "Failed to log cronet traffic sample for CronetEngine %s: %s", 179 cronetEngineId, e.getMessage())); 180 } 181 } 182 convertToProtoCronetSource(CronetSource source)183 private static int convertToProtoCronetSource(CronetSource source) { 184 switch (source) { 185 case CRONET_SOURCE_STATICALLY_LINKED: 186 return CronetStatsLog 187 .CRONET_ENGINE_CREATED__SOURCE__CRONET_SOURCE_STATICALLY_LINKED; 188 case CRONET_SOURCE_PLAY_SERVICES: 189 return CronetStatsLog.CRONET_ENGINE_CREATED__SOURCE__CRONET_SOURCE_GMSCORE_DYNAMITE; 190 case CRONET_SOURCE_FALLBACK: 191 return CronetStatsLog.CRONET_ENGINE_CREATED__SOURCE__CRONET_SOURCE_FALLBACK; 192 case CRONET_SOURCE_UNSPECIFIED: 193 return CronetStatsLog.CRONET_ENGINE_CREATED__SOURCE__CRONET_SOURCE_UNSPECIFIED; 194 default: 195 return CronetStatsLog.CRONET_ENGINE_CREATED__SOURCE__CRONET_SOURCE_UNSPECIFIED; 196 } 197 } 198 convertToProtoHttpCacheMode(int httpCacheMode)199 private static int convertToProtoHttpCacheMode(int httpCacheMode) { 200 switch (httpCacheMode) { 201 case 0: 202 return CronetStatsLog.CRONET_ENGINE_CREATED__HTTP_CACHE_MODE__HTTP_CACHE_DISABLED; 203 case 1: 204 return CronetStatsLog.CRONET_ENGINE_CREATED__HTTP_CACHE_MODE__HTTP_CACHE_DISK; 205 case 2: 206 return CronetStatsLog 207 .CRONET_ENGINE_CREATED__HTTP_CACHE_MODE__HTTP_CACHE_DISK_NO_HTTP; 208 case 3: 209 return CronetStatsLog.CRONET_ENGINE_CREATED__HTTP_CACHE_MODE__HTTP_CACHE_IN_MEMORY; 210 default: 211 throw new IllegalArgumentException("Expected httpCacheMode to range from 0 to 3"); 212 } 213 } 214 hashNegotiatedProtocol(String protocol)215 private static long hashNegotiatedProtocol(String protocol) { 216 if (MD5_MESSAGE_DIGEST == null || protocol == null || protocol.isEmpty()) { 217 return 0L; 218 } 219 220 byte[] md = MD5_MESSAGE_DIGEST.digest(protocol.getBytes(UTF_8)); 221 return ByteBuffer.wrap(md).getLong(); 222 } 223 } 224