summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFrank Li <lifr@google.com>2020-06-22 12:45:27 +0000
committerRemi NGUYEN VAN <reminv@google.com>2020-07-03 14:56:43 +0900
commite04f8b7dfb9d7efa7f87de3859014d3e7eb524f2 (patch)
treefde057f1d2620e1dd9c9c6ad7351ce08b537cfd0 /src
parentb002fd1e07d040b9c08852f63cfee2dbb7e811fe (diff)
Injecting network validation stats into statsd
1. Fill in each field of the NetworkValidationReported 2. Write the NetworkValidationReported into statsd This patch also refactors tryCapportApiProbe to return null when the capport data is incorrect, instead of doing the check after calling the method. This makes it easier to compile capport API probe metrics. Test: atest NetworkStackIntegrationTests NetworkStackTests Test: atest FrameworksNetTests Test: Manual test with statsd_testdrive Bug: 151796056 Original-Change: https://android-review.googlesource.com/1295496 Merged-In: Icf34402d6a293cc76c32d00835cbf358c99a87fa Change-Id: Icf34402d6a293cc76c32d00835cbf358c99a87fa
Diffstat (limited to 'src')
-rw-r--r--src/com/android/networkstack/metrics/NetworkValidationMetrics.java220
-rwxr-xr-xsrc/com/android/server/connectivity/NetworkMonitor.java94
2 files changed, 302 insertions, 12 deletions
diff --git a/src/com/android/networkstack/metrics/NetworkValidationMetrics.java b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java
new file mode 100644
index 0000000..3789c5c
--- /dev/null
+++ b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.metrics;
+
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+
+import static java.lang.System.currentTimeMillis;
+
+import android.net.INetworkMonitor;
+import android.net.NetworkCapabilities;
+import android.net.captiveportal.CaptivePortalProbeResult;
+import android.net.metrics.ValidationProbeEvent;
+import android.net.util.NetworkStackUtils;
+import android.net.util.Stopwatch;
+import android.stats.connectivity.ProbeResult;
+import android.stats.connectivity.ProbeType;
+import android.stats.connectivity.TransportType;
+import android.stats.connectivity.ValidationResult;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.networkstack.apishim.common.CaptivePortalDataShim;
+
+/**
+ * Class to record the network validation into statsd.
+ * 1. Fill in NetworkValidationReported proto.
+ * 2. Write the NetworkValidationReported proto into statsd.
+ * @hide
+ */
+
+public class NetworkValidationMetrics {
+ private final NetworkValidationReported.Builder mStatsBuilder =
+ NetworkValidationReported.newBuilder();
+ private final ProbeEvents.Builder mProbeEventsBuilder = ProbeEvents.newBuilder();
+ private final CapportApiData.Builder mCapportApiDataBuilder = CapportApiData.newBuilder();
+ private final Stopwatch mWatch = new Stopwatch();
+ private int mValidationIndex = 0;
+ // Define a maximum size that can store events.
+ public static final int MAX_PROBE_EVENTS_COUNT = 20;
+
+ /**
+ * Reset this NetworkValidationMetrics.
+ */
+ public void reset(@Nullable NetworkCapabilities nc) {
+ mStatsBuilder.clear();
+ mProbeEventsBuilder.clear();
+ mCapportApiDataBuilder.clear();
+ mWatch.restart();
+ mStatsBuilder.setTransportType(getTransportTypeFromNC(nc));
+ mValidationIndex++;
+ }
+
+ /**
+ * Returns the enum TransportType
+ *
+ * @param NetworkCapabilities
+ * @return the TransportType which is defined in
+ * core/proto/android/stats/connectivity/network_stack.proto
+ */
+ @VisibleForTesting
+ public static TransportType getTransportTypeFromNC(
+ @Nullable NetworkCapabilities nc) {
+ if (nc == null) return TransportType.TT_UNKNOWN;
+ boolean hasCellular = nc.hasTransport(TRANSPORT_CELLULAR);
+ boolean hasWifi = nc.hasTransport(TRANSPORT_WIFI);
+ boolean hasBT = nc.hasTransport(TRANSPORT_BLUETOOTH);
+ boolean hasEthernet = nc.hasTransport(TRANSPORT_ETHERNET);
+ boolean hasVpn = nc.hasTransport(TRANSPORT_VPN);
+ boolean hasWifiAware = nc.hasTransport(TRANSPORT_WIFI_AWARE);
+ boolean hasLopan = nc.hasTransport(TRANSPORT_LOWPAN);
+
+ if (hasCellular && hasWifi && hasVpn) return TransportType.TT_WIFI_CELLULAR_VPN;
+ if (hasWifi) return hasVpn ? TransportType.TT_WIFI_VPN : TransportType.TT_WIFI;
+ if (hasCellular) return hasVpn ? TransportType.TT_CELLULAR_VPN : TransportType.TT_CELLULAR;
+ if (hasBT) return hasVpn ? TransportType.TT_BLUETOOTH_VPN : TransportType.TT_BLUETOOTH;
+ if (hasEthernet) return hasVpn ? TransportType.TT_ETHERNET_VPN : TransportType.TT_ETHERNET;
+ if (hasWifiAware) return TransportType.TT_WIFI_AWARE;
+ if (hasLopan) return TransportType.TT_LOWPAN;
+ return TransportType.TT_UNKNOWN;
+ }
+
+ /**
+ * Map {@link ValidationProbeEvent} to {@link ProbeType}.
+ */
+ public static ProbeType probeTypeToEnum(final int probeType) {
+ switch(probeType) {
+ case ValidationProbeEvent.PROBE_DNS:
+ return ProbeType.PT_DNS;
+ case ValidationProbeEvent.PROBE_HTTP:
+ return ProbeType.PT_HTTP;
+ case ValidationProbeEvent.PROBE_HTTPS:
+ return ProbeType.PT_HTTPS;
+ case ValidationProbeEvent.PROBE_PAC:
+ return ProbeType.PT_PAC;
+ case ValidationProbeEvent.PROBE_FALLBACK:
+ return ProbeType.PT_FALLBACK;
+ case ValidationProbeEvent.PROBE_PRIVDNS:
+ return ProbeType.PT_PRIVDNS;
+ default:
+ return ProbeType.PT_UNKNOWN;
+ }
+ }
+
+ /**
+ * Map {@link CaptivePortalProbeResult} to {@link ProbeResult}.
+ */
+ public static ProbeResult httpProbeResultToEnum(final CaptivePortalProbeResult result) {
+ if (result == null) return ProbeResult.PR_UNKNOWN;
+
+ if (result.isSuccessful()) {
+ return ProbeResult.PR_SUCCESS;
+ } else if (result.isDnsPrivateIpResponse()) {
+ return ProbeResult.PR_PRIVATE_IP_DNS;
+ } else if (result.isFailed()) {
+ return ProbeResult.PR_FAILURE;
+ } else if (result.isPortal()) {
+ return ProbeResult.PR_PORTAL;
+ } else {
+ return ProbeResult.PR_UNKNOWN;
+ }
+ }
+
+ /**
+ * Map validation result (as per INetworkMonitor) to {@link ValidationResult}.
+ */
+ @VisibleForTesting
+ public static ValidationResult validationResultToEnum(int result, String redirectUrl) {
+ if ((result & INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID) != 0) {
+ return ValidationResult.VR_SUCCESS;
+ } else if (redirectUrl != null) {
+ return ValidationResult.VR_PORTAL;
+ } else if ((result & INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL) != 0) {
+ return ValidationResult.VR_PARTIAL;
+ } else {
+ return ValidationResult.VR_FAILURE;
+ }
+ }
+
+ /**
+ * Write each network probe event to mProbeEventsBuilder.
+ */
+ public void setProbeEvent(final ProbeType type, final long durationUs, final ProbeResult result,
+ @Nullable final CaptivePortalDataShim capportData) {
+ // When the number of ProbeEvents of mProbeEventsBuilder exceeds
+ // MAX_PROBE_EVENTS_COUNT, stop adding ProbeEvent.
+ if (mProbeEventsBuilder.getProbeEventCount() >= MAX_PROBE_EVENTS_COUNT) return;
+
+ int latencyUs = NetworkStackUtils.saturatedCast(durationUs);
+
+ final ProbeEvent.Builder probeEventBuilder = ProbeEvent.newBuilder()
+ .setLatencyMicros(latencyUs)
+ .setProbeType(type)
+ .setProbeResult(result);
+
+ if (capportData != null) {
+ final long secondsRemaining =
+ (capportData.getExpiryTimeMillis() - currentTimeMillis()) / 1000;
+ mCapportApiDataBuilder
+ .setRemainingTtlSecs(NetworkStackUtils.saturatedCast(secondsRemaining))
+ .setRemainingBytes(NetworkStackUtils.saturatedCast(capportData.getByteLimit()))
+ .setHasPortalUrl((capportData.getUserPortalUrl() != null))
+ .setHasVenueInfo((capportData.getVenueInfoUrl() != null));
+ probeEventBuilder.setCapportApiData(mCapportApiDataBuilder);
+ }
+
+ mProbeEventsBuilder.addProbeEvent(probeEventBuilder);
+ }
+
+ /**
+ * Write the network validation info to mStatsBuilder.
+ */
+ public void setValidationResult(int result, String redirectUrl) {
+ mStatsBuilder.setValidationResult(validationResultToEnum(result, redirectUrl));
+ }
+
+ /**
+ * Write the NetworkValidationReported proto to statsd.
+ */
+ public NetworkValidationReported sendValidationStats() {
+ if (!mWatch.isStarted()) return null;
+ mStatsBuilder.setProbeEvents(mProbeEventsBuilder);
+ mStatsBuilder.setLatencyMicros(NetworkStackUtils.saturatedCast(mWatch.stop()));
+ mStatsBuilder.setValidationIndex(mValidationIndex);
+ // write a random value(0 ~ 999) for sampling.
+ mStatsBuilder.setRandomNumber((int) (Math.random() * 1000));
+ final NetworkValidationReported mStats = mStatsBuilder.build();
+ final byte[] probeEvents = mStats.getProbeEvents().toByteArray();
+
+ NetworkStackStatsLog.write(NetworkStackStatsLog.NETWORK_VALIDATION_REPORTED,
+ mStats.getTransportType().getNumber(),
+ probeEvents,
+ mStats.getValidationResult().getNumber(),
+ mStats.getLatencyMicros(),
+ mStats.getValidationIndex(),
+ mStats.getRandomNumber());
+ mWatch.reset();
+ return mStats;
+ }
+}
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index b6b1fff..47c910a 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -129,6 +129,8 @@ import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.Settings;
+import android.stats.connectivity.ProbeResult;
+import android.stats.connectivity.ProbeType;
import android.telephony.AccessNetworkConstants;
import android.telephony.CellIdentityNr;
import android.telephony.CellInfo;
@@ -155,6 +157,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.RingBufferIndices;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -168,6 +171,7 @@ import com.android.networkstack.apishim.common.ShimUtils;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.networkstack.metrics.DataStallDetectionStats;
import com.android.networkstack.metrics.DataStallStatsUtils;
+import com.android.networkstack.metrics.NetworkValidationMetrics;
import com.android.networkstack.netlink.TcpSocketTracker;
import com.android.networkstack.util.DnsUtils;
import com.android.server.NetworkStackService.NetworkStackServiceManager;
@@ -500,6 +504,10 @@ public class NetworkMonitor extends StateMachine {
private final boolean mPrivateIpNoInternetEnabled;
+ @GuardedBy("mNetworkValidationMetrics")
+ private final NetworkValidationMetrics mNetworkValidationMetrics =
+ new NetworkValidationMetrics();
+
private int getCallbackVersion(INetworkMonitorCallbacks cb) {
int version;
try {
@@ -775,6 +783,31 @@ public class NetworkMonitor extends StateMachine {
}
}
+ private void recordMetricsReset(@Nullable NetworkCapabilities nc) {
+ synchronized (mNetworkValidationMetrics) {
+ mNetworkValidationMetrics.reset(nc);
+ }
+ }
+
+ private void recordMetricsProbeEvent(ProbeType type, long latencyMicros, ProbeResult result,
+ CaptivePortalDataShim capportData) {
+ synchronized (mNetworkValidationMetrics) {
+ mNetworkValidationMetrics.setProbeEvent(type, latencyMicros, result, capportData);
+ }
+ }
+
+ private void recordMetricsValidationResult(int result, String redirectUrl) {
+ synchronized (mNetworkValidationMetrics) {
+ mNetworkValidationMetrics.setValidationResult(result, redirectUrl);
+ }
+ }
+
+ private void recordMetricsValidationStats() {
+ synchronized (mNetworkValidationMetrics) {
+ mNetworkValidationMetrics.sendValidationStats();
+ }
+ }
+
// DefaultState is the parent of all States. It exists only to handle CMD_* messages but
// does not entail any real state (hence no enter() or exit() routines).
private class DefaultState extends State {
@@ -787,6 +820,7 @@ public class NetworkMonitor extends StateMachine {
transitionTo(mEvaluatingState);
return HANDLED;
case CMD_NETWORK_DISCONNECTED:
+ recordMetricsValidationStats();
logNetworkEvent(NetworkEvent.NETWORK_DISCONNECTED);
quit();
return HANDLED;
@@ -938,6 +972,7 @@ public class NetworkMonitor extends StateMachine {
initSocketTrackingIfRequired();
// start periodical polling.
sendTcpPollingEvent();
+ recordMetricsValidationStats();
}
private void initSocketTrackingIfRequired() {
@@ -957,6 +992,9 @@ public class NetworkMonitor extends StateMachine {
transitionTo(mValidatedState);
break;
case CMD_EVALUATE_PRIVATE_DNS:
+ // TODO: this causes reevaluation of a single probe that is not counted in
+ // metrics. Add support for such reevaluation probes in metrics, and log them
+ // separately.
transitionTo(mEvaluatingPrivateDnsState);
break;
case EVENT_DNS_NOTIFICATION:
@@ -1285,6 +1323,7 @@ public class NetworkMonitor extends StateMachine {
sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */,
CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
mValidations++;
+ recordMetricsValidationStats();
}
@Override
@@ -1316,6 +1355,10 @@ public class NetworkMonitor extends StateMachine {
notifyPrivateDnsConfigResolved();
} else {
handlePrivateDnsEvaluationFailure();
+ // The private DNS probe fails-fast if the server hostname cannot
+ // be resolved. Record it as a failure with zero latency.
+ recordMetricsProbeEvent(ProbeType.PT_PRIVDNS, 0 /* latency */,
+ ProbeResult.PR_FAILURE, null /* capportData */);
break;
}
}
@@ -1425,6 +1468,8 @@ public class NetworkMonitor extends StateMachine {
validationLog(PROBE_PRIVDNS, host,
String.format("%dus - Error: %s", time, uhe.getMessage()));
}
+ recordMetricsProbeEvent(ProbeType.PT_PRIVDNS, time, success ? ProbeResult.PR_SUCCESS :
+ ProbeResult.PR_FAILURE, null /* capportData */);
logValidationProbe(time, PROBE_PRIVDNS, success ? DNS_SUCCESS : DNS_FAILURE);
return success;
}
@@ -1435,6 +1480,8 @@ public class NetworkMonitor extends StateMachine {
@Override
public void enter() {
+ recordMetricsValidationStats();
+ recordMetricsReset(mNetworkCapabilities);
if (mEvaluateAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) {
//Don't continue to blame UID forever.
TrafficStats.clearThreadStatsUid();
@@ -1516,6 +1563,7 @@ public class NetworkMonitor extends StateMachine {
private class WaitingForNextProbeState extends State {
@Override
public void enter() {
+ recordMetricsValidationStats();
scheduleNextProbe();
}
@@ -2265,6 +2313,8 @@ public class NetworkMonitor extends StateMachine {
// network validation (the HTTPS probe, which would likely fail anyway) or the PAC probe.
if (mPrivateIpNoInternetEnabled && probeType == ValidationProbeEvent.PROBE_HTTP
&& (proxy == null) && hasPrivateIpAddress(resolvedAddr)) {
+ recordMetricsProbeEvent(NetworkValidationMetrics.probeTypeToEnum(probeType),
+ 0 /* latency */, ProbeResult.PR_PRIVATE_IP_DNS, null /* capportData */);
return CaptivePortalProbeResult.PRIVATE_IP;
}
return sendHttpProbe(url, probeType, null);
@@ -2296,6 +2346,9 @@ public class NetworkMonitor extends StateMachine {
result = ValidationProbeEvent.DNS_FAILURE;
}
final long latency = watch.stop();
+ recordMetricsProbeEvent(ProbeType.PT_DNS, latency,
+ (result == ValidationProbeEvent.DNS_SUCCESS) ? ProbeResult.PR_SUCCESS :
+ ProbeResult.PR_FAILURE, null /* capportData */);
logValidationProbe(latency, ValidationProbeEvent.PROBE_DNS, result);
return addresses;
}
@@ -2413,12 +2466,17 @@ public class NetworkMonitor extends StateMachine {
}
logValidationProbe(probeTimer.stop(), probeType, httpResponseCode);
+ final CaptivePortalProbeResult probeResult;
if (probeSpec == null) {
- return new CaptivePortalProbeResult(httpResponseCode, redirectUrl, url.toString(),
- 1 << probeType);
+ probeResult = new CaptivePortalProbeResult(httpResponseCode, redirectUrl,
+ url.toString(), 1 << probeType);
} else {
- return probeSpec.getResult(httpResponseCode, redirectUrl);
+ probeResult = probeSpec.getResult(httpResponseCode, redirectUrl);
}
+ recordMetricsProbeEvent(NetworkValidationMetrics.probeTypeToEnum(probeType),
+ probeTimer.stop(), NetworkValidationMetrics.httpProbeResultToEnum(probeResult),
+ null /* capportData */);
+ return probeResult;
}
@VisibleForTesting
@@ -2570,8 +2628,7 @@ public class NetworkMonitor extends StateMachine {
super(deps, proxy, url, captivePortalApiUrl);
}
- private CaptivePortalDataShim tryCapportApiProbe() {
- if (mCaptivePortalApiUrl == null) return null;
+ private CaptivePortalDataShim sendCapportApiProbe() {
validationLog("Fetching captive portal data from " + mCaptivePortalApiUrl);
final String apiContent;
@@ -2610,26 +2667,38 @@ public class NetworkMonitor extends StateMachine {
try {
final JSONObject info = new JSONObject(apiContent);
- return CaptivePortalDataShimImpl.fromJson(info);
+ final CaptivePortalDataShim capportData = CaptivePortalDataShimImpl.fromJson(info);
+ if (capportData != null && capportData.isCaptive()
+ && capportData.getUserPortalUrl() == null) {
+ validationLog("Missing user-portal-url from capport response");
+ return null;
+ }
+ return capportData;
} catch (JSONException e) {
validationLog("Could not parse capport API JSON: " + e.getMessage());
return null;
} catch (UnsupportedApiLevelException e) {
+ // This should never happen because LinkProperties would not have a capport URL
+ // before R.
validationLog("Platform API too low to support capport API");
return null;
}
}
+ private CaptivePortalDataShim tryCapportApiProbe() {
+ if (mCaptivePortalApiUrl == null) return null;
+ final Stopwatch capportApiWatch = new Stopwatch().start();
+ final CaptivePortalDataShim capportData = sendCapportApiProbe();
+ recordMetricsProbeEvent(ProbeType.PT_CAPPORT_API, capportApiWatch.stop(),
+ capportData == null ? ProbeResult.PR_FAILURE : ProbeResult.PR_SUCCESS,
+ capportData);
+ return capportData;
+ }
+
@Override
protected CaptivePortalProbeResult sendProbe() {
final CaptivePortalDataShim capportData = tryCapportApiProbe();
if (capportData != null && capportData.isCaptive()) {
- if (capportData.getUserPortalUrl() == null) {
- validationLog("Missing user-portal-url from capport response");
- return new CapportApiProbeResult(
- sendDnsAndHttpProbes(mProxy, mUrl, ValidationProbeEvent.PROBE_HTTP),
- null /* capportData */);
- }
final String loginUrlString = capportData.getUserPortalUrl().toString();
// Starting from R (where CaptivePortalData was introduced), the captive portal app
// delegates to NetworkMonitor for verifying when the network validates instead of
@@ -3343,6 +3412,7 @@ public class NetworkMonitor extends StateMachine {
p.redirectUrl = redirectUrl;
p.timestampMillis = SystemClock.elapsedRealtime();
notifyNetworkTested(p);
+ recordMetricsValidationResult(result, redirectUrl);
}
@VisibleForTesting