diff --git a/pom.xml b/pom.xml
index 7fa211962..e5ee5181f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -138,7 +138,7 @@
org.junit.jupiter
- junit-jupiter-api
+ junit-jupiter
test
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
index 97d47be60..89abd4b12 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
@@ -110,6 +110,7 @@ import org.whispersystems.textsecuregcm.metrics.MaxFileDescriptorGauge;
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
+import org.whispersystems.textsecuregcm.metrics.NstatCounters;
import org.whispersystems.textsecuregcm.metrics.OperatingSystemMemoryGauge;
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager;
import org.whispersystems.textsecuregcm.metrics.TrafficSource;
@@ -221,7 +222,7 @@ public class WhisperServerService extends Application webSocketEnvironment, WebSocketEnvironment provisioningEnvironment) {
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NstatCounters.java b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NstatCounters.java
new file mode 100644
index 000000000..aa5e56a57
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NstatCounters.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2021 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.metrics;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.annotations.VisibleForTesting;
+import io.micrometer.core.instrument.Metrics;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NstatCounters {
+
+ private final Map networkStatistics = new ConcurrentHashMap<>();
+
+ private static final String[] NSTAT_COMMAND_LINE = new String[] { "nstat", "--zero", "--json", "--noupdate", "--ignore" };
+ private static final String[] EXCLUDE_METRIC_NAME_PREFIXES = new String[] { "Icmp", "Udp", "Ip6" };
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ private static final Logger log = LoggerFactory.getLogger(NstatCounters.class);
+
+ @VisibleForTesting
+ static class NetworkStatistics {
+ private final Map kernelStatistics;
+
+ @JsonCreator
+ private NetworkStatistics(@JsonProperty("kernel") final Map kernelStatistics) {
+ this.kernelStatistics = kernelStatistics;
+ }
+
+ public Map getKernelStatistics() {
+ return kernelStatistics;
+ }
+ }
+
+ public void registerMetrics(final ScheduledExecutorService refreshService, final Duration refreshInterval) {
+ refreshNetworkStatistics();
+
+ networkStatistics.keySet().stream()
+ .filter(NstatCounters::shouldIncludeMetric)
+ .forEach(metricName -> Metrics.globalRegistry.more().counter(name(getClass(), "kernel", metricName),
+ Collections.emptyList(), networkStatistics, statistics -> statistics.get(metricName)));
+
+ refreshService.scheduleAtFixedRate(this::refreshNetworkStatistics,
+ refreshInterval.toMillis(), refreshInterval.toMillis(), TimeUnit.MILLISECONDS);
+ }
+
+ private void refreshNetworkStatistics() {
+ try {
+ networkStatistics.putAll(loadNetworkStatistics().getKernelStatistics());
+ } catch (final InterruptedException | IOException e) {
+ log.warn("Failed to refresh network statistics", e);
+ }
+ }
+
+ @VisibleForTesting
+ static boolean shouldIncludeMetric(final String metricName) {
+ for (final String prefix : EXCLUDE_METRIC_NAME_PREFIXES) {
+ if (metricName.startsWith(prefix)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @VisibleForTesting
+ static NetworkStatistics loadNetworkStatistics() throws IOException, InterruptedException {
+ final Process nstatProcess = Runtime.getRuntime().exec(NSTAT_COMMAND_LINE);
+
+ if (nstatProcess.waitFor() == 0) {
+ return OBJECT_MAPPER.readValue(nstatProcess.getInputStream(), NetworkStatistics.class);
+ } else {
+ throw new IOException("nstat process did not exit normally");
+ }
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/metrics/NstatCountersTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/NstatCountersTest.java
new file mode 100644
index 000000000..b1b565af9
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/metrics/NstatCountersTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.metrics;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.whispersystems.textsecuregcm.metrics.NstatCounters.NetworkStatistics;
+
+import java.io.IOException;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+class NstatCountersTest {
+
+ @Test
+ @EnabledOnOs(OS.LINUX)
+ void loadNetworkStatistics() throws IOException, InterruptedException {
+ final NetworkStatistics networkStatistics = NstatCounters.loadNetworkStatistics();
+
+ assertNotNull(networkStatistics.getKernelStatistics());
+ assertFalse(networkStatistics.getKernelStatistics().isEmpty());
+ }
+
+ @ParameterizedTest
+ @MethodSource("shouldIncludeMetricNameProvider")
+ void shouldIncludeMetric(final String metricName, final boolean expectInclude) {
+ assertEquals(expectInclude, NstatCounters.shouldIncludeMetric(metricName));
+ }
+
+ static Stream shouldIncludeMetricNameProvider() {
+ return Stream.of(Arguments.of("IpInReceives", true),
+ Arguments.of("TcpActiveOpens", true),
+ Arguments.of("UdpInDatagrams", false),
+ Arguments.of("Ip6InReceives", false),
+ Arguments.of("Udp6InDatagrams", false),
+ Arguments.of("TcpExtSyncookiesSent", true),
+ Arguments.of("IpExtInNoRoutes", true));
+ }
+}